웹 애플리케이션을 만들면 요즘 같은 경우 REST API를 사용하는 것이 일반적이고 특히 AngularJS를 사용하면 SPA(Single Page Applicaion)처럼 만드는 경우가 많으므로 REST API를 많이 사용하게 된다.
angular.module('MyApp', [])
.factory('ApiService', function($http) {
return {
members: $http.get('http://express.dev/members/')
};
});
위와 같은 ApiService
가 있다고 해보자. ApiService
는 가상으로 만든 express.dev
라는 서버에 회원목록을 조회하는 서비스다. 이 서비스를 Karma로 테스트하려면(Karma 테스트 러너 사용하기 참고) 다음과 같이 테스트를 작성할 것이다.
describe('MyApp', function() {
'use strict';
var $rootScope;
beforeEach(module('MyApp'));
beforeEach(inject(function(_$rootScope_) {
$rootScope = _$rootScope_;
}));
describe('ApiService', function() {
it('member\'s length should be 4', function(done) {
inject(function(ApiService) {
ApiService.members.success(function(data) {
expect(data.members).to.have.length(4);
done();
});
$rootScope.$digest();
});
});
});
});
이 테스트의 결과를 보기 전에 먼저 설명을 해야 할 부분이 좀 있다. 이 테스트 코드에는 이전 글에는 없었던 $rootScope
와 관련된 부분이 들어있다. 이 코드가 필요한 이유는 AngularJS의 메커니즘 때문에 그런데 내부 구조를 자세히는 몰라서 동작방식까지 설명하기는 어렵지만 AngularJS에서는 어떤 값이 처리되는 시점은 뷰에 표현될 때이다.(정확한 표현인지는 모르겠지만) 웹 브라우저에서 HTML이 있으므로 항상 잘 동작하지만, Karma에서 유닛 테스트를 수행할 때는 뷰가 존재하지 않으므로 뷰 표현 단계가 존재하지 않고 $http
처럼 Promise 같은 것도 resolve
단계에 들어가지 않는다. 좀 더 정확히 얘기하자면 스코프가 $digest
되는 순간(혹은 $apply
)에 실제 모든 처리가 이뤄지는 시점이다. 그래서 테스트코드에서 수동으로 해당 스코프를 $digest
해주지 않으면 처리가 되지 않는다. 즉, 디렉티브로 DOM을 그린다면 컴파일만 해놓고 표현이 되지 않는다거나 위 예제 같은 경우는 HTTP 요청을 보내지 않거나 한다. 그래서 테스트 전인 beforeEach
단계에서 $rootScope
를 가져와서 테스트 마지막 부분에서 $rootScope.$digest();
를 실행(19라인)시킨 것이다.
위 코드를 보면 루트 스코프를 주입할 때 _$rootScope_
로 주입한 뒤에 이를 $rootScope
변수에 할당한 걸 볼 수 있는데 이는 여러 소스를 찾아본 결과 AngularJS 테스트에서 일반적으로 사용하는 테크닉(이라고 쓰고 꼼수라 읽는다)이다. 테스트 내에서 $rootScope
를 계속 사용할 것이므로 $rootScope
를 변수로 정의해서 계속 사용하고 싶은데 이럴 경우 주입하는 파라미터 이름과 겹쳐서 $rootScope = $rootScope;
같은 형태가 돼서 동작하지 않으므로 주입을 _$rootScope_
와 같이 한 것이다. 이렇게 하면 주입하는 파라미터 명은 (약간 이상해 보이는) _$rootScope_
가 되지만 AngularJS가 알아서 _
는 무시하고 루트 스코프를 잘 주입한다. 물론 _$rootScope_
와 같이 사용하는 게 싫다면 $rootScope
로 주입하고 로컬에서 사용하는 변수명을 $rootScope
대신 root
나 rs
처럼 원하는 대로 작명해도 상관없다.
어쨌든 얘기가 다른 곳으로 좀 샜는데 위 테스트를 실행하면 다음과 같이 Error: Unexpected request: GET http://express.dev/members/
라는 오류가 던져지면서 No more request expected
라는 메시지가 나온다.
처음에 이 메시지를 보았을 때는 무척 당황스럽지만, 이는 AngularJS에서 의도적으로 막은 것으로 보인다. 유닛테스트에서는 $http
를 사용해서 외부로 요청을 보낼 수 없다. 이상적인 유닛 테스트의 관점에서는 테스트가 외부 리소스에 의존성을 갖지 않는 것이 좋으므로 옳다고 느껴지지만, 너무 엄격하게 느껴지는 것도 사실이다.(다른 우회 방법이 있는지는 아직 잘 모르겠다.) 그래서 테스트에서는 $http
도 목킹해서 테스트를 작성해야 하므로 Angular는 ngMock에서 $httpBackend를 제공하고 있다.
$httpBackend
을 사용해서 테스트를 다음과 같이 다시 작성할 수 있다.
describe('MyApp', function() {
'use strict';
var $rootScope, $httpBackend;
beforeEach(module('MyApp'));
beforeEach(inject(function(_$rootScope_, _$httpBackend_) {
$rootScope = _$rootScope_;
$httpBackend = _$httpBackend_;
$httpBackend.whenGET('http://express.dev/members/')
.respond({
"members": ["zziuni", "nanha", "odyss", "danni"]
});
}));
describe('ApiService', function() {
it('member\'s length should be 4', function(done) {
inject(function(ApiService) {
ApiService.members.success(function(data) {
expect(data.members).to.have.length(4);
done();
});
$rootScope.$digest();
$httpBackend.flush();
});
});
});
});
$httpBackend
도 $rootScope
와 같은 방법으로 주입하고 $httpBackend
에서 whenGet
함수를 이용해서(여기선 GET 요청을 사용하므로) URL을 정의하고 respond
함수로 응답을 지정한다. 이것에 하면 $http
로 보내는 요청을 $httpBackend
을 가로채고 지정한 URL 같은 경우 해당 응답을 대신 보내준다. (여기서 URL 정의는 파라미터까지 완전히 같아야 한다.) 마지막으로 테스트에서 $httpBackend.flush();
를 실행하면 $httpBackend
가 응답을 돌려준다.(flush()
하지 않으면 응답을 보내지 않는다.) AngularJS 문서에 따르면 $digest
이전에 $httpBackend.flush()
를 실행하지 말라고 한다.
Comments