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
는 멱등성이라는 뜻인데, '연산을 여러 번 적용하더라도 결과가 달라지지 않는 성질'을 뜻한다. 함수형 프로그래밍을 좀 했다는 사람들은 pure function을 떠올리면 된다. 단적으로 어떤 함수를 실행했을 때, 항상 동일한 결과가 나오지 않는다면 그 함수는 비멱등성(non-idempotent)하다고 말한다.
이걸 왜 얘기하냐면, 테스트 코드들은 멱등성이 지켜져야 하는 것이 좋기 때문이다. 멱등성이 지켜진 테스트 코드를 작성해 두면, 테스트를 실행하는 입장에서 두려움이 없기 때문이다. '아 이 테스트 제대로 돌리려면 DB에 게시글 5개 넣어둬야 하는데'같은 생각이 드는 테스트는 잘못 짠 테스트다.
자, 우리는 아직 mocking에 대해 이야기하지 않았으니까 아래의 룰대로 테스트 코드를 작성해서 idempotent를 보장해 보자.
조회가 기반이 되는 API(조회, 수정, 삭제)를 테스트하는 코드에선, 직접 데이터를 생성한다.
SQLAlchemy의 도움을 받아, setUp
에서 모든 테이블을 생성하고, tearDown
에서 모든 테이블을 drop한다.
추가적으로, 테스트 코드의 진행에 따라 실행될 DROP
쿼리가 RDS에 올라가 있는 production DB에 적용되면 안 되므로, 데이터베이스는 로컬에서 돌아가고 있는 MySQL을 사용하도록 할 것이다. '실수로 production DB 쓰게 설정 바꾸고 돌려버리면 어떡함?'이라는 의문은 17챕터에서 한 번 커버하고, 2~30챕터 사이에 언젠가 한 번 더 커버할 예정이다.
테스트 코드를 작성하자. 13챕터 때처럼 작업들을 적절히 쪼개서 이슈로 만들어 두고, Project Board로 관리할 계획이다.
...
원래 type hinting을 주제로 글을 쓰려다가, 타입 추론에 대해 글을 써내려가다 보니 이거 하나만으로도 글 하나가 대충 완성될 것 같았다. 그래서 이번에는 그냥 타입 추론 이야기를 해보려고 한다. 타입 추론 Python은 타입 검사가 동적이기 때문에, 모든 타입이 컴파일 타임이 아니라 런타임에 결정된다.
테스트 코드와 관련된 챕터에 글을 쓰고 나서, 거기서 얘기한 테스트 코드를 모두 작성한 뒤에 챕터를 진행하려고 했더니, 평일에 하루종일 코딩하다 집 들어와서 다시 코딩해야 하는 상황이라 너무 진행이 되질 않습니다. 따라서, 이제부턴 테스트 코드 작성과 챕터 진행을 동시에 진행하려고 합니다. AWS에서 인스턴스 기반으로 움직이는 요소(EC2, RDS 등)...
16. 테스트에 대한 고민 - (1)에서 이어집니다. 고민 간접 테스트에 만족할 것인가? 개발해 두었던 어플리케이션의 코드를 보면, DB에 쿼리하는 부분들을 모두 ORM 모델의 class method로 만들어 두었다. 예를 들어, ID 중복 체크는 아래처럼 메소드화되어 있다. [16. 테스트에 대한 고민 - (1)](https://ve...
오늘 이야기할 내용은 사실 내가 떠드는 거 읽으면서 간접경험하는 것보다, 어떤 언어든 프레임워크든 상관 없으니 실제로 코드를 짜 보면서 직접경험을 하는 편이 훨씬 낫다. 나는 책이고 강의고 뭐고 그냥 코딩 엄청 해보는 게 최고의 경험이라고 생각한다. 그럼에도 불구하고
해당 챕터는 '아 그래서 테스트를 코드로 작성하는 것이 좋구나' 정도만 이해하고 넘어가도 좋습니다. Python과 Flask에 익숙하지 않은 개발자라면, 굳이 코드 전체를 이해하려고 용쓰지 않아도 됩니다. API를 개발하고, Lambda라는 완전 관리형 컴퓨팅 엔진에 이를 배포해 둔 입장에서 가장 무서운 것은, API가 원하는 대로 동작하지 않는 이슈가...