본문 바로가기

개발/JavaScript

[Vanilla Javascript] Jasmine을 이용해 테스트 코드 작성해보기 -2

카운터 데이터는 돔(DOM)에 반영되어야 한다.
이 역할을 하는 ClickCountView 모듈을 만들자.
데이터를 출력하고 이벤트 핸들러를 바인딩하는 일을 담당할 것이다.

 

첫번째 스펙
"ClickCountView 모듈의 updateView()는 카운트 값을 출력한다"

 

그런데.. 

데이터를 조회할 ClickCounter를 어떻게 얻지?

게다가 데이터를 출력할 돔 엘레먼트는 어떻게 테스트하지?

 

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

TDD 방식으로 사고하다 보면
이런식으로 필요한 모듈을 주입받아 사용하는 경향이 생긴다.
하나의 기능 단위로 모듈을 분리할 수 있기 때문에 단일 책임 원칙을 지킬 수 있다.

 

Red

describe('App.ClickCountView', () => {
    let clickCounter, updateEl, view;

    beforeEach(() => {
        clickCounter = App.ClickCounter();
        updateEl = document.createElement('span');
        view = App.ClickCountView(clickCounter, updateEl);
    });

    describe('updateView()', () => {
        it('ClickCounter의 getValue(0 값을 출력한다.', () => {
            const counterValue = clickCounter.getValue;
            view.updateView();
            expect(updateEl.innerHTML).toBe(counterValue.toString());
        });
    });
});

ClickCountView(clickCounter, updateEl) 객체를 파라미터로 전달

 

Green

var App = App || {}

App.ClickCountView = (clickCounter, updateEl) => {

    return {
        updateView() {
            updateEl.innerHTML = clickCounter.getValue();
        }
    }
}

 

ClickCountView에 의존성 주입이 되었는지 체크

 

Red

it('clickCounter를 주입하지 않으면 에러를 던진다', () => {
    const clickCounter = null;
    const updateEl = document.createElement('span');
    const actual = () => App.ClickCountView(clickCounter, updateEl);
    expect(actual).toThrowError();
})

it('updateEl 주입하지 않으면 에러를 던진다', () => {
    clickCounter = App.ClickCounter();
    updateEl = null;    
    const actual = () => App.ClickCountView(clickCounter, updateEl);
    expect(actual).toThrowError();
});

Green

if (!clickCounter) throw Error('clickCounter');
if (!updateEl) throw Error('updateEl');

 

두번째 스펙
"ClickCountView 모듈의 increaseAndUpdateView()는 카운트 값을 증가하고 그 값을 출력한다."
두개의 기능이기 때문에(카운트 값 증가, 그 값을 출력) 분리해서 테스트 하기

 

 

테스트 더블

단위 테스트 패턴으로, 테스트하기 곤란한 컴포넌트를 대체하여 테스트 하는 것.
특정한 동작을 흉내만 낼 뿐이지만 테스트 하기에는 적합하다.
종류 : 더미, 스텁, 스파이, 페이크, 목
자스민에서는 테스트 더블을 스파이스(spies)라고 부른다
       

spyOn(MyApp, 'foo') // MyApp에 foo라는 함수를 감시하겠다        

expect(Myapp.foo).toHaveBeemCalled() // foo 함수가 실행되었는지 체크한다

 

Red

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();
    });
});

Green

var App = App || {}

App.ClickCountView = (clickCounter, updateEl) => {
    if (!clickCounter) throw Error('clickCounter');
    if (!updateEl) throw Error('updateEl');
    

    return {
        updateView() {
            updateEl.innerHTML = clickCounter.getValue();
        },
        increaseAndUpdateView() {
            clickCounter.increase(); 
            this.updateView();
        }
    }
}

 

세번째 스펙
"클릭 이벤트가 발생하면 increaseAndUpdateView()를 실행한다"

 

Red

it("클릭 이벤트가 발생하면 increaseAndUpdateView를 실행한다", () => {
    spyOn(view, 'increaseAndUpdateView');
    triggerEl.click();
    expect(view.increaseAndUpdateView).toHaveBeenCalled();
});

triggerEl 생성

 

Green

var App = App || {}

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

    const view = {
        updateView() {
            options.updateEl.innerHTML = clickCounter.getValue();
        },
        
        increaseAndUpdateView() {
            clickCounter.increase(); 
            this.updateView();
        }
    }

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

    return view;
}

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

화면에 붙여보기

index.html

<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 clickCounter = App.ClickCounter();
                const updateEl = document.querySelector("#counter-display");
                const triggerEl = document.querySelector("#btn-increase");
                const view = App.ClickCountView(clickCounter, {updateEl, triggerEl});
                view.updateView();
            })()
        </script>
    </body>
</html

Reference

https://www.inflearn.com/course/tdd-%EA%B2%AC%EA%B3%A0%ED%95%9C-%EC%86%8C%ED%94%84%ED%8A%B8%EC%9B%A8%EC%96%B4-%EB%A7%8C%EB%93%A4%EA%B8%B0

 

견고한 JS 소프트웨어 만들기 - 인프런 | 강의

같은 기능을 만들더라도 자바스크립트 문법을 이제 막 뗀 주니어 개발자와 경험 많은 시니어 개발자의 코드는 상당히 다릅니다. 물론 결과물은 같더라도 말이죠. 후자의 코드가 인정받는 이유

www.inflearn.com