해당 챕터는 '아 그래서 테스트를 코드로 작성하는 것이 좋구나' 정도만 이해하고 넘어가도 좋습니다. Python과 Flask에 익숙하지 않은 개발자라면, 굳이 코드 전체를 이해하려고 용쓰지 않아도 됩니다.

API를 개발하고, Lambda라는 완전 관리형 컴퓨팅 엔진에 이를 배포해 둔 입장에서 가장 무서운 것은, API가 원하는 대로 동작하지 않는 이슈가 생기는 것이다. 분명 게시글 목록 API가 데이터들을 제대로 응답해줘야 하는데 비어있다거나, ID와 비밀번호를 제대로 입력했는데도 로그인이 제대로 수행되지 않는다거나 하는, 쉽게 말하면 API에 버그가 생기는 것이다.

독자 여러분이 테스트라는 개념을 모르고 있더라도, 무언가 코드 작업을 하고 나면 '잘 돌아가나?' 하고 한 번씩 확인을 거치곤 했을 것이다. 내가 옛날에 처음 백엔드 개발을 했었던 기숙사 시스템 프로젝트에서도 버그 해결이 뭐 이런 식이었다.

  1. 따로 로그 시스템은 없었으니까, '2017년의 급식 데이터가 싹 다 안 불러와진다'라는 버그 리포트를 받는다.

  2. PostMan으로 2017년의 아무 날짜나 잡아서 API를 호출해 보니, 서버로부터 500이 반환된다.

  3. 로컬에서 서버를 열고, PostMan에서 호스트를 localhost로 변경한 후 다시 한 번 요청을 보내고, Exception을 확인한다.

  4. 문제가 되는 코드를 고친다.

  5. 다시 PostMan으로 요청을 보내 보고, 잘 동작하니까 배포한다.

수동 테스트 문제점

위에서 얘기한 것도 사실 일종의 테스트다. 13챕터에서 API 작업을 할 때도 로컬에 MySQL 띄우고, 로컬에서 서버 열고, Insomnia라는 HTTP 클라이언트 툴을 통해 로컬에 요청을 날려보는 식이었다.

capture001.PNG

내가 생각하기에 체크해야 될 것 같은 부분들을 눈으로 직접 확인했다. 예를 들어, 회원가입 API의 테스트는 다음과 같았다.

  1. 정상적인 범주(happy path)에 대해 요청을 보내고, 회원가입이 잘 처리되었는지(201 Created가 잘 응답되는지, 데이터베이스에 잘 들어갔는지, 비밀번호가 잘 해시되었는지) 확인한다.

  2. 이미 가입된 ID를 사용해 요청을 보내고, 409 Conflict가 잘 응답되는지 확인한다.

  3. 비밀번호의 제약조건인 '최소 길이는 8자리'를 어기고, 400 Bad Request가 잘 응답되는지 확인한다.

  4. 닉네임의 제약조건은 '최대 길이는 32자리'를 어기고, 400 Bad Request가 잘 응답되는지 확인한다.

이 정도 양의 테스트를 우리가 작성한 API들에 모두 적용해야 한다. HTTP 요청 전송 버튼만 50번 넘게 눌러야 한다는 것이다. 여기서 우리가 주목해야 할 것은 다음과 같다.

  • API 테스트는 배포하기 전에 꼭 해야 한다.

  • 손으로 직접 하면 시간 소비가 너무 크고, 사람이 관여하게 되니 실수할 수도 있다.

  • 테스트의 내용은 항상 정해져 있어서, 순서도로 표현할 수 있으며, 이는 코드로 옮길 수 있는 여지가 있다는 것이다.

  • Python과 Flask가 지원해 주는 여러 테스트 헬퍼들이 이런 일을 쉽게 할 수 있도록 도와준다.

그럼 우리가 테스트를 코드로 작성해 두면, 손으로 직접 할 필요 없이 테스트를 수행하는 커맨드만 실행해 두고 가만히 있으면 된다는 것이다. 쉽게 생각하자. 이미 가입된 ID를 사용해 회원가입 API를 호출하는 테스트를 작성한다고 치면, 회원가입 API가 동작하고 있는 엔드포인트회원가입과 관련된 데이터들을 JSON 형태로 만들어 HTTP 요청을 보낸 후, 응답의 status code가 409번인지 확인하면 된다.

다 알겠는데 코드로 어떻게 옮겨야 할 지 모르겠으니까, 구글에 'flask test'같은 키워드로 검색 돌려서 나온 Testing Flask Application 문서를 조금 참고해서 코드로 옮기면 이런 식으로 작성해볼 수 있겠다.

from unittest import TestCase

from app import create_app
from app.extensions import main_db
from app.models.user import TblUsers
from config.app_config import LocalLevelConfig
from config.db_config import LocalDBConfig


class TestSignupAPI(TestCase):
    def setUp(self):
        self.app = create_app(LocalLevelConfig, LocalDBConfig)
        self.test_client = self.app.test_client()

        self.test_user = TblUsers(
            id=self.signed_id,
            password='',
            nickname=''
        )
        session = main_db.session
        session.add(self.test_user)
        session.commit()

    def test_already_signed_id_409(self):
        response = self.test_client.post('/user/account/signup', json={
            'id': self.test_user.id,
            'password': 'testpassword',
            'nickname': 'planb'
        })

        self.assertEqual(409, response.status_code)

