자바스크립트에서 

'1' + 1 = ?
'2' * 3 = ?
'1' + 2 + 3 * 4 = ?



'1' + 1 = 11
'2' * 3 = 6
'1' + 2 + 3 * 4 = 1212


자바스크립트는 컴파일러가 없으니 테스트가 최선이다.
직접실행해서 결과를 검증하자.

자바스크립트 TDD 를 해보자!


테스트의 개념
  1. 단위(유닛) 테스트
                인풋에 따라 계산한 결과를 아웃풋으로 내놓는 것.  ( 단위테스트는 함수를 테스트 하는 것이라고 생각하면됨)
                단위테스트 , 준비 , 실행  , 단언(검증) 단계로 나뉘어집니다.



테스트 주도개발

레드, 그린 , 리펙터 순환 구조

테스트하기 쉬운 코드

관심사의 분리

함수는 하나의 기능만 해야함



자바스크립트의 테스트 프레임워크  - 재스민 프레임워크 


재스민 프레임워크 다운 후 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가지의 역할을 하고 있음 


  1. 전역 변수의 충돌 
var counter 전역변수를 사용하여 어지럽힌 점  전역변수 사용은 전형적인 안티 패턴이기 때문에 사용하더라도 제한적으로 사용 해야함

  1. 재사용 어려움
var el = document.getElementById('count-display')


함수에 count-display를 하드코딩 했기 때문에 다른 선택자를 찾으려면 해당 함수를 사용할수없고 새로운 함수를 만들어야 하기 때문에 재사용이 어려움
open-close 원칙에 의거 쉽게 추가하면안되고 확장에는 언제든 열려있어야 한다. 



자바스크립트에서 문제를 해결 하는 방식에는 모듈 패턴이라는 것이 있음
모듈 패턴이란 ? 함수로 데이터를 감추고 모듈 API를 담고 있는 객체를 반환하는 형태
모듈 패턴에는 1. 임의 모듈 패턴 , 2. 즉시 실행 함수 기반의 모듈 패턴이 있음

모듈생성원칙
  1. 한가지 모듈에는 한가지 역할만 한다.
  2. 모듈에 사용할 객체가 있다면 의존성 주입형태로 제공해야한다.


임의모듈패턴으로 기존코드 수정하기
  기존의 클릭카운터 소스를 수정해보자.

A 우선 getValue()는 카운트 값을 반환하도록 수정해보자.

  1. 실패하는 테스트 코드 작성하기


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을 호출하는 모듈을 생성 후 재스민으로 테스트 하면 오류가 나지 않는다.


  1. 모듈을 수정하자 카운터가 증가해야하나 현재는 상수로 박아놨기 때문에 그부분을 변수로 처리 후 수정해보자. 리펙토링 단계
var App = App || {}

App.ClickCounter = () =>{
let value = 0;
return {
getValue() {
return value
}
}
}


정상이다.
이 것이 테스트 주도 개발의 한 사이클이라고 보면될것 같다.

레드(오류) --> 그린(패스) --> 블루(리펙터)  반복


두번째 스펙
B. ClickCounter 의 increase() 는  카운터 값을 1씩 증가시킨다.

  1. 테스트 코드를 작성한다  (레드)
describe('increase()', ()=> {
it('카운터를 1 올린다', ()=> {
var counter = App.ClickCounter()
counter.increase();

expect(counter.getValue()).toBe(1);
})
})
})


실행하면 당연히 오류다   왜냐하면 increase() 모듈을 작성한적이 없쟈나쟈나

  1. 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()는 카운터 값을 출력한다.

생각해봐야 할 점 
  1. 데이터를 조회할 때마다 ClickCounter를 얻어와야 함 
  2. 데이터를 출력할 돔 엘리먼트를 테스트 해야함

주입하자!
ClickCounter는 객체를 만들어 파라미터로 전달받자.
데이터를 출력할 돔 엘리먼트도 만들어 전달 받자. 

  1.  테스트 코드를 작성하자.
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단계를 계속 반복하고 있음

  1. 모듈을 만들어보자!
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가지를 통칭하여 테스트 더블이라고 함 
  1. 더미 : 인자를 채우기 위해 사용
  2. 스텁 : 더미를 개선하여 실제 동작하게끔 리턴값을 하드코딩함
  3. 스파이 : 스텁과 유사 내부적으로 기록을 남기는 추가기능
  4. 페이크 : 스텁에서 발전한 실제 코드 운영에서는 사용할수 없다. 
  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();
})
})


모듈 수정

var App = App || {}

//App.ClickCountView = (clickCounter, updateEl) => {
//이 부분을 {updateEl, TriggerEl} -> options로 처리

App.ClickCountView = (clickCounter, options) => {
if (!clickCounter) throw new Error(App.ClickCountView.messages.noClickCounter)
if (!options,updateEl) throw new Error(App.ClickCountView.messages.noUpdateEl)

//return으로 처리하던 부분으 view 변수에 담은 후 view 를 리턴하는형태로 변경

const view = {
updateView() {
options.updateEl.innerHTML = clickCounter.getValue()
},

increaseAndUpdateView() {
clickCounter.increase()
this.updateView()
},
}

options.triggerEl.addEventListener('click',()=>{
view.increaseAndUpdateView();
})


return view
}

App.ClickCountView.messages = {
noClickCounter: 'clickCount를 주입해야 합니다',
noUpdateEl: 'updateEl를 주입해야 합니다'
}




모듈을 화면에 붙여보자!!!
<html>
<body>
//화면에 영역을 생성 한다.
<span id="counter-display"></span>
<button id="btn-increase">Increase</button>

//생성한 모듈을 호출한다.
<script src="ClickCounter.js"></script>
<script src="ClickCountView.js"></script>

<script>
(() => {
const clickCount = App.ClickCounter();
const updateEl = document.querySelector('#counter-display')
const triggerEl = document.querySelector('#btn-increase')

const view = App.ClickCountView(clickCount,{updateEl,triggerEl});
view.updateView();

})()
</script>
</body>
</html



실행화면

increase 버튼을 클릭하면 정상적으로 숫자가 증가한다.


















+ Recent posts