javascript - Hur bör run-blocket hanteras i enhetsprov i Angular?
Min förståelse är att när du laddar din modul i Angular enhetstester kallas run-blocket.
Jag skulle tro att om du testar en komponent vill du inte samtidigt testa run-blocket, eftersom unittest ska bara testa en enhet. Är det sant?
Om så är fallet, finns det ett sätt att förhindra att run-blocket körs? Min forskning får mig att tro att svaret är ”nej” och att run-blocket alltid körs när modulen laddas, men kanske finns det ett sätt att åsidosätta detta. Om inte, hur skulle jag testa run-blocket?
Kör block:
function run(Auth, $cookies, $rootScope) {
$rootScope.user = {};
Auth.getCurrentUser();
}
Auth.getCurrentUser:
getCurrentUser: function() {
// user is logged in
if (Object.keys($rootScope.user).length > 0) {
return $q.when($rootScope.user);
}
// user is logged in, but page has been refreshed and $rootScope.user is lost
if ($cookies.get('userId')) {
return $http.get('/current-user')
.then(function(response) {
angular.copy(response.data, $rootScope.user);
return $rootScope.user;
})
;
}
// user isn't logged in
else {
return $q.when({});
}
}
auth.factory.spec.js
describe('Auth Factory', function() {
var Auth, $httpBackend, $rootScope, $cookies, $q;
var user = {
username: 'a',
password: 'password',
};
var response = {
_id: 1,
local: {
username: 'a',
role: 'user'
}
};
function isPromise(el) {
return !!el.$$state;
}
beforeEach(module('mean-starter', 'ngCookies', 'templates'));
beforeEach(inject(function(_Auth_, _$httpBackend_, _$rootScope_, _$cookies_, _$q_) {
Auth = _Auth_;
$httpBackend = _$httpBackend_;
$rootScope = _$rootScope_;
$cookies = _$cookies_;
$q = _$q_;
}));
afterEach(function() {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
it('#signup', function() {
$rootScope.user = {};
$httpBackend.expectPOST('/users', user).respond(response);
spyOn(angular, 'copy').and.callThrough();
spyOn($cookies, 'put').and.callThrough();
var retVal = Auth.signup(user);
$httpBackend.flush();
expect(angular.copy).toHaveBeenCalledWith(response, $rootScope.user);
expect($cookies.put).toHaveBeenCalledWith('userId', 1);
expect(isPromise(retVal)).toBe(true);
});
it('#login', function() {
$rootScope.user = {};
$httpBackend.expectPOST('/login', user).respond(response);
spyOn(angular, 'copy').and.callThrough();
spyOn($cookies, 'put').and.callThrough();
var retVal = Auth.login(user);
$httpBackend.flush();
expect(angular.copy).toHaveBeenCalledWith(response, $rootScope.user);
expect($cookies.put).toHaveBeenCalledWith('userId', 1);
expect(isPromise(retVal)).toBe(true);
});
it('#logout', function() {
$httpBackend.expectGET('/logout').respond();
spyOn(angular, 'copy').and.callThrough();
spyOn($cookies, 'remove');
Auth.logout();
$httpBackend.flush();
expect(angular.copy).toHaveBeenCalledWith({}, $rootScope.user);
expect($cookies.remove).toHaveBeenCalledWith('userId');
});
describe('#getCurrentUser', function() {
it('User is logged in', function() {
$rootScope.user = response;
spyOn($q, 'when').and.callThrough();
var retVal = Auth.getCurrentUser();
expect($q.when).toHaveBeenCalledWith($rootScope.user);
expect(isPromise(retVal)).toBe(true);
});
it('User is logged in but page has been refreshed', function() {
$cookies.put('userId', 1);
$httpBackend.expectGET('/current-user').respond(response);
spyOn(angular, 'copy').and.callThrough();
var retVal = Auth.getCurrentUser();
$httpBackend.flush();
expect(angular.copy).toHaveBeenCalledWith(response, $rootScope.user);
expect(isPromise(retVal)).toBe(true);
});
it("User isn't logged in", function() {
$rootScope.user = {};
$cookies.remove('userId');
spyOn($q, 'when').and.callThrough();
var retVal = Auth.getCurrentUser();
expect($q.when).toHaveBeenCalledWith({});
expect(isPromise(retVal)).toBe(true);
});
});
});
Försök 1:
beforeEach(module('mean-starter', 'ngCookies', 'templates'));
beforeEach(inject(function(_Auth_, _$httpBackend_, _$rootScope_, _$cookies_, _$q_) {
Auth = _Auth_;
$httpBackend = _$httpBackend_;
$rootScope = _$rootScope_;
$cookies = _$cookies_;
$q = _$q_;
}));
beforeEach(function() {
spyOn(Auth, 'getCurrentUser');
});
afterEach(function() {
expect(Auth.getCurrentUser).toHaveBeenCalled();
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
Detta fungerar inte. run-blocket körs när modulen laddas, så Auth.getCurrentUser() anropas innan spionen är inställd.
Expected spy getCurrentUser to have been called.
Försök 2:
beforeEach(inject(function(_Auth_, _$httpBackend_, _$rootScope_, _$cookies_, _$q_) {
Auth = _Auth_;
$httpBackend = _$httpBackend_;
$rootScope = _$rootScope_;
$cookies = _$cookies_;
$q = _$q_;
}));
beforeEach(function() {
spyOn(Auth, 'getCurrentUser');
});
beforeEach(module('mean-starter', 'ngCookies', 'templates'));
afterEach(function() {
expect(Auth.getCurrentUser).toHaveBeenCalled();
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
Detta fungerar inte eftersom Auth inte är tillgängligt för att injiceras innan mitt appmodul laddas.
Error: [$injector:unpr] Unknown provider: AuthProvider <- Auth
Försök 3:
Som du kan se finns det ett hönan-eller-ägget-problem här. Jag behöver injicera Auth och sätta upp spionen innan modulen laddas, men jag kan inte eftersom Auth inte är tillgänglig för att injiceras innan modulen laddas.
Detta blogginlägg nämner hönan-och-ägget-problemet och ger en intressant möjlig lösning. Författaren föreslår att jag borde skapa min Auth-tjänst manuellt med $provide innan jag laddar mitt modul. Eftersom jag skapar tjänsten själv, istället för att injicera den, skulle jag kunna göra det innan modulen laddas, och jag skulle kunna sätta upp spionen. Sedan, när modulen laddas, skulle den använda denna skapade mock-tjänst.
Här är hans exempel på kod:
describe('example', function () {
var loggingService;
beforeEach(function () {
module('example', function ($provide) {
$provide.value('loggingService', {
start: jasmine.createSpy()
});
});
inject(function (_loggingService_) {
loggingService = _loggingService_;
});
});
it('should start logging service', function() {
expect(loggingService.start).toHaveBeenCalled();
});
});
Problemet med detta är att jag behöver min Auth-tjänst! Jag skulle bara vilja använda den mockade för run-blocket; jag behöver min riktiga Auth-tjänst någon annanstans så att jag kan testa den.
Jag antar att jag skulle kunna skapa själva Auth-tjänsten med $provide, men det känns fel.
Sista frågan - för vilken kod jag än använder för att hantera detta run-blockproblem, finns det ett sätt för mig att extrahera det så att jag inte behöver skriva om det för varje av mina spec-filer? Det enda sättet jag kan tänka mig att göra det på skulle vara att använda någon form av global funktion.
auth.factory.js
angular
.module('mean-starter')
.factory('Auth', Auth)
;
function Auth($http, $state, $window, $cookies, $q, $rootScope) {
return {
signup: function(user) {
return $http
.post('/users', user)
.then(function(response) {
angular.copy(response.data, $rootScope.user);
$cookies.put('userId', response.data._id);
$state.go('home');
})
;
},
login: function(user) {
return $http
.post('/login', user)
.then(function(response) {
angular.copy(response.data, $rootScope.user);
$cookies.put('userId', response.data._id);
$state.go('home');
})
;
},
logout: function() {
$http
.get('/logout')
.then(function() {
angular.copy({}, $rootScope.user);
$cookies.remove('userId');
$state.go('home');
})
.catch(function() {
console.log('Problem logging out.');
})
;
},
getCurrentUser: function() {
// user is logged in
if (Object.keys($rootScope.user).length > 0) {
return $q.when($rootScope.user);
}
// user is logged in, but page has been refreshed and $rootScope.user is lost
if ($cookies.get('userId')) {
return $http.get('/current-user')
.then(function(response) {
angular.copy(response.data, $rootScope.user);
return $rootScope.user;
})
;
}
// user isn't logged in
else {
return $q.when({});
}
}
};
}
Redigera - misslyckat försök + lyckat försök:
beforeEach(module('auth'));
beforeEach(inject(function(_Auth_) {
Auth = _Auth_;
spyOn(Auth, 'requestCurrentUser');
}));
beforeEach(module('mean-starter', 'ngCookies', 'templates'));
beforeEach(inject(function(_Auth_, _$httpBackend_, _$rootScope_, _$cookies_, _$q_) {
// Auth = _Auth_;
$httpBackend = _$httpBackend_;
$rootScope = _$rootScope_;
$cookies = _$cookies_;
$q = _$q_;
}));
// beforeEach(function() {
// spyOn(Auth, 'getCurrentUser');
// });
afterEach(function() {
expect(Auth.getCurrentUser).toHaveBeenCalled();
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
Jag är inte säker på varför detta inte skulle fungera (oavsett problemet med att använda inject två gånger).
Jag försökte undvika att använda $provide eftersom det initialt kändes hackigt/konstigt för mig. Men efter att ha tänkt på det lite mer känner jag nu att $provide är okej, och att följa ditt förslag att använda mock-auth är fantastiskt!!! Båda fungerade för mig.
I auth.factory.spec.js laddade jag precis auth-modulen (jag kallar den auth, inte mean-auth) utan att ladda mean-starter. Detta har inte problemet med run-blocket eftersom den modulen inte har run-blockkod, men det låter mig testa min Auth-fabrik. Annars fungerar detta:
beforeEach(module('mean-starter', 'templates', function($provide) {
$provide.value('Auth', {
requestCurrentUser: jasmine.createSpy()
});
}));
Liksom den fantastiska mock-auth-lösningen:
auth.factory.mock.js
angular
.module('mock-auth', [])
.factory('Auth', Auth)
;
function Auth() {
return {
requestCurrentUser: jasmine.createSpy()
};
}
user.service.spec.js
beforeEach(module('mean-starter', 'mock-auth', 'templates'));
Svar #1
Min förståelse är att när du laddar ditt modul i Angular enhetstester, körs run-blocket.
Korrekt.
Jag skulle tro att om du testar en komponent, vill du inte samtidigt testa run-blocket, eftersom enhetstester är tänkta att bara testa en enhet. Är det sant?
Också korrekt, eftersom du just nu i praktiken testar integrationen av Auth och din run-block, och det finns ingen isolering av det ena från det andra.
Om så är fallet, finns det ett sätt att förhindra att körblocket körs? Min forskning får mig att tro att svaret är "nej", och att körblocket alltid körs när modulen laddas, men kanske finns det ett sätt att åsidosätta detta. Om inte, hur skulle jag testa körblocket?
Som implementerat kan du nej, du kan inte förhindra att körblocket körs. Det förblir dock möjligt med viss mindre omstrukturering eftersom din fråga i slutändan handlar om modularisering. Utan att kunna se din moduldeklaration skulle jag föreställa mig att det ser ut ungefär så här:
angular.module('mean-starter', ['ngCookies'])
.factory('Auth', function($cookies) {
...
});
.run(function(Auth, $rootScope) {
...
});
Detta mönster kan delas upp i moduler för att stödja testbarhet (och modulåteranvändning):
angular.module('mean-auth', ['ngCookies'])
.factory('Auth', function() {
...
});
angular.module('mean-starter', ['mean-auth'])
.run(function(Auth, $rootScope) {
...
});
Detta gör det nu möjligt för dig att testa din Auth-fabrik isolerat genom att endast ladda mean-auth-modulen i dess test.
Medan detta löser problemet med att ditt run-block stör dina enhetstester för Auth, står du fortfarande inför problemet att mocka Auth.getCurrentUser för att kunna testa ditt run-block isolerat. Blogginlägget du refererade till har rätt i att du bör försöka utnyttja konfigurationsstadiet av modulen för att stubba/spionera på beroenden som används under run-stadiet. Därför, i ditt test:
module('mean-starter', function ($provide) {
$provide.value('Auth', {
getCurrentUser: jasmine.createSpy()
});
});
När det gäller din sista fråga kan du skapa återanvändbara mockar genom att deklarera dem som moduler. Till exempel, om du vill skapa en återanvändbar mockfabrik för Auth definierar du den i en separat fil som laddas innan dina enhetstester:
angular.module('mock-auth', [])
.factory('Auth', function() {
return {
getCurrentUser: jasmine.createSpy()
};
});
och sedan ladda in det i dina tester efter alla moduler där du behöver det, eftersom Angular kommer att skriva över alla tjänster med samma namn:
module('mean-starter', 'mock-auth');
