Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

JavaScript BDD 프레임워크 Jasmine 튜토리얼

자바스크립트에서의 유닛테스트는 여러번 시도하기는 했었는데 잘 되지 않았습니다. 기본적으로 UI랑 의존성이 크기 때문에 테스트하기 어려웠기 때문인데 node.js를 하면서 서버사이드이기 때문에 다시 유닛테스트를 하고 있습니다. 그동안은 expresso라는 TDD프레임워크를 썼었는데 몇가지 맘에 안드는 부분이 있어서 다른 프레임워크를 찾다가 점점 인기도 얻고 있는 Jasmine이 마침 현재 스터디하는 곳에서도 관심을 가지고 있고 해서 겸사겸사 Jasmine을 만지기 시작했습니다.

Jasmine은 자바스크립트를 위한 BDD(Behavior Driven Development) 프레임웍입니다. BDD는 저도 잘 모르기에 BDD에 대해서 설명하는 것은 이글의 범주는 넘어서지만 간단히 얘기하면 TDD와는 약간 다른데(전에는 말장난이 아닌가 싶은 생각도 했지만.) TDD가 Inside-out의 접근이라면 BDD는 Outside-in의 접근을 가지고 있다고 할수 있습니다.

node.js에서 쓸 수 있는 jasmine-node가 있지만 써보려고 하다보니 Jasmine 그 자체를 좀 알아야 할것 같아서 여러가지 자료를 찾아봤습니다. Jasmine은 문서도 잘 되어있고 API문서도 정리가 잘 되어 있는데 좀 배워보려고 찾아보다가 괜은 튜토리얼을 찾았습니다. 아래 튜토리얼은 Evan Hahn이 만든 튜토리얼이고 CCL에 따라 가져왔습니다.(직역으로 번역한건 아니고 적당한 선에서 정리했습니다.)





어떻게 Jasmine을 하는가: 튜토리얼

자스민은 자바스크립트를 위한 유닛테스트 프레임웍이고 아주 매력적이지만 좋은 튜토리얼을 찾을 수 없었기 때문에 배운다음에 직접 튜토리얼을 만들었습니다.

이 튜토리얼은 고급 자바스크립트(콜백, 객체지향 프로그래밍)에 익숙한 사람들과 유닛테스트를 하기 원하는 사람들을 위해서 만들었습니다.



자스민은 무엇인가?

보통 프로그램은 함수와 클래스들을 많이 가지고 있을 것입니다. 이런 것들이 어떻게 수행되는지를 확인하고 싶을 것인데 예를 들어 어떤 함수가 항상 "hello" 문자열을 리턴한다고 할 때 유닛테스트는 계획대로 정확하게 동작하는 지를 확인해 줍니다.

만약 이전에 자바스크립트보다 다른 언어에 익숙하다면 자바스크립트를 쓰면서 약간의 어려움이 있을 것이고 다른 유닛테스트 프레임웍에 익숙하다면 자스민을 사용하면서 비슷한 어려움을 갖게 될지도 모릅니다.



자스민 얻기

Standalone 버전을 다운받은 후 압축을 풀어주고 /spec/src 디렉토리안에 있는 파일들을 지워줍니다. 이 디렉토리에는 예제파일들이 들어있지만 이 튜토리얼에서는 필요없습니다.(이 튜토리얼에서는 1.0.2를 사용했습니다.)



초심자를 위한 예제 : 헬로 월드

무엇을 테스트 하기 원하는가?

아래의 함수를 가지고 있는 프로그램을 만든다고 해보겠습니다. 이 함수는 "Hello world!"를 리턴해 줍니다.


function helloWorld() {
    return "Hello world!"; 
}

스펙(Spec)

물론 위의 함수는 아주 간단하기 때문에 테스트하지 않아도 동작여부를 확인할 수 있지만 자스민을 이용해서 이것을 확인해 보려고 합니다. 우선 /src 디렉토리에 위의 소스를 HelloWorld.js라는 파일로 저장합니다.

아래 소스는 스펙인데 소스를 먼저 본 뒤에 설명할 것입니다.


describe("Hello world", function() {
    it("says hello", function() {
        expect(helloWorld()).toEqual("Hello world!");
    });
});

아주 간단합니다. describe('Hello world')를 Suite이라고 부릅니다. 일반적으로 이것은 애플리케이션의 컴포넌트이고 클래스이거나 단순히 여러가지 함수가 될 수 있습니다. 이 Suite은 "Hello world"라고 부르고 이 이름은 코드가 아닌 그냥 영어입니다.

