보통 테스트 주도 개발이나 단위 테스트에서 가장 어려운 것은 뭘까?
테스트 코드를 작성하는 것이다! 문법이나 도구의 문제가 아니라—이런 것들은 15분이면 배워서 시작할 수 있다. 문제는 원하는 바를 머릿속에 막연하게 떠올리고, 그것을 어떤 함수의 작동을 검증하는 무언가로 변환하는 것이다… 게다가 그놈의 코드를 작성하기도 전에!
사람들은 당신에게 TDD를 활용하라고 한다. 하지만 존재하지도 않는 뭔가에 대한 테스트 코드 작성이 대체 어떻게 가능할까? 아직 그 함수가 뭘 할지도 모르고—어쩌면 하나 대신 두개의 함수를 쓰고 싶을수도 있는데—그대신 테스트를 생각하라니? 미친소리처럼 들리는가?
당신에게 TDD를 활용하라고 말하는 그 모든 사람들은 어떻게 하길래?
바로 그것이다—테스트 주도 개발은 당신의 코드에 대한 다른 방식의 사고를요구한다. 그리고 누구도 당신에게 방법을 알려주지 않는다. 지금까진.
막연한 생각을 테스트로 전환하는 방법
여기에 활용할 만한 한가지 방법을 소개하고자 한다—요구사항과 아이디어가 무엇이든, 이를 명확한 무언가로 전환하는것, 그것이 바로 테스트가 가능한 것이다.
나는 테스트 주도 개발을 많이 활용한다. 이 글은 내 코드를 위한 테스트 코드를 작성할 때와 똑같은 사고과정에 기반한다. 이것은 “red-green-refactor” 같은, 자세한 요령에 대한 것이 아니다. 대신, 숙련된 개발자가 TDD 방식으로 코드를 작성할 때의 사고과정과 이를 쉽게 하는 방법에 집중한다.
기억해둘 점: 필요한 것은 사고방식에 적응하는 것이다. 그렇다 — 맨 처음엔 의식적인 노력이 다소 필요하지만, 충분히 노력하면 일상적인 것이 된다. 반복문과 조건문을 쓰듯, 오래 생각할 필요가 없이 그냥 테스트 코드를 작성하게 된다.
첫번째 예제로 시작해보자 : 비밀번호 강도 측정하기
내가 이 글을 쓰기 시작했을 때, 막 떠올린 생각이다. 아직 한번도 코드를 작성하지 않았다 — 당신이 실제로 코드를 작성할 때처럼 나 역시 더듬더듬 시작할 것이다.
시작하기 전에, 당신이 알아야 할 한가지 중요한 점이 있다. 목표는 완벽이 아니다! 테스트 주도 개발은 반복적인 과정으로, 작은 절차들의 반복적인 수행을 의미한다. 그렇다, 우리는 경험에 근거한 좋은 추측을 하고싶을 뿐, 정확히 옳아야하는 것은 아니다. 어떤 작은 디테일을 고민하느라 멈추지 마라, 왜냐하면 소프트웨어 분야에선 뭐든지 항상 바뀌기 때문이다. TDD의 가장 좋은 점은 변경이 쉬운 것이고 따라서 첫 시도가 100% 정확하지 않다면, 그냥 다시 해보면 된다. 그것이 이상적인 모습이다!
1단계: 입력과 출력 결정하기
우리는 이 과정을 하이레벨로부터 시작할 것이다. 당장에는 구현을 고려하지 않는다.
우리의 목표: 비밀번호 강도의 측정. 이를 위해서 보통은 입력값들이 필요하고… 다음으로, 그에 근거한 출력값을 얻을 것이다.
일반적으로 어떤 목표를 위해 코드를 작성하기 시작한다면, 아마 당신은 함수로 운을 뗄 것이다. 대개는 함수의 동작에 어떤 데이터가 필요한지, 그리고 어떤 종류의 결과값을 반환할 지 등을 생각한다. 우리는 똑같이 절차를 시작할 것이다 — 다만 아직은 어떤 코드도 작성하지 않을 것이다.
그래서 어떻게 하면 될까?
- 입력값은 쉽다: 비밀번호.
- 결과값도 쉽다: 비밀번호의 강도를 나타내는 어떤 값이 되어야 한다. 간단하게 하려면, 비밀번호가 강력하거나 아니거나 — 즉
boolean
값을 출력에 사용할 수 있다.
2단계: 함수 시그니처 선택하기
이제 어떤 데이터가 들어오고 나가는지 알았으니, 우리는 함수 시그니처를 골라야 한다 — 즉 함수가 어떤 매개변수들을 취하며, 어떤 결과를 반환하는 지를 말한다.
이번 과정도 TDD와 무관하게 당신이 코드 작성에 접근하는 방법과 비슷하다. 어떤 함수 코드든 작성하기 전에, 당신은 매개변수와 반환 값이 무엇이 될 지 정해야 한다.
먼저, 매개변수. 우리의 함수는 어떤 동작을 해야할까? 이 경우에는 간단한데 — 필요한 것은 비밀번호 뿐이다. 우리는 단 하나의 값에만 의존해 모든 측정을 할 수 있다.
반환값은 어떨까? 측정이기 때문에 간단한데, 결과를 곧장 반환할 수 있다. 보다 복잡한 상황이라면, 반환값은 Promise
가 된다. 또는 값을 반환하는 대신, 콜백 매개변수를 취하거나 — 아무것도 반환하지 않을 수도 있다.
어쨌든간에, 이 시점에서 함수 호출을 어떻게 할지 결정할 수 있다:
var strong = isStrongPassword(‘password string goes here’);
3단계: 기능상 아주 작은 하나의 관점으로 판단하기
이제 우리는 목표, 관련 데이터와 함수 시그니처를 알고있다.
TDD가 아닌 방식이라면, 당신은 코드 작성에 돌입할 것이다. 이미 당신에겐 작동 방식에 대한 몇가지 아이디어가 있을 것이다 — 이걸 확인해야 하고, 저걸 확인해야하고, 반환값은 X에 의해 영향받고…
TDD에 있어서 대부분의 사람들이 곤란에 빠지는 지점이 여기다. 당신의 머릿속은 함수를 어떻게 작성할 것인가에 대한 생각들로 가득찼지만… 작성을 시작할 때까지 당신은 코드를 정확히 어떻게 설계할 지 확신할 수 없다.
모든 선택지에 대해 생각하는 대신에… 그냥 작은 것에만 집중해보자.
목표에 아주 조금 다가가기 위해 필요한, 최대한 간단한 동작은 뭘까?
흔한 문제점은 너무 큰 덩어리에 매달리거나 해결하려고 하는 것이다. 비밀번호 강도를 생각해보면, 특수문자, 숫자, 길이, 등등… 다양한 규칙들이 있을 것이다. 이 모든 것을 포함하는 테스트를 생각하는 건 당연히 어렵다!
그렇다면 비밀번호 강도 측정이라는 궁극적인 목표에 도달하는 함수를 만들기 위해 시도할 수 있는 가장 단순한 절차는 무엇일까?
만약 TDD와 무관하게 함수를 작성한다면, 맨 첫 줄(아니면 두번째)은 무엇이 될까?
몇 줄을 추가하면 제대로 동작하는 함수에 가까워질 수 있을까?
비밀번호 강도에 관해 가장 단순한 규칙은 빈 문자열이다. 이건 매우 쉽다 — 비밀번호가 비어있다면 출력값은 언제나 false
여야 한다.
4단계: 테스트 구현
우여곡절 끝에, 테스트 코드 구현까지 왔다. 생각보다 쉽다 느끼길 바란다 :)
앞선 과정들이 TDD와 무관하게 코딩하는 것과 실제로 비슷한 걸 알겠는가?
주된 차이점은 함수 구현 대신, 어떻게 호출되어야 하며, 어떤 결과가 발생하는 지에 집중하고 있다는 점이다. 즉 — 어떤 조건 하에서 함수가 어떻게 동작하는 지 생각하고 있다.
함수가 어떻게 동작하는지 테스트하고 싶은 것이다. 어떤 조건 하에서 일단 시작하고나면(특정 매개변수, 시간대, 무엇이든) 테스팅이 무척 쉬워지는데, 외부에서 동작을 바라볼 수 있기 때문이다. 그저 동작을 선택하기만 하면 구현에 대해서 알 필요가 없다.
우리는 비밀번호를 함수의 유일한 매개변수로 결정했다. 비밀번호가 강력하거나 그렇지 않음을 표시하기 위해서 boolean
을 반환하기로 결정하기도 했다.
또한 빈 문자열에 대해, 결과값은 항상 false
라고 정했다 — 빈 문자열 비밀번호가 약함을 표시하기 위해서.
이제 모든 것을 테스트 코드에 연결해보자:
describe('isPasswordStrong', function() {
it('should give negative result for empty string', function() {
var password = ''; var result = isPasswordStrong(password); expect(result).to.be.false;
});
});
함수의 한줄 한줄을 모른 채로 쉽게 작성했다는 점에 주목하자. 우리는 빈 문자열이 매개변수로 전달되면, 결과값은 false
라고 결정했다. 테스트로 옮길 수 있는 하나의 단순한 동작이다.
5단계: 코드 구현
설명이 필요없다. 단지 테스트를 통과할 수 있는 최소한의 코드만 추가한다.
function isPasswordStrong(password) {
if(!password) {
return false;
}
}
계속해서 비밀번호 강도 측정 함수를 개발하고 싶다면, 그냥 반복하면 된다. 우리는 3단계로 돌아가서, 아주 작은 다음 단계를 선택할 것이다. 4단계, 테스트 로직 추가. 5단계, 구현. 반복.
이런 작은 절차들을 거듭해서 전진하다보면, 갑자기 TDD가 훨씬 쉬워질 것이다. 그렇다 — 결국엔 아주 짧은 코드에 대한 몇번의 테스트로 끝날 수도 있지만, 그것도 나쁘지 않다. TDD는 당신이 작성할 법한 쓸데없는 코드의 양을 줄여줌으로써 도움이 된다, 왜냐하면 당신이 추가하는 모든 코드가 테스트에 의해 검증되기 때문이다.
더 복잡한 예제
이런 방식이 정말 더 복잡한 문제에도 효과가 있을까? 너무 단순해보이는데.
스포 주의: 정답은, 그렇다!
조금 더 복잡한 예를 살펴보자, 좀 더 동적인 부분을 다룰 때 어떻게 이 방법이 효과적인지 직관적으로 알 수 있을 것이다.
테스트가 적당히 까다로운 예제는 뭐가 있을까?
디바운스 함수는 어떤가? 특정 시간범위 안에 이미 호출된 다른 어떤 함수도 호출하지 않음을 보장하는 것이 디바운스 함수의 개념이다. 예를 들어서 당신이 스크롤 이벤트를 조작해야 한다면, 특히 오직 사용자가 스크롤을 멈출 때만 작동하길 원한다면 이것이 매우 편리하다.
시간과 관련되기 때문에, 내가 제시한 5단계 절차가 좀 더 어려울 것이다.
1단계를 다시 해보자. 디바운스 함수의 입력과 출력은 무엇인가?
존재하면서 어떤 시점까지는 호출되지 않는 함수를 생성하는 것이 목표이기 때문에, 첫 번째 입력값은 함수가 아마 함수일 것이다. 두 번째 입력값은 디바운스를 위한 지속시간이 된다.
함수의 결과에 따라, 디바운스 함수는 원래 함수의 지연된 버전을 반환해야 하고, 비로소 함수가 호출될 것이다.
2단계: 함수 시그니처. 우리는 두 입력값을 함수의 매개변수로 전달하고, 함수는 새 함수를 반환한다. 간단하다.
대략 이런 식으로:
var delayedFunction = debounce(targetFunction, delayInMilliseconds);
3단계는 더 흥미로운데… 함수를 구현할 작은 부분을 선택해야한다. 디바운스가 가능한 부분은 다양하다: 예를 들면 지연으로, 반환된 함수를 충분한 지연시간 없이 여러번 호출한다면 호출이 안되는 것…등등.
하지만 일단 시작할만한 가장 단순한 것을 찾아보자. 디바운스에 의해 반환된 지연 함수를 호출한다면, 해당 함수는 일정 시간만큼 기다렸다가 원래 함수를 실행할 것이다. 내 생각엔 시작에 적당한 지점같아 보인다.
4벌써 4단계다. 이런 경우 테스트 코드는 어떻게 생겼을까?
이전과 마찬가지로, 정한 것들을 테스트와 연결해보자:
describe(‘debounce’, function() {
it(‘should call returned function after delay passes’,
function(done) {
var delay = 5;
var targetFn = function() {
done();
}; var delayedFn = debounce(targetFn, delay); delayedFn();
});
});
밀리초 단위의 지연시간이 필요하기 때문에, 거기서부터 시작할 것이다. 또한 대상 함수도 필요하다. 대상 함수는 지연시간 후에 호출되어야 하므로 이를 활용, 대상 함수의 콜백으로 done
함수를 호출함으로써 쉽고 빠르게 테스트를 검증할 수 있다. done
함수 호출이 안되면 — 테스트는 실패한다.
이어서, debounce
함수를 호출한다. 앞서 결정한 것처럼, 우리는 두 매개변수를 전달하고 결과값을 취한다. 마지막으로, 결과값을 호출하여 다음의 동작을 테스트한다: 지연 함수의 호출과 지연시간 이후에, 대상 함수가 호출되어야 한다.
우리는 정확한 구현을 알 필요가 없다 — 단지 앞 단계에서 얻은 정보를 곧장 테스트에 연결하면 된다. 한가지 알아둘 것은 자바스크립트에서 지연은 비동기적인 것, 따라서 우리는 비동기 테스트가 필요하다. 맞다, 이건 세부 구현에 해당하지만, 당신이 자바스크립트의 작동방식을 안다면 자연스럽게 따라오는 것이며 결코 이런 특정 함수만 해당되는 것이 아니다.
5단계에 바로 진입해서 코드를 구현하자:
function debounce(targetFn, delay) {
return function() {
targetFn();
};
}
잠깐! 함수를 전혀 지연시키지 않잖아!
맞다 — 우리는 TDD를 실천하는 중이니까! 필요한 모든 것은 작성한 테스트를 충족하는 것이고… 이 코드는 테스트를 통과한다.
누군가는 이걸 꼼수라고 하겠지만, 어쨌든 우리 모두 이것이 올바른 동작이 아닌걸 안다. TDD의 다른 말은 테스트를 통과할 정도로만 코드를 구현하는 것이므로, 그냥 넘어가자.
3단계로 돌아가서 구현할 또 다른 작은 동작을 선택할 것이다. 한가지 매우 중요한 동작은 함수가 너무 일찍 호출되지 않는 것으로, 당장 우리 코드의 동작과 비슷하다.
전진할 다음 단계가 생겼다. 4단계, 테스트 코드를 구현한다:
it(‘should not run debounced function too early’, function() {
var delay = 100;
var targetFn = function() { }; var delayedFn = debounce(targetFn, delay); delayedFn(); // 그런데 이걸 어떻게 검증하지?
});
‘Sinon’의 페이크 타이머가 필요할 것 같다. Sinon.js(테스트 스텁의 한 종류라고 합니다..)를 사용하여 페이크 타이머를 생성하여 더 진행할 수 있으며, 지연 된 함수가 예상보다 일찍 호출되진 않았는지 확인할 수 있다.
it(‘should not run debounced function too early’, function() {
var clock = sinon.useFakeTimers();
var delay = 100; var targetFn = sinon.spy();
var delayedFn = debounce(targetFn, delay);
delayedFn();
clock.tick(delay — 1);
clock.restore();
sinon.assert.notCalled(targetFn);
});
먼저, 페이크 타이머를 활성화한다. 대상 함수를 sinon.spy
로 변경한 것을 명심하자. 이것은 나중에 함수 호출 여부를 쉽게 검증하는 데 도움이 된다.
delayFn
함수를 호출한 뒤, 시간 경과를 위해 clock.tick
을 사용한다. 하지만, 필요한 지연시간보다 딱 1밀리초 부족하게 경과시켰다. sinon.assert.nonCalled
함수를 호출함으로써, 대상 함수가 너무 일찍 작동되는 지 확인할 수 있다.
Sinon의 기능이나 페이크 타이머에 대해서 더 알아보고 싶다면, grab my free Sinon.js in the Real-World guide에서 훨씬 자세한 내용을 다루고 있으니 참고하자.
결론
예제로 살펴본 것처럼, 우리는 모든 종류의 함수에 5단계 절차를 적용할 수 있다.
연습이 좀 필요하다면, 여기서 구현하기 시작한 두 함수 중 하나를 골라서, 완전히 동작할 때까지 5단계의 과정을 적용하는 연습을 해보라.
테스트 주도 개발은 한번 기본을 터득하고나면 어렵지 않다. 어려운 것은 사고방식을 전환하는 것이다: TDD 없이는, 무언가를 어떻게 구현할 지 직접적으로 생각한다. 하지만 TDD를 활용하면, 무엇이 어떻게 동작하길 원하는지 생각한다.
- 어떤 입력과 출력(동작)이 함수 호출에 필요한가?
- 코드에서 함수를 어떻게 호출할 것인지 결정하기
- 생각하는 입력에 대한 동작의 가장 작은 부분 선택하기
- 입력값으로 함수를 호출하는 테스트 코드를 작성하고, 동작을 검증하기
- 테스트를 충분히 통과하는 코드를 구현
이런 방식의 간단한 절차를 따르면, 테스트 코드를 미리 작성하는 것이 무척 쉬워진다. 계속해서 코드를 작성하려면, 단지 3단계에서 5단계를 반복하면 된다.
기억하자 — 테스트와 코드를 구현한 후 다르게 작동해야만 하는 걸 알아채더라도, 괜찮다! 계속해서 다시하면 된다 — 우리는 첫 술에 배부를 필요가 없으며, 그런 걸 추구해도 가로막힐 뿐이다. 이것은 TDD 만의 전유물이 아니다: 어쨌거나 당신은 코드의 일부분을 다시 작성하고 리팩토링해야 할 것이고, TDD는 변경된 코드가 망가지진 않았는지 검증하는 테스트로써 그 과정이 안전하도록 도와줄 뿐이다.
후기
적절한(!) 실패의 극복을 통해 보다 완벽해지는 것. 꼭 개발에만 국한된 이야기만은 아닌 것 같습니다. 어쩌면 배우는 과정이 모두 비슷하지 않을까요?
TDD, 도대체 어떻게 하는 건지 가이드를 뒤적이다가 우연히 눈에 띈 이 포스트를 번역하며 계속해서 든 생각입니다.
테스트 스텁이 무엇인지, jasmine 프레임워크의 문법은 무엇인지도 잘 모른 채로 TDD가 뭔지 배우기 위해서(그리고 새해니까…) 또 번역을 해봤습니다. 새해 시작부터 오래 붙잡기 싫어서 후다닥 마무리하면서 이런 것도 TDD 식인가.. 하는 엉뚱한 생각을 해봅니다.
제가 종종 다시보는 영상으로 글을 마치고.. 테스트 코드를 작성하러 가보겠습니다. 읽어주셔서 감사합니다~