'대충 이런 식으로 짤 수 있겠다'하고 생각하면서 작성한 것이라 제대로 동작하진 않을 것이다. 아무튼 대충 이런 식이 된다. 직접 DB에서 이미 가입된 사용자 ID를 가져와서 → 요청 정보를 수정하고 → 직접 버튼을 누르고 → 직접 응답을 확인하는 번거로운 작업을 수동으로 할 필요가 없어진다. 만약 위 테스트에서 response.status_code가 409가 아니라면 AssertionError가 발생하면서 테스트가 실패할 것이고, 테스트를 실행하며 기다리던 개발자는 테스트가 실패한 것을 보고 API를 고치던 테스트 코드를 고치던 할 것이다.

테스트 코드를 작성한다는 게 정말로 의미가 있는가?

테스트 코드를 작성하기 위해 시간을 써야 하니까, 그럴 시간에 그냥 직접 PostMan같은 걸로 직접 테스트하는 게 더 낫지 않나? 싶을 수 있다. 이건 실제로 프로젝트를 한다고 생각해보면 되는데, 뭔가 서비스 하나를 개발한다고 치면 13챕터에서 개발한 웹 어플리케이션보다 몇 배는 되는 양의 API가 작성될 가능성이 크다. 테스트해야 되는 케이스는 쉽게 100개를 넘어 버린다. 여기서 핵심은, 회원가입 API 손봤다고 게시글 API 테스트 안해도 되는 게 아니라는 것이다. 아래와 같은 상황이 연출될 수 있기 때문이다.

  • 회원가입 API가 고장나서 찾아보니 a라는 함수의 문제였고, 이걸 고치고 나서 잘 되길래 배포했다.
  • 그러고 나니까 게시글 API가 오류가 나서 보니까 얘도 함수 a를 사용하고 있었다.

어차피 내 코드가 의도대로 동작하는지 확인하려면 HTTP로 찔러보든 해서 실행을 시켜봐야 하는데, 이걸 수동으로 할 바에 시간 들여서 테스트 코드 작성해 두고 확인하고 싶을 때마다 실행시키기만 하는 것이 속도 면에서도, 정신건강 면에서도 훨씬 낫다. 개발자가 리팩토링 등의 이유로 코드를 변경했을 때, 이 변경에 문제가 없음을 검증하기 위해 해야 하는 작업'테스트 코드 실행하기' 하나 뿐이라면, 코드를 수정하는 모든 작업들에 안정감이 생긴다.

테스트에는 종류가 다양한데, 여기선 다른 시스템과의 연결 없이 코드만을 테스트하는 유닛 테스트를 작성하도록 하자. 다른 것들을 알아보고 싶다면 열심히 구글링해도 되고, 챕터가 나올 때까지 기다려도 된다. 아무튼 이제 당장의 목표는 '테스트 코드를 작성하는 것'으로 둔 상태에서, 이번 챕터에서는 이 목표를 이루기 위한 의사결정을 진행하자.

의사결정

테스트 프레임워크

배경과 요구사항

  • Python에는 많은 테스트 프레임워크들이 존재한다. 여러 개를 한 번에 적용하는 건 불가능하진 않지만 여기선 불필요하니까, 하나 정해서 그걸로 테스트 코드를 작성하자.

  • 조직이 unittest라는 테스트 프레임워크에 익숙하다.

  • Flask가 제공하는 테스트 클라이언트를 잘 써먹을 수 있는 프레임워크면 좋다.

선택지

테스트 프레임워크가 이것저것 많긴 한데, 아래 세 개가 가장 많이 쓰인다.

  • unittest

  • doctest

  • pytest

의사결정

unittest를 사용하겠다. 그 이유는,

  • 조직이 unittest에 익숙하다.

  • 난 객체지향 알못이라 이런 얘기 해도 될런지 모르겠지만, 클래스를 기반으로 한 구조 덕분에 얻을 수 있는 이점이 많다. 상속하고 뭐하고 뚝딱뚝딱 하다 보면 잘 빠진 코드가 나오곤 했던 경험이 있다.

  • doctest는 API 테스트보단 라이브러리 테스트에 더 어울린다. 글을 마무리하기 전에 링크할 파이썬 테스트 관련 문서를 보면 느낌이 올 것이다.

  • pytest는 pytest만이 가진 독특한 스타일 때문인지, 'pytest 써봐야겠다' 하며 입문은 쉽더라도 잘 쓰기는 어려웠던 기억이 있다.

  • 테스트 입문을 다 unittest로 해서 그런지, 파이썬 개발자들 사이에서 unittest는 '기본 소양'같은 느낌이라, 새로운 개발자가 '아 pytest네요?' 하면서 러닝커브를 감당하지 않아도 되지 않을까 하는 작은 바람도 있다.

테스트 자체에 대한 이야기는 outsider님의 글 유닛테스트에 대한 생각, 파이썬 테스트에 관련해선 코드 테스트하기 - The Hitchhiker's Guide to Python 문서가 아주 잘 작성되어 있습니다. Hitchhiker에 있는 문서들은 책으로 발간될 정도로 양질의 내용이 많으니, 짬나는 시간에 읽어보면 정말 좋습니다.