Suite안에서(기술적으로는 클로저의 내부입니다.) it()함수를 가지고 있는데 이것을 Spec이라고 부릅니다. 당신의 프로그램의 일부가 무엇을 해야하는지를 말해주는 자바스크립트 함수입니다. "says hello"도 코드가 아닌 그냥 영어이고 Suite안에는 많은 수의 Spec을 가질 수 있습니다.

위의 예제에서는 helloWorld() 함수가 "Hello world!"와 같은지를 테스트하는데 여기서 이 확인하는 부분을 Matcher라고 부르고 자스민에는 내장된 많은 수의 Matcher가 있고 필요하다면 자신만의 Matcher를 만들 수 있습니다. 위 소스를 HelloWorldSpec.js라는 파일로 /spec 디렉토리안에 저장합니다.

스펙 러너(Spec Runner)

Jasmine폴더 루트에 있는 SpecRunner.html을 아래와 같이 수정합니다. 이 파일은 단순히 테스트를 수행하는 역할을 하고 있으며 자스민의 코드와 소스파일, 스펙파일을 가지고 있습니다. 그리고 body부분에서 테스트를 하는 부분이 담겨있습니다.


<html>
    <head>
        <title>Jasmine Test Runner</title>
        <!-- Jasmine includes -->
        <link rel="stylesheet" type="text/css" href="lib/jasmine-1.0.2/jasmine.css">
        <script type="text/javascript" src="lib/jasmine-1.0.2/jasmine.js"></script>
        <script type="text/javascript" src="lib/jasmine-1.0.2/jasmine-html.js"></script>

        <!-- Source files -->
        <script type="text/javascript" src="src/HelloWorld.js"></script>

        <!-- Spec files -->
        <script type="text/javascript" src="spec/HelloWorldSpec.js"></script>
    </head>

     <body>
        <script type="text/javascript">
            jasmine.getEnv().addReporter(new jasmine.TrivialReporter());
            jasmine.getEnv().execute();
        </script>
    </body>
</html>

위 파일을 실행하면 테스트가 성공했다는 결과를 볼 수 있을 것입니다. helloWorld()함수에서 다른 문자열을 리턴하도록 수정하고 다시 실행해보면 테스트가 실패하는 것을 볼 수 있습니다.



더 많은 매처들

앞의 예제에서 helloworld()함수가 "Hello world!"와 같다는 것을 확인하는데 toEqual()함수를 사용했습니다. 하지만 "world"라는 단어가 포함되어 있는지만 확인하기를 원한다면 그냥 다른 매처를 사용하면 됩니다.


describe("Hello world", function() {
    it("says world", function() {
        expect(helloWorld()).toContain("world");
    });
});

정확하게 "Hello world!"를 비교하는 대신에 단순히 "world"를 담고 있는지를 확인하는데 그냥 영어문장을 읽는 것처럼 쉽습니다.

자스민에는 많은 내장 매처들이 있습니다. toBeNull()은 변수가 null이기를 기대하고 toBeTruthy()는 어떤 값이 true이기를 기대합니다. expect(x).not.toEqual(y)는 동일하지 않다는 것을 확인해주는데 .not 오퍼레이터를 사용했습니다. toEqual()은 동등성을 확인하는 반면에 toBe()는 정확히 같은 객체인지를 확인해 줍니다.



자신만의 매처를 만들어 보자

자스민이 가지고 있는 내장 매처들은 충분히 좋지만 보통 자신만의 매처를 만들기 원할 것입니다.beforeEach()함수는 이것이 가능하도록 만들어 줍니다. 아래 Suite를 보겠습니다.


describe('Hello world', function() {
    beforeEach(function() {
        this.addMatchers({
            toBeDivisibleByTwo: function() {
                return (this.actual % 2) === 0;
            }
        });
    });

    it('is divisible by 2', function() {
        expect(gimmeANumber()).toBeDivisibleByTwo();
    });
});

toBeDivisibleByTwo라는 매처를 정의한 것을 볼 수 있는데 이 매처는 2로 나눌 수 있는지 여부를 리턴 해 줍니다. 각 스펙이 실행되기 바로 전에 정의되기를 원하기 때문에 beforeEach()안에서 정의하였고 내장매처를 사용하는 것과 동일하게 사용하였습니다. 매처 내부에서 객체의 인스턴스가 아닌 객체의 컨텐츠를 참조하기 위해서 this를 사용하는 대신에 this.actual이라고 사용한 것을 주의해야 합니다.



