test-must-be-actionable
Test Failures Should Be Actionable⚑
- 테스트에 관한 다양한 모범 사례들과 규칙이 있지만, 집중해야 할 한 가지를 꼽자면 테스트의 실패는 반드시 행동 가능해야 한다는 것이다.
- 테스트가 실패했을 때, 테스트의 이름과 실패 메시지만으로도 조사를 시작할 수 있어야 한다.
Breaking Down Asserts and Tests for Clarity⚑
- 아래 예시를 생각해보면, 테스트가 실패했을 때, 왜 실패했는지 바로 알 수 없다.
- 한꺼번에 assertEqual 을 통해서 모든 것을 한꺼번에 테스트 하면, 테스트 실패의 원인을 파악하는데 어려움이 발생한다.
def test_it_parses_log_lines(self):
line = "2015-03-11T20:09:25|GET /foo?bar=baz|..."
parsed_dict = parse_line(line)
self.assertEqual({
"date": datetime(2015, 3, 11, 20, 9, 25),
"method": "GET",
"path": "/foo",
"query": "bar=baz",
}, parsed_dict)
AssertionError: {'date': datetime.datetime(2015, 3, 11, 20, 9, 25), 'path': '/foo', 'method': 'G [truncated]... != {'date': datetime.datetime(2015, 3, 11, 20, 9, 25), 'path': '/foo?', 'method': ' [truncated]...
- 따라서 아래와 같이 한 번에 하나씩 테스트하도록하게 만들어라
def test_it_parses_log_lines(self):
line = "2015-03-11T20:09:25|GET /foo?bar=baz|..."
parsed_dict = parse_line(line)
self.assertEqual(
datetime(2015, 3, 11, 20, 9, 25),
parsed_dict["date"],
)
self.assertEqual("GET", parsed_dict["method"])
self.assertEqual("/foo", parsed_dict["path"])
self.assertEqual("bar=baz", parsed_dict["query"])
AssertionError: '/foo' != '/foo?'
- 그러나 위 와 같이 assert 가 많아지면, 한 번에 깨지는 것들이 많아질 것이므로, 그냥 최소 단위로 나눠서 테스트하라고 조언한다.
- 물론 중복이 생길 수 있지만, 테스트의 실패를 바로 대응할 수 있도록 하는 것이 유지 보수에는 용이하다는 것
- 이런 방식으로 테스트를 작성하면, 테스트가 실패할 경우 어떤 행동을 해야하는지 즉각적으로 알고 행동할 수 있다.
def test_parse_request_method(self):
line = "2024-08-03T20:09:25|POST /foo?bar=baz|..."
parsed_dict = parse_line(line)
self.assertEqual("POST", parsed_dict["method"])
def test_parse_request_path(self):
line = "2024-08-03T20:09:25|POST /foo?bar=baz|..."
parsed_dict = parse_line(line)
self.assertEqual("/foo", parsed_dict["path"])
def test_parse_query_string(self):
line = "2024-08-03T20:09:25|POST /foo?bar=baz|..."
parsed_dict = parse_line(line)
self.assertEqual("bar=baz", parsed_dict["query"])
def test_parse_no_query_string(self):
line = "2024-08-03T20:09:25|POST /foo|..."
parsed_dict = parse_line(line)
self.assertIsNone(parsed_dict.get("query"))
Descriptive Test Names⚑
- 테스트 이름을 명확하게 작성하는 것은 테스트 작성 시 중요한 요소 중 하나이다.
- 다음의 예시를 통해 그 이유와 방법을 알아보자.
def test_is_user_locked_out_invalid_login(self):
authenticator.authenticate(username, invalid_password)
self.assertFalse(authenticator.is_user_locked_out(username))
authenticator.authenticate(username, invalid_password)
self.assertFalse(authenticator.is_user_locked_out(username))
authenticator.authenticate(username, invalid_password)
self.assertTrue(authenticator.is_user_locked_out(username))
- 위의 코드에서 어떤 동작이 테스트되고 있는지 파악하는 데 얼마나 걸렸는가?
- 아마도 코드의 각 줄을 읽고 이해하는 데 상당한 시간이 소요되었을 것이다.
- 그러나 테스트 이름을 다음과 같이 작성했다면 어땠을까?
def test_is_user_locked_out_after_three_invalid_login_attempts(self):
authenticator.authenticate(username, invalid_password)
self.assertFalse(authenticator.is_user_locked_out(username))
authenticator.authenticate(username, invalid_password)
self.assertFalse(authenticator.is_user_locked_out(username))
authenticator.authenticate(username, invalid_password)
self.assertTrue(authenticator.is_user_locked_out(username))
- 이제 테스트 이름만 보고도 어떤 동작이 테스트되고 있는지 바로 이해할 수 있다.
-
이처럼 테스트 이름에 시나리오와 기대 결과를 명시하면 여러 가지 이점이 있다.
- 클래스의 모든 동작을 알고 싶을 때, 테스트 코드 전체를 읽지 않고도 테스트 이름만 읽으면 된다. 이는 코드 리뷰 시에도 유용하다. 테스트 이름을 통해 예상되는 모든 경우가 커버되는지 빠르게 파악할 수 있다.
- 명확한 테스트 이름은 서로 다른 동작을 별도의 테스트로 분리하도록 강제한다. 그렇지 않으면 다양한 동작에 대한 assert를 하나의 테스트에 몰아넣게 되어, 시간이 지남에 따라 테스트가 커지고 이해하기 어려워질 수 있다.
- 테스트 코드만으로는 테스트 중인 동작이 항상 명확하지 않을 수 있다. 테스트 이름이 이를 명시하지 않으면, 테스트가 실제로 무엇을 테스트하는지 추측해야 할 수도 있다.
- 어떤 기능이 테스트되지 않고 있는지 쉽게 알 수 있다. 찾고자 하는 동작을 설명하는 테스트 이름이 보이지 않으면, 해당 테스트가 존재하지 않는다는 것을 알 수 있다.
- 테스트가 실패할 때, 테스트 소스 코드를 보지 않고도 어떤 기능이 문제가 있는지 즉시 파악할 수 있다.
-
테스트 이름을 구조화하는 몇 가지 일반적인 패턴이 있다. 예를 들어 "should"를 사용한 영어 문장처럼 이름을 지을 수 있다. (예:
should_lock_out_user_after_three_invalid_login_attempts
). - 어떤 패턴을 사용하든, 시나리오와 기대 결과를 모두 포함하도록 하는 것이 중요하다.
때로는 테스트하려는 메서드 이름만 지정해도 충분할 수 있다. 특히 메서드가 단순하고 그 동작이 이름에서 명확할 때 그렇다. 그러나 대부분의 경우, 명확하고 구체적인 테스트 이름을 작성하는 것이 유지보수와 이해도를 높이는 데 도움이 된다.