도구 다루기-2

재스민 들어가기

재스민은 행위 주도 개발 (Behavior-Driven Development, BDD) 방식으로 자바스크립트 단위 테스트를 작성하기 위한 라이브러리다.

BDD와 TDD는 상호 배타 관계가 아니다. BDD는 단위 테스트로 확인할 기능 또는 작동 로직을 일상 언어로 서술하는데, 이로써 개발자는 자신이 작성 중인 코드가 어떻게가 아니라 무엇을 해야하는지 테스트 코드에 표현할 수 있다. 그리고 행위 주도 스타일로 정의/구성한 테스트는 쉬운 문장으로 서술한 기능 명세서로 삼을 만하다는 이점도 있다.

테스트 꾸러미와 스펙

재스민 테스트 꾸러미는 전역 함수 describe로 정의되며, 이 함수는 두 인자를 받는다.

  • 문자열: 무엇을 테스트할지 서술한다.
  • 함수: 테스트 꾸러미의 구현부다.

테스트 꾸러미는 스펙, 즉 개별 테스트로 구현되며, 각 스펙은 전역 함수 it으로 정의된다. it 함수도 describe 처럼 인자를 2개 받는다.

  • 문자열: 무엇을 테스트할지 서술한다.
  • 적어도 한 개의 기대식을 가진 함수: 코드 상태의 true/false를 확인하는 단언

테스트 꾸러미 구현부에 전역 함수 beforeEach/afterEach 를 쓰면 각 꾸러미 테스트가 실행되기 이전에 beforeEach 함수를, 그 이후에는 afterEach 함수를 호출한다. 전체 테스트가 공유할 설정과 정리 코드를 두 함수에 담아두면 코드 중복을 피할 수 있어 좋다.

설정 단계가 정확히 같은 테스트가 2개 있는데, beforeEach 함수로 간단히 해결할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
describe('createReservation(passenger, flight)', function () {
it('주어진 passenger를 passengerInfo 프로퍼티에 할당한다.', function () {
var testPassenger = null,
testFlight = null,
testReservation = null;

beforeEach(function () {
testPassenger = {
firstName: "윤지",
lastName: "김"
};

testFlight = {
number: "3443",
carrier: "대한항공",
destination: "울산"
};

testReservation = createReservation(testPassenger, testFlight);
});

it("passenger를 passenger Information 프로퍼티에 할당한다.", function () {
expect(testReservation.passengerInformation).toBe(testPassenger);
});

it("flight를 flightInformation 프로퍼티에 할당한다.", function () {
expect(testReservation.flightInformation).toBe(testFlight);
})
})
});

기대식과 매처

expect 문은 테스트마다 있다. 다음은 첫 번째 단위 테스트 createReservation의 expect 문이다.

expect(testReservation.passengerInformation).toBe(testPassenger);

expect 함수는 테스트 대상 코드가 낸 실제값을 인자로 받아 기댓값과 견주어본다. 이 테스트가 기대하는 값은 testPassenger다.

실제값과 기댓값을 비교하는 일은 매처(matcher) 함수의 몫이다. 매처는 비교 결과 성공하면 true, 실패하면 false를 반환한다. 하나 이상의 기대식이 포함된 스펙에서 매처가 하나라고 실패하면 모조리 실패한 것으로 간주한다.

toBe 매처는 이름에서 짐작할 수 있듯이 testResevation.passengerInformation이 testPassenger 와 같은 객체여야 한다는 의미다.

스파이

재스민 스파이(spy)는 테스트 더블(test double) 역할을 하는 자바스크립트 함수다. 테스트 더블은 어떤 함수/객체의 본래 구현부를 테스트 도중 다른 코드로 대체한 것을 말하며, 웹 서비스 같은 외부 자원과의 의존 관계를 없애고 단위 테스트의 복잡도를 낮출 목적으로 사용된다.

다음 다섯 가지를 통칭하여 테스트 더블이라고 한다.

  1. 더미(dummy): 보통 인자 리스트를 채우기 위해 사용되며, 전달은 하지만 실제로 사용되지는 않는다.
  2. 틀(stub): 더미를 조금 더 구현하여 아직 개발되지 않은 클래스나 메서드가 실제 작동하는 것 처럼 보이게 만든 객체로 보통 리턴 값은 하드 코딩한다.
  3. 스파이(spy): 틀과 비슷하지만 내부적으로 기록을 남긴다는 점이 다르다. 특정 객체가 사용되었는지, 예상되는 메서드가 특정한 인자로 호출되었는지 등의 상황을 감시하고 이러한 정보를 제공하기도 한다.
  4. 모의체(fake): 틀에서 조금 더 발전하여 실제로 간단히 구현된 코드를 갖고는 있지만, 운영 환경에서 사용할 수는 없는 객체다.
  5. 모형(mock): 더미, 틀, 스파이를 혼합한 형태와 비슷하나 행위를 검증하는 용도로 주로 사용된다.