before와 after

앞에서 사용한 beforeEach()를 볼 것이고 afterEach()도 있습니다.

모든 스펙이 실행되기 이전에 변수를 설정하거나 함수를 정의하는 등 어떤 동작이 필요하다면 beforeEach()안에 이러한 코드를 둬서 모든 스펙 이전에 실행되게 할 수 있습니다. 모든 스펙이후에 무언가 실행되기를 원한다면 afterEach()안에 코드를 작성하면 됩니다.

더 자세히 알고 싶다면 자스민 문서에 있는 예제들을 참고하면 됩니다.



스파이(Spy)

함수가 호출되었는지 여부나 어떻게 호출하기를 원하는지를 확인하고 싶다고 해보겠습니다. 이 때 spy를 사용할 수 있습니다. Spy는 테스트하는 프로그램의 일부분을 스파이해줍니다.

예제 : spyOn()으로 존재하지 않는 함수를 수정하지 않고 스파이해보자

Person이라는 클래스가 있고 Person은 hello라고 말하거나 누군가에게 hello라고 인사를 할 수 있다고 정의해 보겠습니다.


var Person = function() {
};

Person.prototype.helloSomeone = function(toGreet) {
    return this.sayHello() + " " + toGreet;
};

Person.prototype.sayHello = function() {
    return "Hello";
};

아주 간단한 2개의 메서드가 있습니다. 여기서 helloSomeone()함수를 호출했을 때 sayHello()함수를 호출되는 지를 확인하기를 원한다고 하겠습니다. 이제 Suite는 아래와 같습니다.


describe("Person", function() {
    it("calls the sayHello() function", function() {
        var fakePerson = new Person();
        spyOn(fakePerson, "sayHello");
        fakePerson.helloSomeone("world");
        expect(fakePerson.sayHello).toHaveBeenCalled();
    });
});

위의 Suite를 설명하겠습니다. 테스트를 하기 위해서 fakePerson을 만듭니다. 이 fakePerson의 sayHello()함수를 스파이하도록 지정하고 sayHello를 호출하는 helloSomeone()를 실행합니다. 그 다음에 sayHello()가 기대대로 호출되었는지를 확인합니다. 이제 스펙러너를 실행하면 통과하는 것을 볼수 있고 fakePerson.sayHello()이 호출되었다는 것을 알 수 있습니다.

보통은 이런 상황보다 더 다양한 경우가 필요할 것입니다. 여기서는 helloSomeone이 아규먼트로 "world"와 함께 호출되었다는 것을 확인하기를 원한다고 해보겠습니다. 다음 Suite를 보겠습니다.


describe("Person", function() {
    it("greets the world", function() {
        var fakePerson = new Person();
        spyOn(fakePerson, "helloSomeone");
        fakePerson.helloSomeone("world");
        expect(fakePerson.helloSomeone).toHaveBeenCalledWith("world");
    });
});

마치 영어를 읽는 것처럼 쉽게 읽을 수 있습니다. 이 스파이는 helloSomeone()의 아규먼트가 "world"인지를 확인해 줍니다. 스펙러너를 실행해보면 기대대로 동작한 다는 것을 알 수 있습니다.

함수가 호출되지 않았다는 것을 확인하고자 한다면 앞에서 변수가 동일하지 않다는 것을 확인했을 때와 비슷하게 .not을 사용하면 됩니다. 아래 소스는 함수가 특정 아규먼트와 함께 호출되지 않았다는 것을 확인합니다.

expect(fakePerson.helloSomeone).not.toHaveBeenCalledWith("foo");

자스민 문서에서 다른 spy 아규먼트들을 볼 수 있습니다.

예제: 이미 존재하는 함수를 수정해서 스파이 해보자 : jasmine.createSpy()의 사용

앞의 예제와 동일한 Person이 있습니다.


var Person = function() {
};

Person.prototype.helloSomeone = function(toGreet) {
    return this.sayHello() + " " + toGreet;
};

Person.prototype.sayHello = function() {
    return "Hello";
};

일반적으로는 함수가 호출되었는지를 확인하는 것이 전부입니다.(이것은 다른 곳에서 이미 테스트를 했기 때문이거나 아니면 당신이 게으르기 때문입니다.) 자스민에서는 테스트하는 동안 함수의 내용을 비어있게 만들 수 있습니다. 아래 예제를 보겠습니다.


