오늘 이야기할 내용은 사실 내가 떠드는 거 읽으면서 간접경험하는 것보다, 어떤 언어든 프레임워크든 상관 없으니 실제로 코드를 짜 보면서 직접경험을 하는 편이 훨씬 낫다. 나는 책이고 강의고 뭐고 그냥 코딩 엄청 해보는 게 최고의 경험이라고 생각한다. 그럼에도 불구하고 글을 쓰는 이유는, 테스트 코드를 작성하기 이전에 13. 웹 어플리케이션 개발 챕터에서 했던 '일러두기'같은 것이 필요하다고 느꼈기 때문이다.

어쨌든 독자 여러분들은 남이 작업하고 난 코드를 보는 입장일텐데, '아니 대체 왜 이렇게 했음?'이라는 의문을 미리 해결해줄만한 내용을 이야기해 보자. 실제로 내가 과거에 테스트 코드라는 걸 처음 마주쳤을 때 했던 고민들이 대부분이다. 그러나 난 테스트에 대한 조예가 깊지 않으므로 '에이 이건 아닌데?' 싶으면 그냥 아닌 거라고 생각하고 넘어가 주길 바란다.

고민

unittest의 구조를 어떻게 써먹을 것인가?

우리가 15챕터에서 쓰기로 결정했던 테스트 프레임워크인 unittest를 조금 살펴보고 나면, 아래의 내용을 이해하고 있을 것이다.

  • unittest.TestCase를 상속받아 테스트 클래스를 먼저 정의한다.

  • test로 시작하는 이름의 메소드가 실제 테스트 실행 대상으로 잡힌다.

  • 몰라도 되는 내용이긴 하지만, 알파벳 순서대로 실행된다.

  • setUptearDown 메소드가 각 테스트 메소드의 수행 전과 후마다 실행된다.

이러한 구조를 어떻게 하면 잘 써먹을 수 있을지 생각해 보자.

test_post_api, test_auth_api, ...

내가 가장 처음 테스트 코드를 작성했던 방식은, 테스트 클래스 하나에 API 그룹(게시글 API, 회원가입과 로그인 API)마다 테스트 메소드를 하나씩 만들고 이것저것 다 집어넣는 것이었다. '회원가입 API 테스트하고 나면 로그인도 테스트해야겠다' 하면서 테스트 메소드 하나에 같이 테스트할만한 거 다 묶었었다. 그렇게 되니까, 메소드 하나가 아래의 것들을 모두 테스트했다.

  • 게시글 작성 API에 대해 실패하는 케이스들을 모두 테스트한다.

  • 게시글 작성 API의 성공을 테스트한다.

  • 게시글 목록 API를 호출해서, 작성된 게시글의 ID가 존재하는지 테스트한다.

  • 작성된 게시글의 ID를 가지고, 게시글 내용 조회 API를 테스트한다.

  • 그 게시글 ID를 그대로 가져다가 게시글 수정 API를 테스트한다.

  • 그거 갖다가 또 게시글 삭제 API를 테스트한다.

  • 다시 게시글 목록 API를 호출해서, 삭제한 게시글 ID가 나타나지 않는지 테스트한다.

테스트를 처음 작성하는 입장에서는 '흐름대로 테스트한다'는 느낌이라 아마 이런 방식이 더 편할 것이다. 사실 테스트 코드를 작성하면서 가장 절제해야 하는 것이 '흐름'인데, 그 이유는 다음과 같다.

  • 테스트 코드에선 assert를 사용한다. assert [expression] 식인데, 'expression이 True로 평가되어야 한다'라는 뜻이다. assert response.status_code == 200같은 식으로 사용할 수 있을 것이다. 모종의 이유로 이 expression이 False로 평가되면, AssertionError가 raise되어서 해당 테스트 메소드가 거기서 종료돼버린다. 위의 예로 따졌을 때, 게시글 작성 API가 status code 201을 내려줘야 하는데 다른 걸 내려줘서 assert가 실패하면, 그 아래에 있는 수정 API 테스트와 삭제 API 테스트는 아예 실행되지도 않는다는 것이다. 모든 테스트는 다른 테스트 코드에 의존하지 않아야 한다.

  • 테스트는 외부 상태에 최소한으로 의존해야 한다. 단적으로 얘기했을 때 DB가 내려갔거다거나 하는 정도가 아니라면, 지금 DB 상태가 어떻든 간에 '테스트 코드 자체'에서의 문제가 없어야 한다는 것이다. 예를 들면, 'test_id'라는 ID로 회원가입 API를 테스트했다가, 다시 실행했을 때 --이미 그 ID에 해당하는 유저가 DB에 들어 있어서 테스트에 실패--하는 경우다. 게시글 조회 API를 테스트하고자 한다면, 앞쪽에서 게시글 작성 API 테스트한거 갖다가 쓸 게 아니라 얘 전용으로 따로 insert해주는게 맞다.

  • 메소드 하나에 싹 넣어버리면, 테스트가 실행된 결과를 보는 입장에서 정리가 제대로 안 된다. 게시글 수정 API랑 삭제 API는 멀쩡한데 작성 API가 고장나서 test_post_api 메소드가 실패했다는 결과를 보는 것보다, test_post_write는 실패하고 test_post_modifytest_post_delete는 성공했다는 결과를 보는 것이 훨씬 낫다.

