Created by 손찬욱 / chanuk.son
경력 10년차 프로젝트 구성원이 코드를 수정했다.
이를 믿을 수 있나?
새롭게 투입된 경력 10년차 개발자가 코드를 수정했다.
이를 믿을 수 있나?
QA나 개발자가 직접적으로 변경 부분에 대해 side-effect를 실제 테스트하여 확인한다.
새로운 경력 1년차 개발자가 코드를 수정했다.
신입 프로젝트 구성원이 코드를 수정했다.
테스트 코드는
개발(action) -> 테스트 (effect)->
피드백 -> 개발 -> 테스트 -> 피드백 ...
또한, 테스트 코드는
이런 장점이 있음에도
서비스 일정은 정해졌지만, 스펙은 계속 늘어나고,
늘어나기만 하면 괜찮지만, 다 바꾸기까지하죠.
설계 + 구현 + 테스트 코드 작성
소스와 같이 소중히...
품질 관리의 시작은 QA 시작부터가 아닌,
테스트 코드 부터...
좀 더 자세히 이야기해보자.
애자일 방법론에서 사용하는 X-Driven Development 방식
둘 다 차이가 없음. 단, BDD는 비즈니스 요구사항 중심적. (좀더 자연어에 걸맞게)
테스트 First. 실패 First
Red - Green - Refactor의 반복
사용자 관점의 명세를 만든다.
숫자가 3으로 나누어지면 결과값은 "Fizz"
숫자가 5로 나누어지면 결과값은 "Buzz"
숫자가 3과 5로 나누어지면 결과값은 "FizzBuzz"
http://martinfowler.com/bliki/GivenWhenThen.html
테스트 프레임워크 : jasmine, qunit, mocha
mocha는 별도의 assertion 모듈(chai, should, expect)이 필요함
describe("the name of test suite", function() {
beforeAll(function() {}); // suite 전에 호출
afterAll(function() {}); // suite 후에 호출
beforeEach(function() {}) // 각각의 spec 전에 호출
afterEach(function() {}) // 각각의 spec 후에 호출
it("should/contains ... spec1 ", function() {
// except(value).matchers
except(true).toBe(true);
except(true).not.toBe(false);
});
it("should/contains ... spec2 ", function() {
except(true).toEqual(true);
})
// ...
});
Given/When/Then 패턴으로 작성한다.
describe("FizzBuzz", function() {
it("should return 'Fizz' when the number is divisible by 3", function() {
// Given
// When
// Then
});
})
테스트 코드를 작성한다.
describe("FizzBuzz", function() {
it("should return 'Fizz' when the number is divisible by 3", function() {
// Given
var fizzBuzz = new FizzBuzz();
// When
var result = fizzBuzz.call(3);
// Then
expect(result).toEqual("Fizz");
});
})
테스트가 실패 했다.
코드를 수정한다.
function FizzBuzz() {}
FizzBuzz.prototype.call = function(num) {
return "Fizz";
}
테스트가 성공 했다.
function FizzBuzz() {}
FizzBuzz.prototype.call = function(num) {
if (num % 3 === 0) {
return "Fizz";
}
}
$(document).ready(function(){
var div1 = $('div.one'),
itemCount = getItemCount('li.item'),
value = window.scrollTop;
div1.on('click',function(){ //... });
function getItemCount(selector) {
return $(selector).length;
}
});
closure로 인한 은닉
이벤트 핸들러 은닉
외부(전역) 객체의 사용
Loosely Coupling. Modular
var app = {
init : function(global){
this.value = global.scrollTop
div1.on(
'click', this.handleDivClick
);
this.itemCount = this.getItemCount( 'li.item' );
},
getItemCount : function( selector ){
return $( selector ).length;
},
handleDivClick : function( e ){
this.global.scrollTop
}
};
Test double : 테스트시에 실제 객체를 대신 할 수 있는 객체
spyOn, createSpy, createSpyObj 를 사용
describe("A spy", function() {
it("tracks that the spy was called", function() {
// Given
var foo = {
getBar: function() {
return bar;
}
};
spyOn(foo, "getBar");
// When
foo.getBar();
// Then
expect(foo.getBar).toHaveBeenCalled();
});
});
and.returnValue, and.callFack 등을 사용
describe("A spy, when configured to fake a return valu", function() {
it("when called returns the requested value", function() {
// Given
var foo = {
getBar: function() {
return bar;
}
};
spyOn(foo, "getBar").and.returnValue(745);
// When
var bar = foo.getBar();
// Then
expect(bar).toEqual(745);
});
});
it("should support async", function(done) {
// ...
});
done();
describe("Asynchronous specs", function() {
var value;
it("should support async execution", function(done) {
// Given
value = 100;
expect(value).toEqual(100);
// Then
setTimeout(function() {
expect(value).toEqual(101);
done();
},100); // 100ms 대기
// When
value++;
});
});
jasmine.clock().install();
jasmine.clock().uninstall();
timerCallback = jasmine.createSpy();
setInterval(function() {
timerCallback();
}, 100);
jasmine.clock().tick(time);
describe("Manually ticking the Jasmine Clock", function() {
var timerCallback;
beforeEach(function() {
timerCallback = jasmine.createSpy();
jasmine.clock().install(); // timer mocking start
});
afterEach(function() {
jasmine.clock().uninstall(); // timer mocking end
});
it("causes a timeout to be called synchronously", function() {
// Given
setTimeout(function() {
timerCallback();
}, 100);
expect(timerCallback).not.toHaveBeenCalled();
// When
jasmine.clock().tick(101);
// Then
expect(timerCallback).toHaveBeenCalled();
});
it("causes an interval to be called synchronously", function() {
// Given
setInterval(function() {
timerCallback();
}, 100);
expect(timerCallback).not.toHaveBeenCalled();
// When
jasmine.clock().tick(101); // 101
// Then
expect(timerCallback.calls.count()).toEqual(1);
// When
jasmine.clock().tick(50); // 151
// Then
expect(timerCallback.calls.count()).toEqual(1);
// When
jasmine.clock().tick(50); // 201
// Then
expect(timerCallback.calls.count()).toEqual(2);
});
});
jasmine.Ajax.install();
jasmine.Ajax.uninstall();
jasmine.Ajax.stubRequest('/another/url').andReturn({
"responseText": 'immediate response'
});
done = jasmine.createSpy();
xhr.onreadystatechange = function(args) {
if (this.readyState == this.DONE) {
done(this.responseText);
}
};
describe("mocking ajax", function() {
describe("suite wide usage", function() {
beforeEach(function() {
jasmine.Ajax.install();
});
afterEach(function() {
jasmine.Ajax.uninstall();
});
it("allows responses to be setup ahead of time", function () {
// Given
var doneFn = jasmine.createSpy();
jasmine.Ajax.stubRequest('/another/url').andReturn({
"responseText": 'immediate response'
});
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function(args) {
if (this.readyState == this.DONE) {
doneFn(this.responseText);
}
};
// When
xhr.open("GET", "/another/url");
xhr.send();
// Then
expect(doneFn).toHaveBeenCalledWith('immediate response');
});
});
});
setFixtures(sandbox());
loadFixtures("domtest.html");
expect($('#sandbox'))
.not.toHaveClass('TestClass');
expect('click')
.toHaveBeenTriggeredOn('#sandbox');
describe("Using sandbox", function() {
it ("should remove a class", function() {
// Given
setFixtures(sandbox({class: 'TestClass'}));
expect($('#sandbox')).toHaveClass('TestClass');
// When
$('#sandbox').removeClass("TestClass");
// Then
expect($('#sandbox')).not.toHaveClass('TestClass');
});
it ("should invoke the btnShowMessage click event.", function() {
// Given
setFixtures(sandbox());
var spyEvent = spyOnEvent('#sandbox', 'click');
// When
$('#sandbox').trigger( "click" );
// Then
expect('click').toHaveBeenTriggeredOn('#sandbox');
expect(spyEvent).toHaveBeenTriggered();
});
});
window, document 테스트
function service_mod = function(global, doc) {
// ...
};
service_mod({
navigator: {
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 9_0 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Mobile/13A452 Safari Line/5.4.0"
}, document
})
소스는 global 객체를 주입할 수 있게 구성한다.
var agent = function(global) {
return /iPhone|iPad/.test(global.navigator.userAgent) ?
"ios" : "android";
};
테스트에서는 Mock 객체를 주입하여 테스트 한다.
describe("Using sandbox", function() {
it ("should return OS type", function() {
// Given
var mockWindow = {
navigator: {
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 9_0 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Mobile/13A452 Safari Line/5.4.0"
}
};
// When
var osTypo = agent(mockWindow);
// Then
expect(osTypo).toEqual('ios');
});
});
알고는 있자...
테스트 코드가 많아진다.
= 소스의 코드가 많아진다.
= 변경시 비용이 많이 든다.
결산 로직, 공통 라이브러리...
회기 테스트를 위한 테스트 코드 작성
신뢰 할 수 있는가?
X-Driven 방식의 개발 프로세스가 잘되고 있는지
회기 테스트가 잘되고 있는지
가 더 중요하다.
테스트 코드는 하나의 서비스다