(여기에 관련 질문이 있습니다 : Jasmine 테스트에 AngularJS 모듈이 표시되지 않음 )
Angular를 부트 스트랩하지 않고 서비스를 테스트하고 싶습니다.
몇 가지 예제와 튜토리얼을 보았지만 아무데도 가지 않을 것입니다.
세 개의 파일 만 있습니다.
-
myService.js : AngularJS 서비스를 정의하는 곳
-
test_myService.js : 서비스에 대한 Jasmine 테스트를 정의합니다.
-
specRunner.html : 일반 jasmine 구성이 포함 된 HTML 파일이며 이전 두 개의 다른 파일과 Jasmine, Angularjs 및 angular-mocks.js를 가져옵니다.
다음은 서비스 코드입니다 (테스트하지 않을 때 예상대로 작동 함).
var myModule = angular.module('myModule', []);
myModule.factory('myService', function(){
var serviceImplementation = {};
serviceImplementation.one = 1;
serviceImplementation.two = 2;
serviceImplementation.three = 3;
return serviceImplementation
});
서비스를 격리하여 테스트하려고 할 때 서비스에 액세스하고 방법을 확인할 수 있어야합니다. 제 질문은 AngularJS를 부트 스트래핑하지 않고 테스트에 서비스를 어떻게 주입 할 수 있습니까?
예를 들어 Jasmine을 사용하여 서비스 메서드에 대해 반환 된 값을 다음과 같이 어떻게 테스트 할 수 있습니까?
describe('myService test', function(){
describe('when I call myService.one', function(){
it('returns 1', function(){
myModule = angular.module('myModule');
//something is missing here..
expect( myService.one ).toEqual(1);
})
})
});
답변
문제는 서비스를 인스턴스화하는 팩토리 메소드가 위의 예제에서 호출되지 않는다는 것입니다 (모듈을 생성하는 것만으로는 서비스를 인스턴스화하지 않음).
서비스를 인스턴스화 하려면 angular.injector 를 서비스가 정의 된 모듈과 함께 호출해야합니다. 그런 다음 서비스에 대한 새 인젝터 객체에 요청할 수 있으며 서비스가 최종적으로 인스턴스화 될 때만 요청할 수 있습니다.
다음과 같이 작동합니다.
describe('myService test', function(){
describe('when I call myService.one', function(){
it('returns 1', function(){
var $injector = angular.injector([ 'myModule' ]);
var myService = $injector.get( 'myService' );
expect( myService.one ).toEqual(1);
})
})
});
또 다른 방법은 ‘ invoke ‘를 사용하여 서비스를 함수에 전달하는 것입니다 .
describe('myService test', function(){
describe('when I call myService.one', function(){
it('returns 1', function(){
myTestFunction = function(aService){
expect( aService.one ).toEqual(1);
}
//we only need the following line if the name of the
//parameter in myTestFunction is not 'myService' or if
//the code is going to be minify.
myTestFunction.$inject = [ 'myService' ];
var myInjector = angular.injector([ 'myModule' ]);
myInjector.invoke( myTestFunction );
})
})
});
그리고 마지막으로 ‘적절한’방법 은 ‘ beforeEach’jasmine 블록 에서 ‘ inject ‘와 ‘ module ‘을 사용하는 것 입니다. 그것을 할 때 우리는 ‘inject’함수가 표준 angularjs 패키지가 아니라 ngMock 모듈에 있으며 jasmine에서만 작동한다는 것을 깨달아야합니다.
describe('myService test', function(){
describe('when I call myService.one', function(){
beforeEach(module('myModule'));
it('returns 1', inject(function(myService){ //parameter name = service name
expect( myService.one ).toEqual(1);
}))
})
});
답변
위의 대답은 아마도 잘 작동하지만 (나는 그것을 시도하지 않았습니다 :)), 나는 종종 더 많은 테스트를 실행하기 때문에 테스트 자체에 주입하지 않습니다. it () 사례를 설명 블록으로 그룹화하고 각 설명 블록의 beforeEach () 또는 beforeAll ()에서 주입을 실행합니다.
Robert는 또한 Angular $ injector를 사용하여 테스트가 서비스 또는 공장을 인식하도록해야한다고 말합니다. Angular는 응용 프로그램에서도이 인젝터 자체를 사용하여 응용 프로그램에 사용 가능한 것을 알려줍니다. 그러나 둘 이상의 위치에서 호출 할 수 있으며 명시 적으로 대신 암시 적 으로 호출 할 수도 있습니다 . 아래의 예제 사양 테스트 파일에서 beforeEach () 블록은 암시 적으로 인젝터 를 호출 하여 테스트 내부에 할당 할 수 있도록합니다.
그룹화 및 사전 블록 사용으로 돌아가서, 여기에 작은 예가 있습니다. 나는 Cat 서비스를 만들고 있고 그것을 테스트하고 싶으므로 서비스를 작성하고 테스트하는 간단한 설정은 다음과 같습니다.
app.js
var catsApp = angular.module('catsApp', ['ngMockE2E']);
angular.module('catsApp.mocks', [])
.value('StaticCatsData', function() {
return [{
id: 1,
title: "Commando",
name: "Kitty MeowMeow",
score: 123
}, {
id: 2,
title: "Raw Deal",
name: "Basketpaws",
score: 17
}, {
id: 3,
title: "Predator",
name: "Noseboops",
score: 184
}];
});
catsApp.factory('LoggingService', ['$log', function($log) {
// Private Helper: Object or String or what passed
// for logging? Let's make it String-readable...
function _parseStuffIntoMessage(stuff) {
var message = "";
if (typeof stuff !== "string") {
message = JSON.stringify(stuff)
} else {
message = stuff;
}
return message;
}
/**
* @summary
* Write a log statement for debug or informational purposes.
*/
var write = function(stuff) {
var log_msg = _parseStuffIntoMessage(stuff);
$log.log(log_msg);
}
/**
* @summary
* Write's an error out to the console.
*/
var error = function(stuff) {
var err_msg = _parseStuffIntoMessage(stuff);
$log.error(err_msg);
}
return {
error: error,
write: write
};
}])
catsApp.factory('CatsService', ['$http', 'LoggingService', function($http, Logging) {
/*
response:
data, status, headers, config, statusText
*/
var Success_Callback = function(response) {
Logging.write("CatsService::getAllCats()::Success!");
return {"status": status, "data": data};
}
var Error_Callback = function(response) {
Logging.error("CatsService::getAllCats()::Error!");
return {"status": status, "data": data};
}
var allCats = function() {
console.log('# Cats.allCats()');
return $http.get('/cats')
.then(Success_Callback, Error_Callback);
}
return {
getAllCats: allCats
};
}]);
var CatsController = function(Cats, $scope) {
var vm = this;
vm.cats = [];
// ========================
/**
* @summary
* Initializes the controller.
*/
vm.activate = function() {
console.log('* CatsCtrl.activate()!');
// Get ALL the cats!
Cats.getAllCats().then(
function(litter) {
console.log('> ', litter);
vm.cats = litter;
console.log('>>> ', vm.cats);
}
);
}
vm.activate();
}
CatsController.$inject = ['CatsService', '$scope'];
catsApp.controller('CatsCtrl', CatsController);
사양 : 고양이 컨트롤러
'use strict';
describe('Unit Tests: Cats Controller', function() {
var $scope, $q, deferred, $controller, $rootScope, catsCtrl, mockCatsData, createCatsCtrl;
beforeEach(module('catsApp'));
beforeEach(module('catsApp.mocks'));
var catsServiceMock;
beforeEach(inject(function(_$q_, _$controller_, $injector, StaticCatsData) {
$q = _$q_;
$controller = _$controller_;
deferred = $q.defer();
mockCatsData = StaticCatsData();
// ToDo:
// Put catsServiceMock inside of module "catsApp.mocks" ?
catsServiceMock = {
getAllCats: function() {
// Just give back the data we expect.
deferred.resolve(mockCatsData);
// Mock the Promise, too, so it can run
// and call .then() as expected
return deferred.promise;
}
};
}));
// Controller MOCK
var createCatsController;
// beforeEach(inject(function (_$rootScope_, $controller, FakeCatsService) {
beforeEach(inject(function (_$rootScope_, $controller, CatsService) {
$rootScope = _$rootScope_;
$scope = $rootScope.$new();
createCatsController = function() {
return $controller('CatsCtrl', {
'$scope': $scope,
CatsService: catsServiceMock
});
};
}));
// ==========================
it('should have NO cats loaded at first', function() {
catsCtrl = createCatsController();
expect(catsCtrl.cats).toBeDefined();
expect(catsCtrl.cats.length).toEqual(0);
});
it('should call "activate()" on load, but only once', function() {
catsCtrl = createCatsController();
spyOn(catsCtrl, 'activate').and.returnValue(mockCatsData);
// *** For some reason, Auto-Executing init functions
// aren't working for me in Plunkr?
// I have to call it once manually instead of relying on
// $scope creation to do it... Sorry, not sure why.
catsCtrl.activate();
$rootScope.$digest(); // ELSE ...then() does NOT resolve.
expect(catsCtrl.activate).toBeDefined();
expect(catsCtrl.activate).toHaveBeenCalled();
expect(catsCtrl.activate.calls.count()).toEqual(1);
// Test/Expect additional conditions for
// "Yes, the controller was activated right!"
// (A) - there is be cats
expect(catsCtrl.cats.length).toBeGreaterThan(0);
});
// (B) - there is be cats SUCH THAT
// can haz these properties...
it('each cat will have a NAME, TITLE and SCORE', function() {
catsCtrl = createCatsController();
spyOn(catsCtrl, 'activate').and.returnValue(mockCatsData);
// *** and again...
catsCtrl.activate();
$rootScope.$digest(); // ELSE ...then() does NOT resolve.
var names = _.map(catsCtrl.cats, function(cat) { return cat.name; })
var titles = _.map(catsCtrl.cats, function(cat) { return cat.title; })
var scores = _.map(catsCtrl.cats, function(cat) { return cat.score; })
expect(names.length).toEqual(3);
expect(titles.length).toEqual(3);
expect(scores.length).toEqual(3);
});
});
사양 : 고양이 서비스
'use strict';
describe('Unit Tests: Cats Service', function() {
var $scope, $rootScope, $log, cats, logging, $httpBackend, mockCatsData;
beforeEach(module('catsApp'));
beforeEach(module('catsApp.mocks'));
describe('has a method: getAllCats() that', function() {
beforeEach(inject(function($q, _$rootScope_, _$httpBackend_, _$log_, $injector, StaticCatsData) {
cats = $injector.get('CatsService');
$rootScope = _$rootScope_;
$httpBackend = _$httpBackend_;
// We don't want to test the resolving of *actual data*
// in a unit test.
// The "proper" place for that is in Integration Test, which
// is basically a unit test that is less mocked - you test
// the endpoints and responses and APIs instead of the
// specific service behaviors.
mockCatsData = StaticCatsData();
// For handling Promises and deferrals in our Service calls...
var deferred = $q.defer();
deferred.resolve(mockCatsData); // always resolved, you can do it from your spec
// jasmine 2.0
// Spy + Promise Mocking
// spyOn(obj, 'method'), (assumes obj.method is a function)
spyOn(cats, 'getAllCats').and.returnValue(deferred.promise);
/*
To mock $http as a dependency, use $httpBackend to
setup HTTP calls and expectations.
*/
$httpBackend.whenGET('/cats').respond(200, mockCatsData);
}));
afterEach(function() {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
})
it(' exists/is defined', function() {
expect( cats.getAllCats ).toBeDefined();
expect( typeof cats.getAllCats ).toEqual("function");
});
it(' returns an array of Cats, where each cat has a NAME, TITLE and SCORE', function() {
cats.getAllCats().then(function(data) {
var names = _.map(data, function(cat) { return cat.name; })
var titles = _.map(data, function(cat) { return cat.title; })
var scores = _.map(data, function(cat) { return cat.score; })
expect(names.length).toEqual(3);
expect(titles.length).toEqual(3);
expect(scores.length).toEqual(3);
})
});
})
describe('has a method: getAllCats() that also logs', function() {
var cats, $log, logging;
beforeEach(inject(
function(_$log_, $injector) {
cats = $injector.get('CatsService');
$log = _$log_;
logging = $injector.get('LoggingService');
spyOn(cats, 'getAllCats').and.callThrough();
}
))
it('that on SUCCESS, $logs to the console a success message', function() {
cats.getAllCats().then(function(data) {
expect(logging.write).toHaveBeenCalled();
expect( $log.log.logs ).toContain(["CatsService::getAllCats()::Success!"]);
})
});
})
});
편집
일부 의견을 기반으로 답변을 약간 더 복잡하게 업데이트했으며 단위 테스트를 보여주는 Plunkr도 구성했습니다. 특히, “컨트롤러의 서비스에 $ log와 같은 단순한 종속성이있는 경우 어떻게됩니까?”라는 의견이 있습니다. -테스트 케이스와 함께 예제에 포함됩니다. 도움이 되었기를 바랍니다. 행성을 테스트하거나 해킹하세요 !!!
답변
다른 지시문 인 Google Places Autocomplete 를 요구하는 지시문을 테스트 해야했습니다. 그냥 조롱해야하는지에 대해 토론 중이었습니다 … 어쨌든 gPlacesAutocomplete가 필요한 지시문에 오류가 발생하지 않고 작동했습니다.
describe('Test directives:', function() {
beforeEach(module(...));
beforeEach(module(...));
beforeEach(function() {
angular.module('google.places', [])
.directive('gPlacesAutocomplete',function() {
return {
require: ['ngModel'],
restrict: 'A',
scope:{},
controller: function() { return {}; }
};
});
});
beforeEach(module('google.places'));
});
답변
컨트롤러를 테스트하고 싶다면 아래와 같이 주입하고 테스트 할 수 있습니다.
describe('When access Controller', function () {
beforeEach(module('app'));
var $controller;
beforeEach(inject(function (_$controller_) {
// The injector unwraps the underscores (_) from around the parameter names when matching
$controller = _$controller_;
}));
describe('$scope.objectState', function () {
it('is saying hello', function () {
var $scope = {};
var controller = $controller('yourController', { $scope: $scope });
expect($scope.objectState).toEqual('hello');
});
});
});