July 16, 2020
얼마 전 드디어 실무에서도 TDD, 아니 정확히는 BDD 로 업무를 진행해보았다.
TDD 와 BDD 를 구분할 정도로 대단한 안목은 커녕, 둘 다 겨우 겨우 배워나가는 수준이나 최근 겪은 것들을 통해 아는 것을 정리해보려고 한다.
현재 자신이 진행하는 것이 TDD 이든 BDD 이든, 아니면 코딩을 먼저하고 이후에 테스트를 추가하든 가장 중요한 것은
내가 달성해야 하는 목표를 정확하게 인지하여 자신이 사용하는 언어와 도메인 지식에 맞게 풀어서 정리하는 것
바로 이것이다.
켄트 백의 저서 TDD 에서는 담백하다 못해 매우 자연스럽게 넘어가서 인지를 못했던 부분이기도 하다. 그 책에서도 TDD 의 3대 cycle 인 red - green -refactor 를 수행하기 전에 선행하는 것이 할 일 목록 작성하기 였다.
거창해보이고 복잡해보이는 비즈니스 요구사항이 있다. 머리 속에서는 이미 이번 작업에서 굉장한 난관으로 예상되는 함수 몇가지를 떠올린다. UI / UX 구현 사항이 많아서 플래그 변수도 서너개는 필수적으로 필요할 것 같다. 코딩을 시작하기 전부터 벌써부터 가슴이 답답하다.
어쨌든 이번 작업은 TDD/BDD 으로 작업하려고 마음 먹었기 때문에 꼬박 반나절 이상을 투자해서 아이패드 프로로 그림도 그리고 화살표도 넣고 하면서 이것저것 고려하며 명세를 정리했다.
그렇게 만들어진 명세 목록을 참고하며 항목 하나 당 테스트(또는 시나리오)를 부여했다.
그리고 그 테스트(또는 시나리오)를 통과하기 위해 가장 쉽고 간단한 방법으로 코딩을 했다. 그때부터 이미 비즈니스 요구사항이라는 추상적이지만 생각하면 괴로운 목표는 고려하지 않았고, 테스트를 통과하는 것만을 생각했다.
이렇게 테스트(또는 시나리오)를 3개 정도 통과하고 나니, 놀랍게도 작성한 코드가 정말 적었다. 구상만 했을 때는 이미 플래그 변수가 2개 정도는 나왔어야 했고, 이번 작업의 큰 산이라고 생각했던 함수는 애초에 필요하지도 않은 함수였다. 분명 비즈니스 요구사항을 만족하기 위해서는 필요했던 함수라고 생각했지만, 개별 테스트(또는 시나리오)를 통과하는데에는 그런 함수가 필요가 없었고 단 하나의 플래그 변수면 충분했다.
이런 경험을 통해, 개발 명세를 상세히 파악하면 다음과 같은 효과가 있다는 것을 몸소 느꼈다.
앞에서 명세가 중요하다는 이야기를 했다.
그런데 실무에서 명세를 정리한 것과, 요 며칠 hackerrank 에서 문제를 풀며 명세를 정리한 것에 조금 차이가 있었다.
일단 실무에서의 명세는 대략 이런 식이다
사용자가 다음 페이지로 이동하기 버튼을 누르면 1페이지에서만 나와야 하는 사용자 정보는 나타나지 않는다.
반면 hackerrank 문제를 풀며 정리한 명세는 이러했다. (이 문제의 링크)
범위와 사과나무의 위치, 각 사과들의 상대위치를 제공하면 사과의 절대위치 개수를 구한다
아무래도 hackerrank 의 명세는 와닿지 않는다. 사실 이건 구현한 함수가 작동하는 방식을 그대로 서술한 것이다. 이 함수가 하는 일은 다음처럼 이야기할 수 있다.
시작점 s 와 끝점 t 가 주어지고, a 라는 점이 주어졌을 때 a 를 기준으로 방향이 주어진 임의의 좌표값을 입력하면 임의의 좌표값이 시작점 s 와 끝점 t 사이의 값인 개수를 구한다
실무에서 사용했던 명세는 사용자가 실제로 앱에서 행하는 시나리오이다. 이 시나리오를 행할 때 사용자는 내부에서 무슨 함수를 사용하든 관심이 없다.
한편 내가 hackerrank 에서 푼 문제에서 명세로 만든 것은 함수가 작동하는 방식이다. 즉 함수 구현(implement) 이다.
현재 내가 속한 프로젝트에서는 팀내에서 리팩토링을 매우 활발하게 하고 있어서 함수나 메소드들을 삭제하거나 시그니쳐를 바꾸는 일이 빈번하다. 하지만 내부에서 무슨 함수를 쓰든 원래 사용하던 메소드를 다른 클래스로 이전하든 시나리오만 만족하면 테스트는 깨질 일이 없다.
하지만 hackerrank 에서 궁극적으로 구하는 답은 사과의 절대위치 개수가 아니라 사과와 오렌지가 지정한 범위 내에 서로 몇개씩 존재하는가를 구하는 것이었고, 난 사과 개수를 따로 구하고 오렌지 개수를 따로 구하는 방식으로 접근했다. 그런데 사과와 오렌지를 한 번에 다루는 대신 다른 부분들을 일반화시키는 방식을 쓴다면 내 테스트들은 모두 깨진다. 명세가 달라지며 테스트 명세 또한 달라지기 때문이다.
아마 이것이 TDD 와 BDD 의 차이점이 아니었나 생각한다.
BDD 에서는 사용자 시나리오에 맞춰서 테스트 스펙을 작성하고, 내부 구현이 바뀐 것만으로 테스트가 깨지지 않고 테스트를 수정할 필요가 없다. 이것은 Kent C. Dodds 가 enzyme 대신 testing-library/react 을 사용하는 이유이다. 참고로 Kent 는 내부구현을 테스트한다면 실패해야 할 테스트가 통과하는 경우가 있다며 내가 예상했던 문제(테스트의 유지보수)와는 다른 문제(테스트 신뢰성)로 비판했다.
한편 TDD 에서는 함수의 오퍼레이션을 테스트 스펙으로 한다. 하지만 TDD 에서도 private 메소드는 테스트 대상이 아니라고 하기 때문에 TDD 가 함수의 내부구현에 의존한다고는 생각하지 않는다. 또한 함수의 추상화 단계가 높아져서 BDD 의 시나리오를 표현하는 함수가 있다면 BDD 와 크게 다를 것도 없다고 생각한다.