describe("Person", function() {
    it("says hello", function() {
        var fakePerson = new Person();
        fakePerson.sayHello = jasmine.createSpy("Say-hello spy");
        fakePerson.helloSomeone("world");
        expect(fakePerson.sayHello).toHaveBeenCalled();
    });
});

이전 예제와 동일하게 fakePerson을 만들었습니다. 하지만 이 예제에서는 sayHello()를 더미함수로 교체하고 스파이를 생성했습니다. 그 뒤 helloSomeone()로 더미함수 sayHello()가 호출되었는지를 확인합니다.

더미함수가 비어있지 않고 어떤 값을 리턴해주기를 원한다면 다음과 같이 만들어 줄 수 있습니다.

fakePerson.sayHello = jasmine.createSpy('"Say hello" spy').andReturn("ello ello");

여기서 만족하지 못하고 spy 더미함수가 어떤 것을 수행하기를 원한다면 아래와 같이 정의할 수 있습니다.


fakePerson.sayHello = jasmine.createSpy('"Say hello" spy').andCallFake(function() {
    document.write("Time to say hello!");
    return "bonjour";
});

자스민 문서에는 예외를 던지는 등에 대해서 더 설명해주고 있습니다. 관심이 있으면 문서를 참고하면 됩니다.



비동기 처리하기

자스민 문서를 보면 "'모든 것이 괜찮지만 비동기 테스트는 어떻게 하는가?'라고 생각했을 것이다"라고 나와있지만 당신이 그런 생각을 안했다는데 돈을 걸수도 있습니다. 이제 자스민의 비동기 지원에 대해서 설명하겠습니다.

비동기에 대해도 도와주는 3개의 자스민 함수가 있습니다 : run(), waitsFor(), wait()
마지막 함수는 보통 쓸모 없다고 생각하는데 왜 그렇게 생각하는지는 뒤에서 다시 설명하겠습니다.

run()블럭은 순차적으로 실행하기 때문에 비동기코드로 인해서 엉망이 되는 것에 대해서 걱정하지 않아도 됩니다.  다음 예제를 보겠습니다.


it("is a test of run()", function() {
    runs(function() {
        var foo = 1;
        expect(foo).toEqual(1);
    });

    runs(function() {
        var bar = 2;
        bar++;
        expect(bar).toEqual(3);
    });
});

비동기적인 것이 없다면 run()함수는 쓸모가 없습니다. 모든 것이 순차적이라면 run()블럭은 단순 나열일 뿐입니다.

그럼 이제 좀더 흥미로운 waitsFor()을 보겠습니다.

2개의 숫자사이에서 최대공약수를 찾는 계산 클래스를 테스트하려한다고 해보겠습니다. 여기서 숫자가 무척이나 큰 숫자이기 때문에 비동기적으로 수행하려고 합니다. 다음 예제는 동작하지 않습니다.


describe("Calculator", function() {
    it("should factor two huge numbers asynchronously", function() {
        var calc = new Calculator();
        var answer = calc.factor(18973547201226, 28460320801839);
        expect(answer).toEqual(9486773600613); // factor()가 비동기라면 동작하지 않습니다.
    });
});

이것을 동작하게 하려면 waitsFor()을 사용하면 됩니다. waitsFor()는 어떤 상황이 true가 되기를 기다리다가 true가 되면 그 다음을 계속 수행하게 됩니다. 옵션사항으로 타임아웃을 명시할 수 도 있어서 타임아웃 이상을 기다리게 되면 부가 메시지와 함께 실패하게 됩니다.


describe("Calculator", function() {
    it("should factor two huge numbers asynchronously", function() {
        var calc = new Calculator();
        var answer = calc.factor(18973547201226, 28460320801839);

        waitsFor(function() {
            return calc.answerHasBeenCalculated();
        }, "It took too long to find those factors.", 10000);

        runs(function() {
            expect(answer).toEqual(9486773600613);
        });
    });
});

위 코드는 calc.answerHasBeenCalculated()에서 true가 리턴될 때까지 기다리다가 true가 리턴되면 그 다음 블럭을 수행하고 10초후에도 리턴되지 않는다면 오류가 발생시킵니다.

마지막 함수인 wait()가 있는데 이 함수는 별로 유용하지 않다고 생각합니다. 왜냐하면 전통적인 비동기 자바스크립트에서는 시간의 양을 지정하지 않기 때문에 어떤 밀리초에서 완료되는 함수대신에 waitsFor()을 사용할 것입니다. 하지만 wait()가 필요하다면 문서를 참고하시면 됩니다.
2011/07/29 03:05 2011/07/29 03:05