자바스크립트에서
'1' + 1 = ?
'2' * 3 = ?
'1' + 2 + 3 * 4 = ?
'1' + 1 = 11
'2' * 3 = 6
'1' + 2 + 3 * 4 = 1212
자바스크립트는 컴파일러가 없으니 테스트가 최선이다.
직접실행해서 결과를 검증하자.
자바스크립트 TDD 를 해보자!
테스트의 개념
단위(유닛) 테스트
인풋에 따라 계산한 결과를 아웃풋으로 내놓는 것. ( 단위테스트는 함수를 테스트 하는 것이라고 생각하면됨)
단위테스트 , 준비 , 실행 , 단언(검증) 단계로 나뉘어집니다.
테스트 주도개발
레드, 그린 , 리펙터 순환 구조
테스트하기 쉬운 코드
관심사의 분리
함수는 하나의 기능만 해야함
자바스크립트의 테스트 프레임워크 - 재스민 프레임워크
재스민 프레임워크 다운 후 SPEC RUNNER.HTML 파일을 클릭하면 성공 간단하쥬?
SPECRUNNER.HTML 파일 구조
//자스민 프래임워크 로드
<script src="lib/jasmine-3.3.0/jasmine.js"></script>
<script src="lib/jasmine-3.3.0/jasmine-html.js"></script>
<script src="lib/jasmine-3.3.0/boot.js"></script>
//테스트 할 소스
<!-- include source files here... -->
<script src="src/Player.js"></script>
<script src="src/Song.js"></script>
//테스트 파일
<!-- include spec files here... -->
<script src="spec/SpecHelper.js"></script>
<script src="spec/PlayerSpec.js"></script>
형식으로 이루어져있음
테스트 꾸러미 (test suite)
테스트 스펙 it
기대식과 매쳐
expect(결과값).toBe(기대하는 값)
스파이 spyOn(감시할 객체, 감시할 메소드)
테스트 형식
describe('hello world', ()=> { // 테스트 스윗: 테스트 유닛들의 모음
it('true is true', ()=> { // 테스트 유닛: 테스트 단위
expect(true).toBe(true) // 매쳐: 검증자
})
테스트 할 수 없는 코드
<button onclick="counter++; countDisplay()"> 증가</button>
<span id="counter-display">0</span>
<script>
var counter = 0;
function countDisplay() {
var el = document.getElementById('count-display');
el.innerHTML= count
}
</script>
위 코드의 문제 점은 무엇일까?
1.관심사의 미분리
2.전역변수의 충돌
3.재사용 어려움
1.관심사의 미분리
관심사가 분리되지 않았음 클릭 이벤트가 처리기를 인라인 형태로 정의한 점
한가지 코드는 하나의 역할만 해야함
<button onclick="counter++; countDisplay()"> 증가</button>
카운트 증가와 디스플레이 2가지의 역할을 하고 있음
전역 변수의 충돌
var counter 전역변수를 사용하여 어지럽힌 점 전역변수 사용은 전형적인 안티 패턴이기 때문에 사용하더라도 제한적으로 사용 해야함
재사용 어려움
var el = document.getElementById('count-display')
함수에 count-display를 하드코딩 했기 때문에 다른 선택자를 찾으려면 해당 함수를 사용할수없고 새로운 함수를 만들어야 하기 때문에 재사용이 어려움
open-close 원칙에 의거 쉽게 추가하면안되고 확장에는 언제든 열려있어야 한다.
자바스크립트에서 문제를 해결 하는 방식에는 모듈 패턴이라는 것이 있음
모듈 패턴이란 ? 함수로 데이터를 감추고 모듈 API를 담고 있는 객체를 반환하는 형태
모듈 패턴에는 1. 임의 모듈 패턴 , 2. 즉시 실행 함수 기반의 모듈 패턴이 있음
모듈생성원칙
한가지 모듈에는 한가지 역할만 한다.
모듈에 사용할 객체가 있다면 의존성 주입형태로 제공해야한다.
임의모듈패턴으로 기존코드 수정하기
기존의 클릭카운터 소스를 수정해보자.
A 우선 getValue()는 카운트 값을 반환하도록 수정해보자.
실패하는 테스트 코드 작성하기
describe('App.ClickCounter', ()=> {
describe('getValue()', ()=> {
it('초기값이 0인 카운터 값을 반환한다', ()=> {
// todo
const counter = App.ClickCounter()
expect(counter.getValue()).toBe(0)
})
})
})
당연히 실패한다 모듈을 만든적이 없으니까.
2. 모듈을 만들어 보자
var App = App || {}
App.ClickCounter = () =>{
return {
getValue() {
return 0
}
}
}
ClickCounter getValue() 불러왔을 때 0을 호출하는 모듈을 생성 후 재스민으로 테스트 하면 오류가 나지 않는다.
모듈을 수정하자 카운터가 증가해야하나 현재는 상수로 박아놨기 때문에 그부분을 변수로 처리 후 수정해보자. 리펙토링 단계
var App = App || {}
App.ClickCounter = () =>{
let value = 0;
return {
getValue() {
return value
}
}
}
정상이다.
이 것이 테스트 주도 개발의 한 사이클이라고 보면될것 같다.
레드(오류) --> 그린(패스) --> 블루(리펙터) 반복
두번째 스펙
B. ClickCounter 의 increase() 는 카운터 값을 1씩 증가시킨다.
테스트 코드를 작성한다 (레드)
describe('increase()', ()=> {
it('카운터를 1 올린다', ()=> {
var counter = App.ClickCounter()
counter.increase();
expect(counter.getValue()).toBe(1);
})
})
})
실행하면 당연히 오류다 왜냐하면 increase() 모듈을 작성한적이 없쟈나쟈나
increase() 모듈을 작성한다
var App = App || {}
App.ClickCounter = () => {
let value = 0
return {
getValue() {
return value
},
increase(){ //increase() 모듈 실행 시 value 값을 1 증가 시킨다.
value++;
}
}
}
이후 테스트 하면 당연히 정상이다.
이번에는 테스트 코드를 수정해보자.
describe('App.ClickCounter', ()=> {
describe('getValue()', ()=> {
it('초기값이 0인 카운터 값을 반환한다', ()=> {
const counter = App.ClickCounter()
expect(counter.getValue()).toBe(0)
})
})
describe('increase()', ()=> {
it('카운터를 1 올린다', ()=> {
const counter = App.ClickCounter()
counter.increase();
expect(counter.getValue()).toBe(1);
})
})
})
전체 코드를 봤을 때 중복 코드가 보인다.
const counter = App.ClickCounter()
중복 코드는 옳지 않다.
우선 describe 실행에 순서가 있다.
describe (()=> {
beforeEach(()=>{ 1번 실행
afterEach(()=>{ 3번 실행
it(()=>{ 2번 실행
it실행 전에 무조건 beforeEach가 실행 되기 때문에 중복코든는 beforeEach로 이동시키자.
describe('App.ClickCounter', ()=> {
let count //전역변수
beforeEach(()=>{
counter = App.ClickCounter() //중복된 부분을 beforeEach 부분으로 추출했다.
})
describe('getValue()', ()=> {
it('초기값이 0인 카운터 값을 반환한다', ()=> {
expect(counter.getValue()).toBe(0)
})
})
describe('increase()', ()=> {
it('카운터를 1 올린다', ()=> {
counter.increase();
expect(counter.getValue()).toBe(1);
})
})
})
중복코드 수정 후 정상적으로 테스트 코드가 통과한다.
언제나 DRY 한 코드를 작성하기 위해 노력해야한다.
DRY는
DO IT REPEAT YOURSELF
클릭카운트 뷰 모듈 만들기
화면에 보이는 부분을 만들어 보자!
dom에 반영하여 화면에 보이게 만드는 영역을 만들어보자!
첫번째 스펙
ClickCounterView 모듈의 updateView()는 카운터 값을 출력한다.
생각해봐야 할 점
데이터를 조회할 때마다 ClickCounter를 얻어와야 함
데이터를 출력할 돔 엘리먼트를 테스트 해야함
주입하자!
ClickCounter는 객체를 만들어 파라미터로 전달받자.
데이터를 출력할 돔 엘리먼트도 만들어 전달 받자.
테스트 코드를 작성하자.
describe('App.ClickCountView', ()=> {
let clickCounter,updateEl,view
beforeEach(()=>{
clickCounter = App.ClickCounter();
updateEl = document.createElement('span')
view = App.ClickCounterView(clickCounter,updateEl) // ClickCounter와 updateEl 을 view 에 주입시킴
})
describe('updateView()', ()=> {
it('ClickCounter의 getValue() 값을 출력한다', ()=> {
const counterValue = clickCounter.getValue();
view.updateView();
expect(updateEl.innerHTML).toBe(counterValue.toString())
})
})
})
당연히 오류난다. 왜냐? 모듈을 안만들었으니까
연습을 위해 3단계를 계속 반복하고 있음
모듈을 만들어보자!
var App = App || {}
App.ClickCounterView = (clickCounter,updateEl)=>{
return{
updateView() {
updateEl.innerHTML = clickCounter.getValue();
}
}
}
ClikcCounterVier모듈 의존이 정상적으로 작동하는지 확인하는 테스트 케이스를 만들어보자.
expect(기대값).toThrowError() 함수를 통해 만들수있다.
describe('App.ClickCountView', ()=> {
let clickCounter,updateEl,view
beforeEach(()=>{
clickCounter = App.ClickCounter();
updateEl = document.createElement('span')
view = App.ClickCounterView(clickCounter,updateEl)
})
//에러를 체크해주는 부분
it("클릭카운터가 널일 경우 에러를 발생한다." , ()=>{
const clickCounter = null;
const updateEl = document.createElement('span');
const actual = () => App.ClickCounterView(clickCounter,updateEl);
expect(actual).toThrowError();
})
describe('updateView()', ()=> {
it('ClickCounter의 getValue() 값을 출력한다', ()=> {
const counterValue = clickCounter.getValue();
view.updateView();
expect(updateEl.innerHTML).toBe(counterValue.toString())
})
})
})
var App = App || {}
App.ClickCounterView = (clickCounter,updateEl)=>{
if(!clickCounter) throw error("clickCounter is null") // clickCounter 가 null일 경우 throw error 를 발생시켜준다.
return{
updateView() {
updateEl.innerHTML = clickCounter.getValue();
}
}
}
두번째 스펙
ClickCountView 모듈의 increaseAndUpdateView();는 카운트 값을 증가하고 그 값을 출력한다.
간단히 보면 이미 만든 ClickCounter의 increase() 함수를 실행하고
updateView 함수를 실행하는 기능임
테스트 더블을 통해 테스트 할 수 있다
다음 5가지를 통칭하여 테스트 더블이라고 함
더미 : 인자를 채우기 위해 사용
스텁 : 더미를 개선하여 실제 동작하게끔 리턴값을 하드코딩함
스파이 : 스텁과 유사 내부적으로 기록을 남기는 추가기능
페이크 : 스텁에서 발전한 실제 코드 운영에서는 사용할수 없다.
목 : 더미 , 스텁, 스파이를 혼합한 형태
자스민에서는 테스트 더블을 스파이스라고 부른다.
spyOn(), createSpy() 함수를 사용 할 수 있다.
모듈 실행 시 spyOn(감시할객체 ,'인자') 를 넘겨줌
특정한 행동을 한뒤 bar()
감시한 객체가 실행되었는지 체크할 수 있음
expect(감시할객체.인자).toHaveBeenCalled();함수로 체크가능
describe('App.ClickCountView 모듈', () => {
let udpateEl, clickCounter, view
it('ClickCounter를 주입하지 않으면 에러를 던진다', ()=> {
const clickCounter = null
const updateEl = document.createElement('span')
const actual = () => App.ClickCountView(clickCounter, updateEl)
expect(actual).toThrowError(App.ClickCountView.messages.noClickCounter)
})
it('updateEl를 주입하지 않으면 에러를 던진다', ()=> {
const clickCounter = App.ClickCounter()
const updateEl = null
const actual = () => App.ClickCountView(clickCounter, updateEl)
expect(actual).toThrowError(App.ClickCountView.messages.noUpdateEl)
})
beforeEach(()=> {
updateEl = document.createElement('span')
clickCounter = App.ClickCounter();
view = App.ClickCountView(clickCounter, updateEl)
})
describe('updateView()', () => {
it('ClickCounter의 getValue() 실행결과를 출력한다', ()=> {
const counterValue = clickCounter.getValue()
view.updateView()
expect(updateEl.innerHTML).toBe(counterValue.toString())
})
})
// 테스트 영역
describe('increaseAndUpdateView()는', ()=> {
it('ClickCounter의 increase 를 실행한다', ()=> {
spyOn(clickCounter,'increase')
view.increaseAndUpdateView();
expect(clickCounter.increase).toHaveBeenCalled()
})
it('updateView를 실행한다', ()=> {
spyOn(view,'updateView')
view.increaseAndUpdateView();
expect(view.updateView).toHaveBeenCalled();
})
})
})
당연히 오류 난다 모듈을 추가해 보자!
var App = App || {}
App.ClickCountView = (clickCounter, updateEl) => {
if (!clickCounter) throw new Error(App.ClickCountView.messages.noClickCounter)
if (!updateEl) throw new Error(App.ClickCountView.messages.noUpdateEl)
return {
updateView() {
updateEl.innerHTML = clickCounter.getValue()
},
//추가 영역
increaseAndUpdateView(){
clickCounter.increase();
this.updateView();
}
}
}
App.ClickCountView.messages = {
noClickCounter: 'clickCount를 주입해야 합니다',
noUpdateEl: 'updateEl를 주입해야 합니다'
}
정상이다.!!!!
세번째 스펙을 만들어보자
클릭 이벤트가 발생하면 increaseAndUpdateView()가 발생한다.
생각해보자
클릭 이벤트를 어떻게 체크할 것인가???
이 역시 주입해서 체크 가능하다.
클릭 이벤트 핸들러를 바인딩할 트리거를 추가한다.
triggerEl 이라는 전역변수를 생성하고 button 을 할당해주었다.
ClickCountView 에 파라미터가 너무 많아 { } 로 묶어서 전달
describe('App.ClickCountView 모듈', () => {
let udpateEl, triggerEl, clickCounter, view
beforeEach(()=> {
updateEl = document.createElement('span')
triggerEl = document.createElement('button')
clickCounter = App.ClickCounter();
view = App.ClickCountView(clickCounter, {updateEl ,triggerEl})
})
describe('네거티브 테스트', ()=> {
it('ClickCounter를 주입하지 않으면 에러를 던진다', ()=> {
const actual = () => App.ClickCountView(null, {updateEl})
expect(actual).toThrowError(App.ClickCountView.messages.noClickCounter)
})
it('updateEl를 주입하지 않으면 에러를 던진다', ()=> {
const actual = () => App.ClickCountView(clickCounter, {null})
expect(actual).toThrowError(App.ClickCountView.messages.noUpdateEl)
})
})
describe('updateView()', () => {
it('ClickCounter의 getValue() 실행결과를 출력한다', ()=> {
const counterValue = clickCounter.getValue()
view.updateView()
expect(updateEl.innerHTML).toBe(counterValue.toString())
})
})
describe('increaseAndUpdateView()는', ()=> {
it('ClickCounter의 increase 를 실행한다', ()=> {
spyOn(clickCounter, 'increase')
view.increaseAndUpdateView()
expect(clickCounter.increase).toHaveBeenCalled()
})
it('updateView를 실행한다', ()=> {
spyOn(view, 'updateView')
view.increaseAndUpdateView()
expect(view.updateView).toHaveBeenCalled()
})
})
it('클릭 이벤트가 발생하면 increseAndUpdateView를 실행한다', ()=> {
// todo
spyOn(view,'increseAndUpdateView')
//클릭
triggerEl.click();
expect(view.increaseAndUpdateView).toHaveBeenCalled();
})
})
increase 버튼을 클릭하면 정상적으로 숫자가 증가한다.