🐙

테스트와 CI

파이썬이 갖는 동적언어의 약점을 촘촘한 테스트와 CI로 보완할 수 있다고 생각했다. 덕분에 게임업계에선 이례적으로 높은 테스트커버리지를 달성했다. 결과적으로 이것만으로 충분하진 않았다. 동적언어의 약점을 충분히 극복할 순 없었다.

테스트 종류

건강한 테스트 문화에선 검증의 범위에 따라 단위테스트(unit testing), 통합테스트(integration testing), 기능테스트(functional testing 혹은 end-to-end testing)를 구별한다. 단위테스트는 가장 쉽게 만들 수 있는 간단한 함수 입출력 테스트다. 단위테스트 코드를 쉽게 짜기 위해선 테스트 용이한(testable) 코드를 만드는 게 중요하다. 방법은 전역 상태와 환경 의존성을 최소화하는 것이다. MMORPG의 게임세계는 거대한 전역 상태라서 테스트 용이한 코드를 만들기 위해 남다른 노력이 필요했다. 단위테스트로는 DB나 외부API 같은 외부세계와의 소통을 검증할 순 없다. 한편 통합테스트는 외부세계와 실제로 소통해서 잘 통합돼있는지 검증한다. Docker를 테스트 환경에 내장해서 통합테스트를 쉽게 만들 수 있었다. 기능테스트는 실제 고객의 의도가 시스템에 잘 적용되는지 검증하는 과정으로, 프로토콜(서버가 클라이언트와 계약한 인터페이스) 테스트나 수동 QA가 여기에 속한다.
단위테스트는 만들기 쉬운 만큼 속도가 가장 빠르고 양도 가장 많아야 한다. 통합테스트는 단위테스트에 비해 속도가 훨씬 느리므로(DB콜...) 검증 절차의 속도를 적절하게 유지하려면 그 수가 적어야 한다. 통합테스트보다도 더 오래 걸리는 기능테스트는 항목이 가장 적어야 한다. 이 셋을 구별해서 적절한 테스트 속도와 양을 추구해야 하지만, 난 듀랑고 개발 중기에서야 이들을 구별할 수 있게 되었다. 그 전까지는 모두 같은 단위테스트라고 생각했다. 그 결과 테스트 환경의 모습은 다음과 같이 되었다:
  • 모든 테스트가 하나의 단위테스트 프레임워크에서 실행된다.
  • 통합테스트와 기능테스트를 만드는 게 어렵지 않다.
  • QA 항목을 충분히 줄이지 못 했다.
  • CI 파이프라인 속도가 느려졌다.
테스트가 없는 것보다야 낫지만 좋은 상황은 아니다. 세 가지 테스트 종류를 구별해서 서로 다른 환경으로 분리하고, 의미 없는 테스트를 적극적으로 찾아서 지우는 노력이 필요해 보인다.

테스트 용이성

테스트코드를 만들기 쉬워야 테스트커버리지가 높아진다. UX가 좋은 API는 테스트코드도 쉽게 만들 수 있다. 코드의 UX에 관심을 두는 것이 테스트 용이성을 높이는 가장 좋은 방법이다. REPL 콘솔에서 가볍게 가지고 놀 수 있는 API를 지향하는 게 좋다.
게임서버를 켜고 플레이어들이 접속하면 DB에 고유한 게임세계가 만들어진다. 수많은 게임플레이 로직은 게임세계의 상태에 의존적이다. 그러다보니 특정한 의도를 담은 게임세계를 짧은 코드로 생성하는 것이 통합테스트나 기능테스트에서 중요했다. 게임세계를 픽스처로 제공하는 데에 꽤 많은 노력을 들인 이유다.