Intro
- 비지니스 코드가 아닌 ui의 이벤트를 활용한 코드를 자스민으로 testing을 진행해보자.
jasmine 이란?
- 행위 주도 개발 (Behavior-Driven Development, BDD) 방식으로 자바스크립트 단위 테스트를 작성하기 위한 라이브러리
- 단위 테스트(unit test)란 코드의 기능 단위(funtionality unit)를 테스트 하는 것을 말한다.
- BDD는 단위 테스트로 확인할 기능 또는 작동 로직을 일상 언어로 서술할 수 있는데 개발자는 자신이 작성 중인 코드가 '어떻게' 가 아닌 '무엇'을 해야하는지 테스트 코드에 표현할수 있다.
- 공식홈페이지
테스트 꾸러미와 스펙
-
테스트 꾸러미는 describe로 정의할수있다.
// dscribe({문자열}, {함수}) decribe("무엇을 테스트할지 서술한다.",function(){ console.log("테스트 꾸러미의 구현부") })
-
스펙(즉, 개별테스트 )은 it으로 정의된다.
// dscribe({문자열}, {함수}) it("무엇을 테스트할지 서술한다.",function(){ // 적어도 한개의 기대식을 가진 함수 })
-
예)
describe('어떤 버튼은', ()=> { describe('클릭했을 때', ()=> { it('경고창을 띄운다', ()=> { }) }) })
- 테스트 꾸러미 구현부에 전역함수 beforeEach/afterEach 를 쓰면 각 꾸러미 테스트가 실행되기 이전에 beforeEach 함수를, 그 이후에는 afterEach 함수를 호출한다. 전체 테스트가 공유할 설정과 정리를 코드를 두 함수에 담아두면 코드 중복을 피할 수 있어 좋다.
기대식과 매처
- expect 함수는 테스트 대상 코드가 낸 실제값을 인자로 받아 기댓값과 견주어본다.
- 실제값과 기댓값을 비교하는 일은 매처 함수의 몫이다. 매처는 비교 결과 성공하면 true를 실패하면 false를 반환한다. 하나 이상의 기대식이 포함된 스펙(it함수) 에서 매처가 하나라도 실패하면 모조리 실패한 거로 간주한다.
- 여러 매처 중에 toBe 매처는 같은 객체여야 한다는 의미이다.
- 용도에 맞는 매처가 없으면 자스민이 지원하는 커스텀 매처를 만들어 쓴다.
expect({실제값이 리턴된다.}).toBe({기댓값})
- 커스텀 매처 만드는 법
// 기본 형식이다.
const customMacher = {
merlin: function(){
return {
compare: function(actual, expected){
let result = {}
// 여기서 result는 pass와 message 프로퍼티를 포함해야 한다.
// pass에는 boolean 값이 들어가야 하고 true / false
// message에는 pass가 true 됬을때 값일때 메시지나 false 됬을때 메시지를 적어준다.
return result
}
}
}
}
describe("테스트", function(){
beforeEach(function(){
// 인자로 커스텀 macher 객체를 넣어준다. 이 객체는 매처의 이름을 키 값으로 값는 메서드를 갖는다.
jasmine.addMatchers(customMacher);
})
})
스파이
- 자스민 스파이는 테스트 더블 역할을 하는 자바스크립트 함수다.
-
테스트 더블이란
- 더미 : 인자 리스트를 채우기 위해 사용되며 전달은 하지만 실제로 사용되지 않는다.
- 틀 : 더미를 조금 더 구현하여 아직 개발되지 않은 클래스나 메서드가 실제 작동하는 것처럼 보이게 만든 객체로 리턴값은 하드코딩한다.
- 스파이 : 틀과 비슷하지만 내부적으로 기록을 남긴다. 특정 객체가 사용되었는지, 특정인자로 호출되었는지 등의 상황을 감시한다.
- 모이체 : 틀에서 조금 더 발전하여 실제로 간단한 구현된 코드를 갖고는 있지만 운영환경에선 사용할수는 객체다.
- 모형 : 더미, 틀 , 스파이를 혼합한 형태와 비슷하나 행위를 검증하는 용도로 쓰인다.
- jasmine.createSpy 함수의 경우 빈 껍데기 스파이이다.
// 예제.
callbackSpy = jasmine.createSpy();
expect(callbackSpy.calls.count()).expect(array.length);
- spyOn 함수를 쓰면 특정 함수를 들여다 볼수 있다. 첫번째 인자는 객체 인스턴스, 두번째 인자는 감시할 함수명이다.
- spyOn 함수는 기존 구현부를 대체하는 함수이다.
// 예제
spyOn(saver,'saveReservation');
spyOn(api, 'getRestaurnatsNearConference').and.returnValue(returnedFromService);
expect(saver.saveReservation).toHaveBeenCalled();
Ui test
좋지 못한 html 코드
- 클릭하면 span 돔에 카운트가 늘어나는 코드이다.
- html과 javascript가 혼재해 있어서 지저분할분 아니라 javascript 코드를 복붙으로 밖에 재활용이 안된다.
<!DOCTYPE html>
<html>
<head>
<script type="text/javascript">
var clickCount = 0,
displayCount = function() {
var countElement = document.getElementById("countDisplay");
countElement.innerText = clickCount.toString();
}
</script>
</head>
<body>
<button type="button" onclick="clickCount++; displayCount();">
Increment
</button>
<span id="countDisplay">0</span>
</body>
</html>
테스트 코드 및 모듈 작성(1)
- 기존에 인라인으로 들어간 스크립트를 모듈로 빼두자. 그러면 재사용성이 좋아진다.
- 이벤트 발생시 카운트가 증가해서 저장하는 부분과 DOM업데이트 함수로 나눈다. 그럼 비지니스 로직만 테스트할 수 있음.
- 우선 DOM과 엮이지 않은 코드를 작성해서 테스트를 진행한다.
function clickCountDisplay(){
let count = 0;
return {
// 현재 count 구하기
getClickCount: function(){
return count;
},
// count를 Dom에 render
updateCountDisplay: function(){
},
// count 증가 및 update Dom
incrementCountAndUpdateDisplay: function(){
count++;
this.updateCountDisplay();
}
}
}
// test코드
decribe("clickCountDisplay", function(){
"use strict"
beforeEach(function(){
let display = clickCountDisplay();
})
it("0으로 초기화",function(){
expect(display.getClickCount()).toEqual(0)
});
describe("incrementCountAndUpdateDisplay()", function() {
it("count 증가 시킨다.",function(){
let initCount = display.getClickCount();
display.incrementCountAndUpdateDisplay();
expect(display.getClickCouny()).toEqual(initCount+1);
});
it("updateCountDisplay 함수를 실행시킨다.",function(){
spyOn(display,"updateCountDisplay");
display.incrementCountAndUpdateDisplay();
expect(display.updateCountDisplay).toHaveBeenCalled();
})
})
})
테스트 코드 및 모듈 작성(2)
- DOM이 바뀌는지 테스트를 진행한다.
- updateCountDisplay 함수를 어떻게 테스트할지 생각해보자.
- updateCountDisplay 이 함수가 조작할수 있는 DOM 요소를 제공해야한다.
- DOM 요소를 주입하고 나서 함수를 실행했을때 그 값이 잘 렌더링 되는지 확인한다.
function clickCountDisplay(opts){
if(!opts) throw new Error("opts를 주입해야합니다.");
let count = 0;
return {
// 현재 count 구하기
getClickCount: function(){
return count;
},
// count를 Dom에 render
updateCountDisplay: function(){
opts.updateElement.innerHTML(count);
},
// count 증가 및 update Dom
incrementCountAndUpdateDisplay: function(){
count++;
this.updateCountDisplay();
}
}
}
// test코드
decribe("clickCountDisplay", function(){
"use strict"
let display,
displayElement;
beforeEach(function(){
displayElement = document.createElement("span");
document.body.appendChild(displayElement);
clickElement = document.createElement("button");
document.body.appendChild(clickElement);
let options = {
updateElement: displayElement,
triggerElement: clickElement
}
// Element를 주입.
display = clickCountDisplay(options);
})
afterEach(function(){
displayElement.remove();
clickElement.remove();
})
describe("incrementCountAndUpdateDisplay()", function() {
it("updateDisplay",function(){
display.incrementCountAndUpdateDisplay();
expect(displayElement.innerText).teEqual(display.getClickCount());
})
});
describe("updateCountDisplay()", function() {
it("횟수를 한번도 늘린 적 없으면 0이 표시된다", function() {
expect(displayElement.innerText).teEqual("");
display.updateCountDisplay();
expect(displayElement.innerText).teEqual("0");
});
});
})
테스트 코드 및 모듈 작성(3)
- 이벤트 트리거가 잘 해당 함수를 호출하는지 확인한다.
// 테스트 코드
// .. 나머진 생략
it("클릭이벤트가 발생하면 incrementCountAndUpdateDisplay를 호출한다.",function(){
spyOn(display, "incrementCountAndUpdateDisplay")
clickElement.dispatchEvent(new Event('click')); // 클릭 발생시
expect(display.incrementCountAndUpdateDisplay).toHaveBeenCalled();
})
function clickCountDisplay(opts){
if(!opts) throw new Error("opts를 주입해야합니다.");
let count = 0;
const module = {
// 현재 count 구하기
getClickCount: function(){
return count;
},
// count를 Dom에 render
updateCountDisplay: function(){
opts.updateElement.innerHTML(count);
},
// count 증가 및 update Dom
incrementCountAndUpdateDisplay: function(){
count++;
this.updateCountDisplay();
}
}
opts.triggerElement.addEventListener('click', function(){
module.incrementCountAndUpdateDisplay();
})
return module
}
결론
-
UI 단위테스트는 다음을 확인하는 정도로 확인해야한다.
- 요소를 클릭하면 알맞은 처리기가 확실히 실행되는가?
- 사용자가 보면 안될 UI 요소가 있는가?