따라서, 해당 방식은 좋지 않다는 생각이다.

URI마다 한 클래스, HTTP 메소드마다 테스트 메소드 하나씩

위의 방식이 되게 불편했어서, 그 다음으로 시도했던 것이다. URI마다 한 테스트 클래스를, HTTP 메소드마다 테스트 메소드를 하나씩 두는 것인데, 아래는 그 예다.

  • /board/posts에서 동작하는 게시글 API를 테스트하기 위해 unittest.TestCase를 상속받은 TestPostAPI 클래스를 만든다.

  • 게시글 작성 API를 테스트하기 위해 test_post_write, 게시글 수정 API를 테스트하기 위해 test_post_modify, 게시글 삭제 API를 테스트하기 위해 test_post_delete같은 식으로 메소드 만들고, 여기서 실패 케이스랑 성공 케이스 이것저것 다 테스트한다.

각각의 테스트 메소드가 비교적 덜 뚱뚱해질 것이지만, 이것도 결국 메소드 하나가 여러 테스트를 한 번에 수행하는 구조라서 위에서 얘기했던 단점들을 시원하게 해결해주진 못한다.

엔드포인트마다 한 클래스, 단일 목적을 가진 테스트 메소드

요새 쓰고 있는 방식이다. 그렇다고 정답은 아니겠지만, 이 방식을 설명하자면,

  • 엔드포인트마다 한 테스트 클래스를 만든다. API 클래스의 이름에서, 'API' 앞에 테스트하고자 하는 메소드를 붙이고, 맨 뒤에 'Test'를 붙인다. 예를 들어, PostAPI를 테스트하기 위해 PostPostAPITest, PostGetAPITest, PostItemAPI를 테스트하기 위해 PostItemGetAPITest, PostItemPatchAPITest, PostItemDeleteAPITest처럼 클래스를 만들 수 있다.

  • 해당 엔드포인트에 대해 테스트하고자 하는 것마다 메소드를 만든다. 게시글 내용 조회 API를 테스트하는 PostItemGetAPITest 하위에, 유효한 게시글 ID를 통해 내용을 조회하는 test_get_with_valid_id, 유효하지 않은 게시글 ID를 통해 내용을 조회하고, status code 404가 응답되는지 검사하는 test_get_with_invalid_id_404 메소드 등을 만들 수 있을 것이다.

이런 식으로 테스트 코드를 작성하다 보니, 딱히 큰 불만 없이 테스트 코드를 작성할 수 있었던 것 같다. 여기서도 이 스타일을 사용할 계획이다.

테스트를 얼마나 구체적으로 할 것인가?

테스트를 어느 정도까지 작성해야 하느냐의 이야기인데, 정량적인 지표는 없고 '테스트를 다 통과하면 배포해도 된다'고 말할 수 있을 때까지 작성하면 된다고 생각한다. 조직이 코드에 대한 걱정이 얼마나 많느냐에 따라 테스트 코드의 양이 결정된다. 난 이정도를 테스트할 것이다.

  • ID가 중복되지 않으면 회원가입 성공, 중복되면 409를 응답하는 것처럼, API가 제공할 수 있는 모든 결과 케이스
  • 리소스가 생성/수정/삭제되는 테스트라면, DB를 직접 조회해 정말로 잘 생성/수정/삭제되었는지 확인

다음 챕터에서 몇가지 고민을 더 이야기해 보고, 테스트 코드를 작성하도록 하자.