16. 테스트에 대한 고민 - (1)에서 이어집니다.

고민

간접 테스트에 만족할 것인가?

개발해 두었던 어플리케이션의 코드를 보면, DB에 쿼리하는 부분들을 모두 ORM 모델의 class method로 만들어 두었다. 예를 들어, ID 중복 체크는 아래처럼 메소드화되어 있다.

class TblUsers(Base):
    __tablename__ = 'tbl_users'

    id = Column(String(64), primary_key=True)
    password = Column(CHAR(93))  # len(werkzeug.security.generate_password_hash())
    nickname = Column(String(32))

    @classmethod
    def is_id_already_signed(cls, session, id):
        return cls.get_first_without_none_check(session, cls.id == id) is not None

16. 테스트에 대한 고민 - (1) 챕터에서, 딱 찝어서 말한 건 아니지만 문맥 상 '우리는 API를 테스트하는 코드를 작성하겠다'고 이야기한 적 있었다. 그럼 예를 들었을 때 ID 중복체크 API는 확실한 테스트 대상이 되겠지만, 위의 is_id_already_signed는 API 핸들러에 의해 간접적으로 테스트된다.

내 경우 '뭐 굳이 저것까지 따로 테스트할 필요 있나?'하고 넘겼던 적이 많은데, 확실히 짚고 넘어가고자 한다.

핵심

테스트가 들어 있는 코드를 유지보수하는 입장에서 유심히 바라봐야 할 지표는 테스트가 성공하는지와, 어떤 코드가 테스트되고/되지 않는지를 아는 것이라 생각한다. '이 코드가 확실히 테스트되겠다'고 안심할 수 있다면 간접 테스트로 만족할 수 있다고 판단하려고 한다(물론 '이 코드가 확실히 테스트되겠다'라는 생각이 맞는지를 검사하기 위해, 추후에 테스트 커버리지에 대해 이야기할 예정이다).

안심할 수 없거나, '뭐 대충 API 테스트하면서 같이 테스트 되겠지'하는 생각이 드는 곳은 따로 테스트를 작성할 것이다. 그 예는 다음과 같다.

  • /app/hooks/error.py의 에러 핸들러들 : 우리가 API를 테스트하는 코드에서 비정상적인 요청을 날리며 schematics의 ValidationError도 발생하고, 로직을 점검하는 테스트에 의해 abort가 실행되며 werkzeug의 HTTPException도 발생할 것이다. 그러나 만약 abort(403)이 실행되어서 status code 403을 반환받았다고 하더라도, 이게 정말로 http_exception_handler가 실행된 결과인지에 대해 확신할 수 없다. 이 에러 핸들러를 실수로 register해두지 않아서, 그냥 Flask에 있는 default error handler가 그대로 403번을 응답해 준 걸수도 있다. 따로 테스트를 작성해야 한다고 생각한다.

  • /app/decorators/validation.py : validate_with_schematics 데코레이터에 PayloadLocation.args를 전달하면 정말로 query params에 접근하는지, 함수에 데코레이터를 달았을 때 실제로 잘 실행되는지, BaseModel의 추상 메소드인 validate_additional이 정말로 실행되는지 등에 대해서는 API 테스트 코드에서 디테일하게 테스트되진 않을 것이라 판단한다. 따로 테스트를 작성하는 것이 좋다고 생각한다.

이들을 제외하고는 API 테스트 코드로 작업 내용을 채울 것이다. 사실 테스트 자체에 대한 경험과 배경지식이 많을 수록 테스트의 대상을 더 잘, 그리고 쉽게 정리할 수 있으며 테스트하기 좋은 코드를 작성하는 방법을 안다. 그래서 테스트를 잘 하는 사람들 입장에선 위에서 얘기했던 방식도 그리 좋은 방법은 아닐 것이다. 그래도 일단 차근차근 가자. 테스트에 대해서는 앞으로도 할 얘기가 많다.

테스트의 Idempotent를 어떻게 보장할 것인가?

Idempotent멱등성이라는 뜻인데, '연산을 여러 번 적용하더라도 결과가 달라지지 않는 성질'을 뜻한다. 함수형 프로그래밍을 좀 했다는 사람들은 pure function을 떠올리면 된다. 단적으로 어떤 함수를 실행했을 때, 항상 동일한 결과가 나오지 않는다면 그 함수는 비멱등성(non-idempotent)하다고 말한다.

이걸 왜 얘기하냐면, 테스트 코드들은 멱등성이 지켜져야 하는 것이 좋기 때문이다. 멱등성이 지켜진 테스트 코드를 작성해 두면, 테스트를 실행하는 입장에서 두려움이 없기 때문이다. '아 이 테스트 제대로 돌리려면 DB에 게시글 5개 넣어둬야 하는데'같은 생각이 드는 테스트는 잘못 짠 테스트다.

Idempotent를 보장할 방법

자, 우리는 아직 mocking에 대해 이야기하지 않았으니까 아래의 룰대로 테스트 코드를 작성해서 idempotent를 보장해 보자.

  • 조회가 기반이 되는 API(조회, 수정, 삭제)를 테스트하는 코드에선, 직접 데이터를 생성한다.

  • SQLAlchemy의 도움을 받아, setUp에서 모든 테이블을 생성하고, tearDown에서 모든 테이블을 drop한다.

추가적으로, 테스트 코드의 진행에 따라 실행될 DROP 쿼리가 RDS에 올라가 있는 production DB에 적용되면 안 되므로, 데이터베이스는 로컬에서 돌아가고 있는 MySQL을 사용하도록 할 것이다. '실수로 production DB 쓰게 설정 바꾸고 돌려버리면 어떡함?'이라는 의문은 17챕터에서 한 번 커버하고, 2~30챕터 사이에 언젠가 한 번 더 커버할 예정이다.

작업

테스트 코드를 작성하자. 13챕터 때처럼 작업들을 적절히 쪼개서 이슈로 만들어 두고, Project Board로 관리할 계획이다.

개발 완료 직후 코드 스냅샷

...