ReservationSaver 라는 자바스크립트 객체를 만들어 이 객체의 saveReservation 함수로 웹 서비스에 예약 데이터를 전송하는 기능을 캡슐화했다. createReservation 함수를 확장하여 이 함수가 ReservationSaver 인스턴스를 인자로 받아 이 인스턴스의 saveReservation 함수를 실행하는지 확인하고자 한다.
saveReservation 함수는 웹 서비스와 통신하므로 지금부터 작성할 테스트는 예약 데이터 저장 후 DB를 질의하고 예약 데이터가 분명히 추가됐는지 확인하는 과정이 모두 들어가야 할 듯 싶다. 하지만 그렇지 않다. 자칫 단위 테스트가 웹 서비스, DB 같은 외부 시스템 유무와 작동 여부에 의존하게 될지도 모른다.

재스민 스파이를 사용하면 복잡한 saveReservation 구현부를 외부 시스템 의존성을 배제한, 단순한 형태로 바꿀 수 있다. 먼저 작성한 ReservationSaver 객체다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function createReservation(passenger, flight, saver) {
var reservation = {
passengerInformation: passenger,
flightInformation: flight
};

saver.saveReservation(reservation);
return reservation;
}

function ReservationSaver() {
this.saveReservation = function (reservation) {
// 예약 정보를 저장하는 웹 서비스와 연동하는 복잡한 코드가 있을 것이다.
}
}

createReservation 함수는 ReservationSaver 인스턴스를 전달받게끔 개선되었다. ReservationSaver를 인자로 받으므로 예약 데이터가 저장되었는지를 확인하는 테스트를 다음과 같이 작성할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
describe("createReservation", function() {
var saver = new ReservationSaver();
var testPassenger = null,
testFlight = null;

beforeEach(function () {
testPassenger = {
firstName: "윤지",
lastName: "김"
};

testFlight = {
number: "3443",
carrier: "대한항공",
destination: "울산"
};
});

createReservation(testPassenger, testFlight, saver);
})

saver.saveReservation이 정말 호출 되었는지 어떻게 알 수 있을까?
이 테스트는 코드에 씌어있는 대로 복잡한 ReservationSaver의 기본 구현부를 createReservation 함수에 전달하고 있다. 이렇게 하면 결국 외부 시스템에 의존하게 되고 함수를 테스트하기가 어려워지므로 별로 내키지 않는다. 이럴때 재스민 스파이가 제격이다.

createReservation을 호출하기 전에 saveReservation 함수에 스파이를 심는다. 스파이로 함수 실행 여부를 알 수 있는데, 첫 번째 테스트에 아주 잘 들어맞는다.
재스민에서 전역 함수 spyOn을 쓰면 특정 함수를 몰래 들여다볼 수 있다. 이 함수의 첫 번째 인자는 객체 인스턴스, 두 번째 인자는 감시할 함수명이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
describe("createReservation", function () {
var saver = new ReservationSaver();
var testPassenger = null,
testFlight = null;

spyOn(saver, 'saveReservation');

beforeEach(function () {
testPassenger = {
firstName: "윤지",
lastName: "김"
};

testFlight = {
number: "3443",
carrier: "대한항공",
destination: "울산"
};
});

createReservation(testPassenger, testFlight, saver);

expect(saver.saveReservation).toHaveBeenCalled();
})

스파이를 써서 saver 객체의 saveReservation 구현부를 예약 데이터 저장 기능과 무관한 함수로 대체했다. 스파이는 함수를 호출한 시점과 호출 시 전달한 인자까지 정확히 포착하고, 무엇보다 재스민은 어떤 스파이가 한 번 이상 실했됐는지 확인하는 기대식을 지닌 스파이 전용 매처(toHaveBeenCalled())를 지원한다.

createReservation 함수의 인자가 늘었으니 기존 두 테스트 역시 수정할 수 밖에 없다. 하지만 saveReservation 함수 구현부를 직접 실행할 테스트는 없을 테니 ReservationSaver 생성 코드와 스파이 관련 코드를 전체 꾸러미의 beforeEach 함수로 옮겨 리팩토링 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
describe("createReservation", function () {
var testPassenger = null,
testFlight = null,
testReservation = null,
testSaver = null;

beforeEach(function () {
testPassenger = {
firstName: "윤지",
lastName: "김"
};

testFlight = {
number: "3443",
carrier: "대한항공",
destination: "울산"
};

testSaver = new ReservationSaver();
spyOn(testSaver, 'saveReservation');

testReservation = createReservation(testPassenger, testFlight, testSaver);
});

it("passenger를 passenger Information 프로퍼티에 할당한다.", function () {
expect(testReservation.passengerInformation).toBe(testPassenger);
});

it("flight를 flightInformation 프로퍼티에 할당한다.", function () {
expect(testReservation.flightInformation).toBe(testFlight);
});

it("예약 정보를 저장한다.", function () {
expect(testSaver.saveReservation).toHaveBeenCalled();
});
})

참조: 자바스크립트 패턴과 테스트
github

댓글

You need to set client_id and slot_id to show this AD unit. Please set it in _config.yml.