October 07, 2021
약 반년 전에 이런 글 을 작성한 적이 있다.
이번에도 다시 테스트 코드와 스펙에 대한 이야기를 하려고 한다. 저번 이야기와 비슷한 이야기지만, 더 중요한 이야기가 될 것 같다.
iOS 에는 단축어라는 기능이 있다.
특정 상황에서 iOS 가 사용자 대신 미리하기로 했던 시나리오를 수행하는 기능이다.
예를 들어 음악앱을 사용할 때, 음악앱 자체는 사용자가 클릭하고 음악을 고르기 전까지는 절대로 스스로 작동하지 않는다. 하지만 단축어를 사용하면, 오전 8시에 음악앱이 스스로 재즈 음악을 재생하도록 할 수 있다.
테스트 코드는 위의 단축어 개념과 유사하다.
테스트 코드의 대상은 사용자 interaction 이 있기 전까지는 작동하지 않는다. 이 때 테스트 러너의 도움을 통해 환경을 구축하고 사용자 interaction 을 상정하는 시나리오를 테스트 코드로 작성하면 테스트 코드의 대상은 무언가 변화를 일으키고 그 결과를 보여준다. 단축어와 차이가 있다면 자동화 테스트는 그 결과가 우리의 기대값과 일치하는지 확인(assertion)하는 과정을 포함한다. 때로는 실제 시각과 상관없이 오전 8시라고 속일 수도(test double) 있다.
다시 단축어로 돌아가보자.
이번에는 단축어 대신 실제로 오전 8시에 스스로 재즈 음악을 재생하는 어플리케이션을 제작한다고 하자.
어플리케이션은 오전 8시 외에는 음악을 재생하면 안 된다.
그런데 만약 오전 7시에 음악을 재생한다면, 그 앱은 스펙(명세)을 어긴 것이다.
예정보다 1시간 일찍 일어나서 분노한 사용자는 가차없이 어플리케이션을 지울 것이다.
이해를 쉽게 하기 위해 단축어와 오전 8시에 자동으로 재생하는 음악 앱의 이야기를 했지만, 예시란 사실 오해의 여지를 남기기도 한다.
그래서 오전 8시의 자동재생은 이제 머리속에서 지우자. 하루에 단 한 번만 통과하는 테스트를 작성할 것이 아니라면 이 테스트 시나리오는 필연적으로 test double 을 사용한다. 또 test double 도입이 곧 변경점이기 때문에 오해의 여지가 있다.
대신 일반적으로 개발할 때 자주 접하는 상황을 이야기하자.
다음과 같은 시나리오를 상정하자.
어떤 버튼을 클릭했을 때 사용자가 로그인 상태라면 회원정보 메뉴를 보여준다. 사용자가 로그아웃 상태라면 로그인하기 메뉴를 보여준다.
이 버튼은 아주 간결한 시나리오를 수행한다. 사용자의 상태는 로그인을 했는지 안 했는지만을 판단한다.
이 시나리오에 테스트 코드를 먼저 작성한다. 그러면 실제 코드를 작성하지 않아 당연히 테스트가 실패한다. 테스트 성공을 위해 실제 코드를 작성한다. 이 실제 코드는 테스트 코드를 만족시키는 코드가 된다.
만약 여기서 사용자가 로그인 상태이지만 이메일 인증 등의 과정을 거치지 않았다면?
만약 사용자가 로그인 상태이지만 발급받은 토큰이 만료되었다면?
위와 같은 의문은 상용화 서비스에서 응당 고려해야 하는 부분이다. 그래서 버튼을 클릭했을 때 이메일 인증과 토큰 만료 여부를 확인하는 과정을 실제 코드에 추가했다고 가정하자.
이 경우, 실제 코드는 테스트 시나리오가 다루지 않는 군더더기 코드를 담는다. 테스트 코드 입장에선 다음 두 가지가 모두 군더더기이다.
실제 코드가 토큰 만료를 확인한다.
실제 코드가 길이 1000짜리 배열의 각 요소들을 제곱한 뒤 그 결과를 모두 합하고, 그 값에 0을 곱한 후, 마지막으로 그 값이 1보다 큰지 비교하는 조건문을 만들어서, 그 조건문 내부에서 logger 를 호출한다.
왜 군더더기인가? 테스트 코드가 요구하는 시나리오에는 위 두가지가 하나같이 필요없기 때문이다.
그래서 뒤늦게 부랴부랴 토큰 만료를 확인하는 테스트를 추가하면, 테스트 코드는 그 시나리오를 다루는 것이 될 수도 있다.
잠정적으로 이야기하는 것은 다음 두 가지 이유에서이다.
테스트 코드를 언제나 성공하는 테스트로 만들었을 경우 그 시나리오를 고려하지 않는 테스트와 동일하다.
방금 작성한 테스트가 충족하는 시나리오 외의 또다른 시나리오를 실제 코드가 내포할 수 있다.
전자는 몇번의 시행착오 및 세심한 주의력으로 어찌저찌 해결할 수 있다. 실제 코드를 의도적으로 오염시킨다거나 하는 방법으로 확인할 수도 있다. 물론 비용이 드는 방법이지만, 어쨌든 해결할 수 있을 것이다.
문제는 후자이다. 이미 작성한 코드가 토큰 만료만 다룬다고 생각했는데 알고보니 앱 초기화까지 포함한 경우라면?
이 경우, 테스트 코드 입장에서는 실제 코드는 버그를 가지고 있는 것이다. 앱 초기화가 기획상에서 요구한 기능이라 하더라도. 그 유명한 ‘버그가 아니라 기능입니다’ 의 탄생이다.
테스트 코드는 스펙일 수도 있다. 테스트 코드가 실제 코드가 지향하는 시나리오의 일부를 담고 있기 때문이다.
하지만 TDD 의 경우, 테스트 코드는 스펙 그 자체이다. 실제 코드는 기획서나 사용자 설문조사의 내용을 반영하지 않는다. 실제 코드는 그저 테스트 코드의 시나리오를 만족시키고 테스트 코드가 통과하는 것을 기대한다.