우리는 지금 세상을 조금 더 효율적으로 바꾸기 위해 소프트웨어를 개발하는 방법에 대해서 배우고 있습니다. 여러분들도 느끼셨겠지만, 수학과는 달리 개발은 공학과 설계에 더욱 가깝습니다.
흔히들 공학 전공자는 수학을 잘해야 한다고 합니다. 자동차 하나를 만든다고 가정해 보겠습니다. 이 자동차가 자동차의 구실을 하기 위해서는 많은 부품이 필요합니다. 이 부품이 잘 맞물려서 작동하려면, 이것이 잘 작동하는지 검증하는 과정이 필요한데, 이 과정에서 수학을 사용하곤 합니다. 자동차 부품 중 하나의 무게가 조금만 바뀌어도 사고로 이어질 수 있기 때문입니다. 따라서 수학으로 엄밀하게 검증하는 과정을 거쳐서, 이론적으로 완벽한 설계를 하는 것이 필요합니다.
그렇다면 소프트웨어는 어떻게 검증할까요? 여러 가지 방법이 있겠지만, 가장 좋은 방법은 역시 테스트 코드를 작성하는 것입니다. 예를 들어 카카오톡 같은 메신저 애플리케이션을 만들 경우, 메시지가 제대로 전송되는지, 로그인은 잘 되는지 등을 테스트 코드를 작성하여 충분히 테스트한 후에 사용자에게 전달하는 것이 바람직합니다.
TDD(Test-driven Development)는 코드를 작성하기 전에 테스트를 쓰는 소프트웨어 개발 방법론입니다. 다시 말해, 개발자 자신이 바람직하다고 생각하는 코드의 결과를 미리 정의하고, 이것을 바탕으로 코드를 작성하는 법입니다. TDD를 통해 소프트웨어를 개발한다는 것은 작은 단위의 테스트 케이스를 작성하고, 이를 통과하는 코드를 작성하는 과정을 반복하는 것을 의미합니다.
TDD의 개발 주기를 그림으로 나타내면 아래와 같이 총 3단계로 이루어집니다.
Write Failing Test: 실패하는 테스트 코드를 먼저 작성한다.
Make Test Pass: 테스트 코드를 성공시키기 위한 실제 코드를 작성한다.
Refactor: 중복 코드 제거, 일반화 등의 리팩토링을 수행한다.
이 과정에서 1의 과정을 마치기 전에 2의 작업을 시작하지 않도록 주의해야 합니다. 또한 2를 진행할 때에는, 1의 테스트를 통과할 정도의 최소 코드만 작성해야 합니다. 테스트를 먼저 작성하는 것은 필연적으로 코드를 어떻게 구성할지 고민하게 된다는 것을 의미하고, 결과적으로 버그가 더 적은 코드를 짤 수 있게 됩니다. 또, 불필요한 설계를 피할 수 있고, 테스트 코드의 요구 사항에 집중할 수 있게 됩니다. 일반적으로 TDD를 따라 소프트웨어를 개발할 경우 그렇지 않은 경우보다 결함을 50 ~ 90% 까지 감소시킬 수 있습니다.
이처럼 버그가 적은, 보다 효과적인 코드를 짤 수 있는 방법임에도 불구하고, 실제로 완전한 TDD를 따르는 개발자는 의외로 많지 않습니다. 그 이유는 대부분의 개발자들이 생각하고 일하는 방식과 일치하지 않기 때문입니다. 대부분의 개발자는 테스트를 작성하는 것보다, 만들어야 하는 것을 바로 코드로 작성하는 방식이 훨씬 자연스럽고 빠르다고 느낄 것입니다. 많은 개발자들에게 왜 TDD를 따르지 않는지 물어보면, 대부분 ‘속도' 때문이라고 대답할 것입니다.
그럼에도 불구하고 TDD를 사용하는 이유는 무엇일까요? 코드를 작성하기에 앞서 테스트 코드를 먼저 작성해야 하기 때문에 시간이 오래 걸리는 것처럼 느껴지지만, 오히려 예상하지 못했던 버그를 줄여 소요 시간을 줄일 수 있기 때문입니다. 개발 과정에서 코드는 다양한 조건에 의해 계속해서 삽입, 수정, 삭제됩니다. 이 과정에서 코드가 중복되거나 불필요한 코드가 남게 됩니다. 그리고 그로 인해 여러 가지 버그가 발생하거나, 디버깅 또한 어려워지는 현상이 발생하기도 합니다. 결국 그런 코드를 유지보수하기 위해서는 처음 개발할 때 아꼈던 리소스보다 더 많은 리소스를 투입해야 하는 경우가 발생합니다.
여러분이 지금 당장 완전한 TDD를 따르는 것은 아주 어려운 일입니다. TDD 방법론에 대해 학습하되, 여러분의 개발 실력을 향상시키는 것을 더 우선순위에 두어야 합니다. 그러나 작성하려는 코드에 대해 특정한 규칙(테스트)을 설정하기 위해 고민하면서, 코드가 큰 틀에서 어떤 의미를 갖게 되는지 고민하는 것은 여러분이 스스로를 성장시킬 수 있는 중요한 도구가 될 것입니다.
여러분은 지금까지 과제를 진행하면서 console.log를 사용하여 현재 작성한 코드가 어떤 결과물을 도출하는지 확인한 경험이 있을 겁니다. console.log를 통해 확인하는 것도 일종의 테스트입니다.
또, JavaScript Koans 과제를 진행하면서 테스트를 작성해 보기도 했습니다. 그 과정에서 describe, it, assert, expect 등의 다양한 키워드들을 마주쳤을 겁니다. 이 키워드가 무엇을 의미하는 것인지 궁금하지 않으셨나요? 결론부터 이야기하면 이 키워드들은 JavaScript 내장 기능이 아니라 테스트 프레임워크에서 제공하는 테스트 작성을 위한 도구입니다.
여러 개발자들이 더 나은 테스트를 작성하기 위해 많은 테스트 오픈소스 프레임워크를 제작했습니다. 이후 진행할 Test Builder 과제에서는 mocha라는 테스트 프레임워크와 chai라는 라이브러리를 사용합니다.
Test Builder 과제를 통해 처음에는 테스트 없이 바닥부터 함수를 작성해보고, console.log를 이용해 결과를 확인합니다. 이후 STEP을 진행해나가면서, 테스트 케이스가 있었을 때와, 없었을 때의 차이점을 이해해보고 자연스럽게 테스트 케이스를 작성하는 연습하기
//detectNetwork.js
/**
* 아래의 detectNetwork 함수는 어떤 카드 번호를 input으로 받아,
* 카드 회사의 이름('MasterCard', 'American Express)을 return 하는 함수입니다.
*
* 예) detectNetwork('343456789012345') // 'American Express'
*
* 그럼 어떻게 카드 번호만 가지고, 카드회사를 알 수 있을까요?
*
* 2가지 방법이 있습니다.
* 1. 앞 자리 숫자들 (이번 과제에서는 prefix라 부릅니다.)
* 2. 숫자들의 길이 (이번 과제에서는 length라고 부릅니다.)
*/
function detectNetwork(cardNumber) {
let num = cardNumber.slice(0,2);
if((num==="38"||"39") && (cardNumber.length===14)) return "Diner's Club";
else if((num==="34"||"37") && (cardNumber.length===15)) return 'American Express';
else if((cardNumber[0]==="4") && (cardNumber.length===13||16||19)) return "Visa";
else if((num==="51"||num==="52"||num==="53"||num==="54"||num==="55") && (cardNumber.length===16)) return 'MasterCard';
else if(((cardNumber.slice(0,4)==="6011") || (cardNumber.slice(0,2)==="65")
|| (cardNumber.slice(0,3)==="644"||"645"||"646"||"647"||"648"||"649"))
&& (cardNumber.length===16||cardNumber.length===19)) return "Discover";
}
// you don't have to worry about this code. keep this code.
if (typeof window === "undefined") {
module.exports = detectNetwork;
}
//detectNetwork.test.js
/*
* 이 파일을 어떻게 사용해야 하는지 STEP을 진행하다보면 알 수 있습니다.
* 만일 그 전에 이 파일을 이용하고 싶다면 주석을 참고하여 직접 연구해야 합니다.
*/
/**
* 11번 줄에 있는 FILL_ME_IN을 수정하실 필요는 없습니다.
* 하지만 이 파일의 다른 곳에서 FILL_ME_IN을 보시게 된다면 다른 것으로 바꾸셔야합니다.
*/
let FILL_ME_IN = "Fill this value in";
describe("Introduction to Mocha Tests - READ ME FIRST", function() {
// Mocha 테스트는 그저 다음 기능을 하는 도구입니다!
// - 함수를 실행할 때 오류가 발생하면, 실패합니다.
// - 오류가 발생하지 않으면, 실패하지 않습니다.
// mocha에 대해 더 알고 싶다면 다음을 참고하세요. mochajs.org
// 먼저 아래의 테스트를 수정해 테스트가 정상적으로 작동하도록 해주세요.
// 그리고 Diner's club과 American Express 테스트로 넘어가주세요
it("오류가 발생하면 테스트가 실패합니다.", function() {
//throw new Error("저를 지워주세요!");
});
it("오류가 발생하지 않으므로, 실패하지 않습니다.", function() {
// 이 테스트는 실제로 아무것도 테스트하지 않습니다. 그러므로 그냥 여기는 통과하게 됩니다.
let even = function(num) {
return num % 2 === 0;
};
return even(10) === true;
});
// 우리는 테스트에서 예상 동작과 실제 동작을 비교하기를 원할 것입니다.
// 예상 동작이 실제 동작과 다르다면, 테스트는 실패해야 합니다.
it("예상 동작이 실제 동작과 일치하지 않을 때 오류가 발생합니다.", function() {
let even = function(num) {
return num % 2 === 0; // 체크하려는 함수에 뭔가 문제가 있군요!
};
if (even(10) !== true) {
throw new Error("10은 짝수여야 합니다!");
}
});
});
/**
* 아래의 테스트들은 detectNetwork 함수를 detectNetwork.js 파일로부터 불러와
* 함수가 정상적으로 작동하는지 테스트합니다.
* detectNetwork.js파일과 현재 파일을 수정해 모든 테스트가 통과하도록 만들어보세요.
*/
describe("Diner's Club", function() {
// 주의하세요, 테스트에도 버그가 존재할 수 있습니다...
it("has a prefix of 38 and a length of 14", function() {
if (detectNetwork("38345678901234") !== "Diner's Club") {
throw new Error("Test failed");
}
});
it("has a prefix of 39 and a length of 14", function() {
if (detectNetwork("39345678901234") !== "Diner's Club") {
throw new Error("Test failed");
}
});
});
describe("American Express", function() {
// 항상 if/throw 구문으로 오류를 체크하는 것은 귀찮은 일이기 때문에,
// 여기에 도움을 줄 수 있는 함수를 하나 제공했습니다. 입력값이 true가 아닐 경우 에러를 발생시킵니다.
let assert = function(isTrue) {
if (!isTrue) {
throw new Error("Test failed");
}
};
it("has a prefix of 34 and a length of 15", function() {
assert(detectNetwork("343456789012345") === "American Express");
});
it("has a prefix of 37 and a length of 15", function() {
assert(detectNetwork("373456789012345") === "American Express");
});
});
describe("Visa", function() {
// Chai는 테스트에 필요한 헬퍼 함수들이 담긴 라이브러리입니다!
// Chai는 이전에 만들었던 assert 함수와 동일한 기능을 하는 assert 함수를 제공합니다.
// Chai가 제공하는 assert 함수를 어떻게 사용하는지 웹사이트의 공식 문서를 참고해보세요.
// http://chaijs.com/
let assert = chai.assert;
it("has a prefix of 4 and a length of 13", function() {
assert(detectNetwork("4123456789012") === "Visa");
});
it("has a prefix of 4 and a length of 16", function() {
assert(detectNetwork("4123456789012345") === "Visa");
});
it("has a prefix of 4 and a length of 19", function() {
assert(detectNetwork("4123456789012345678") === "Visa");
});
});
describe("MasterCard", function() {
// Chai는 좀 더 영어 문법에 가까운 코드로 테스트를 작성할 수 있게 도와줍니다.
// Expect 문법은 그 중 한가지이며, 다른 문법도 있습니다.
// 이와 관련해 더 알고 싶다면, 공식 문서를 참고하세요.
// http://chaijs.com/api/bdd/
let should = chai.should();
it("has a prefix of 51 and a length of 16", function() {
should.equal(detectNetwork("5112345678901234"), "MasterCard");
});
it("has a prefix of 52 and a length of 16", function() {
should.equal(detectNetwork("5212345678901234"), "MasterCard");
});
it("has a prefix of 53 and a length of 16", function() {
should.equal(detectNetwork("5312345678901234"), "MasterCard");
});
// expect 대신에 should라는 문법을 사용해서 스타일을 조금 변경할 수도 있습니다.
// 사실 둘 중 어떤 것을 사용하는지는 중요하지 않습니다.
// 스타일에 관련해서는 다음 사이트를 참조하세요. http://chaijs.com/guide/styles/
// 다만 중요한 것은 스타일의 일관성을 유지하는 것입니다.
// (우리는 공부를 하는 중이기 때문에 두가지 방법 모두를 사용해 보았습니다.)
// 테스트를 작성하는 중에, 두가지 방법을 동시에 사용하려고 하면 진행되지 않을 것입니다.
// expect나 should 둘 중에 한가지 방법을 선택해서 사용하세요.
it("has a prefix of 54 and a length of 16", function() {
should.equal(detectNetwork("5412345678901234"), "MasterCard");
});
it("has a prefix of 55 and a length of 16", function() {
should.equal(detectNetwork("5512345678901234"), "MasterCard");
});
});
describe("Discover", function() {
// 함수가 없는 테스트는 "pending"이라는 표시가 뜨며 실행되지 않습니다.
// 아래 테스트를 작성하고 테스트가 통과하도록 만드십시오.
let expect = chai.expect;
it("has a prefix of 6011 and a length of 19", function() {
expect(detectNetwork("6011345678901256734")).to.equal("Discover");
});
it("has a prefix of 6011 and a length of 16", function() {
expect(detectNetwork("6011123456789012")).to.equal("Discover");
});
it("has a prefix of 65 and a length of 16", function() {
expect(detectNetwork("6511345678901237")).to.equal("Discover");
});
it("has a prefix of 65 and a length of 19", function() {
expect(detectNetwork("6511345678901256734")).to.equal("Discover");
});
for(let i=644; i<650; i++){
it(`has a prefix of ${i} and a length of 16`, function() {
expect(detectNetwork(`${i.toString()}1345678901237`)).to.equal("Discover");
});
it(`has a prefix of ${i} and a length of 19`, function() {
expect(detectNetwork(`${i.toString()}1345678901256734`)).to.equal("Discover");
});
}
});