2015-08-23 2 views
6

Mein Verständnis ist, dass, wenn Sie Ihr Modul in Angular Unit Tests laden, der run Block aufgerufen wird.Wie sollte der Laufblock in Angular Unit Tests behandelt werden?

Ich würde denken, dass, wenn Sie eine Komponente zu test, würden Sie nicht gleichzeitig den run Block testen wollen, weil Einheit Tests sollen nur testen, eine Einheit. Ist das wahr?

Wenn ja, gibt es eine Möglichkeit zu verhindern, dass der run Block läuft? Meine Forschung führt mich zu der Annahme, dass die Antwort "Nein" ist und dass der Block run immer ausgeführt wird, wenn das Modul geladen wird, aber vielleicht gibt es eine Möglichkeit, dies zu überschreiben. Wenn nicht, wie würde ich den run Block testen?

Run 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); 
    }); 
    }); 
}); 

Versuch 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(); 
}); 

Dies funktioniert nicht. Der Baustein run wird ausgeführt, wenn das Modul geladen wird. Daher wird Auth.getCurrentUser() aufgerufen, bevor der Spion eingerichtet wird.

Expected spy getCurrentUser to have been called. 

Versuch 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(); 
}); 

Das funktioniert nicht, weil Auth nicht verfügbar ist injiziert werden, bevor meine App-Modul geladen wird.

Error: [$injector:unpr] Unknown provider: AuthProvider <- Auth 

Versuch 3:

Wie Sie sehen können, gibt es ein hier Huhn-Ei-Problem. Ich muss Auth injizieren und den Spion einrichten, bevor das Modul geladen wird, aber ich kann nicht, da Auth nicht verfügbar ist, um injiziert zu werden, bevor das Modul geladen wird.

This Blog-Beiträge erwähnt das Huhn-Ei-Problem und bietet eine interessante potenzielle Lösung. Der Autor schlägt vor, dass ich meinen Auth Dienst manuell unter Verwendung $providevor ich mein Modul laden sollte. Da ich den Dienst erstelle und ihn nicht einspeise, könnte ich es tun, bevor das Modul geladen wird, und ich könnte den Spion einrichten. Wenn dann das Modul geladen wird, würde es diesen erstellten Scheindienst verwenden.

Hier ist sein Beispiel-Code:

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(); 
    }); 
}); 

Das Problem dabei ist, dass ich meine Auth Service brauchen! Ich würde nur den Schein für den run Block verwenden wollen; Ich brauche meinen echten Auth Service woanders, damit ich es testen kann.

Ich denke, dass ich den tatsächlichen Auth Service mit $provide erstellen könnte, aber das fühlt sich falsch an.


Letzte Frage - gleich aus welchem ​​Code, den ich am Ende mit diesem run Block Problem zu umgehen, gibt es eine Möglichkeit für mich, um sie zu extrahieren, so habe ich es nicht neu schreiben für jede meines Spezifikationsdateien? Die einzige Möglichkeit, dies zu tun, wäre eine Art globaler 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({}); 
     } 
    } 
    }; 
} 

Bearbeiten - gescheiterten Versuch + erfolgreichen Versuch:

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(); 
}); 

Ich bin nicht sicher, warum dies nicht funktionieren würde (unabhängig von dem Problem mit der Verwendung von inject zweimal).

Ich habe versucht, um $provide zu verwenden, wie das anfänglich hacky/seltsam für mich fühlte. Nachdem ich darüber nachgedacht habe, habe ich jetzt das Gefühl, dass $provide in Ordnung ist, und dass nach Ihrem Vorschlag, mock-auth zu verwenden, fantastisch ist !!! Beide haben für mich gearbeitet.

In auth.factory.spec.js Ich lud gerade das auth Modul (Ich nenne es auth, nicht mean-auth) ohne mean-starter geladen. Dies hat nicht das Blockproblem run, weil dieses Modul nicht den run Blockcode hat, aber es erlaubt mir, meine Auth Fabrik zu testen. In anderen Ländern, das funktioniert:

beforeEach(module('mean-starter', 'templates', function($provide) { 
    $provide.value('Auth', { 
    requestCurrentUser: jasmine.createSpy() 
    }); 
})); 

Wie funktioniert die fantastische mock-auth Lösung:

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')); 
+2

off-topic zu Ihrer Frage, aber Sie sollten wissen, dass '.run' nicht auf' $ http' warten, um abzuschließen. Wenn irgendetwas in der App davon abhängt, dass das Ergebnis da ist, hast du eine Wettlaufsituation. Normalerweise würden Sie 'resolve' verwenden, wenn Sie' ngRoute' oder 'ui.router' verwenden. –

Antwort

6

My understanding is that when you load your module in Angular unit tests, the run block gets called.

Korrekt.

I'd think that if you're testing a component, you wouldn't want to simultaneously be testing the run block, because unit tests are supposed to just test one unit. Is that true?

auch richtig, dass gerade jetzt Sie effektiv testen die Integration von Auth und Ihre Laufsatz, und es gibt keine Isolierung eines von der anderen Seite.

Wie implementiert, können Sie nicht verhindern, dass der Ausführungsblock ausgeführt wird. Es bleibt jedoch möglich, mit ein wenig Refactoring, da Ihre Frage letztlich eine Modularisierung ist. Ohne die Möglichkeit, Ihre Moduldeklaration zu sehen, würde ich vorstellen, dass es so etwas wie folgt aussieht:

angular.module('mean-starter', ['ngCookies']) 

    .factory('Auth', function($cookies) { 
    ... 
    }); 

    .run(function(Auth, $rootScope) { 
    ... 
    }); 

Dieses Muster kann in Module zerlegt werden Testbarkeit (und Modul Wiederverwertbarkeit) zu unterstützen:

angular.module('mean-auth', ['ngCookies']) 

    .factory('Auth', function() { 
    ... 
    }); 

angular.module('mean-starter', ['mean-auth']) 

    .run(function(Auth, $rootScope) { 
    ... 
    }); 

Diese jetzt ermöglicht es Ihnen, Ihre Auth Fabrik isoliert zu testen, indem Sie das mean-auth Modul nur in den Test laden.

Während dies das Problem behebt, dass Ihr Laufblock Ihre Komponententests für Auth stört, stehen Sie immer noch vor dem Problem, sich zu ärgern Auth.getCurrentUser, um Ihren Laufblock isoliert zu testen. Der Blogpost, auf den Sie verwiesen haben, ist insofern korrekt, als Sie versuchen sollten, die Konfigurationsphase des Moduls zu nutzen, um Abhängigkeiten, die während der Ausführungsphase verwendet wurden, zu stubben oder auszuspähen. Daher in Ihrem Test:

module('mean-starter', function ($provide) { 
    $provide.value('Auth', { 
    getCurrentUser: jasmine.createSpy() 
    }); 
}); 

Zu Ihrer letzten Frage können Sie wiederverwendbare Mocks erstellen, indem Sie sie als Module deklarieren. Zum Beispiel, wenn Sie eine wiederverwendbare Mock Fabrik für Auth schaffen wollten definieren Sie es in einer separaten Datei vor Ihren Unit-Tests geladen:

angular.module('mock-auth', []) 

.factory('Auth', function() { 
    return { 
    getCurrentUser: jasmine.createSpy() 
    }; 
}); 

und es dann in Ihren Tests zu jedem Modul nachfolgenden laden, in dem Sie benötigen es, als eckig wird jeden Dienst mit dem gleichen Namen überschreiben:

module('mean-starter', 'mock-auth'); 
+0

Danke! Ein paar Fragen (ich werde sie auch selbst untersuchen, aber ich dachte auch, dass ich Sie fragen würde): 1) Re: "Auth.getCurrentUser" verspotten, ist es notwendig, '$ provide' zu ​​verwenden? Könnte ich nicht einfach 'spyOn (Auth, 'getCurrentUser')' 'nach dem Laden' mean-auth' und dem Eingeben von 'Auth' verwenden? 2) Derzeit verwendet meine 'Auth'-Factory' $ rootScope', um den aktuell angemeldeten Benutzer zu verfolgen. Wenn man ein 'mock-auth'-Modul benutzt, ist das' $ rootScope' nicht das gleiche wie das '$ rootScope'-Symbol von 'mean-starter' oder? Ich dachte, das wäre ein guter Anwendungsfall für '$ rootScope', aber ich denke darüber nach, wie ich es jetzt umgestalten könnte. –

+0

Ich habe meine Frage so bearbeitet, dass sie meine aktuelle 'auth.factory.js' enthält. Nachdem ich darüber nachgedacht habe, bin ich mir nicht sicher, wie das separate 'Mean-Auth'-Modul funktionieren würde. Für die ersten 3 Methoden könnte ich einfach das Versprechen nach '.post' /' .get' zurückgeben und den Rest der Logik im Controller erledigen. Aber für 'getCurrentUser' möchte ich die HTTP-Anfrage bedingt abhängig davon machen, ob der aktuelle Benutzer bereits verfügbar ist oder nicht. Aber wie würde "Mean-Auth" über den aktuellen Benutzer wissen? Ich denke, ich könnte das anderswo machen? –

+1

1. Sie benötigen '$ provide 'nicht, um' mean-auth' zu testen, da sein Zustand nicht mehr von Ihrem Laufblock beeinflusst wird. Sie müssen es jedoch zum Testen des "Mittelstarters" verwenden, da der Laufblock sofort ausgelöst wird. 2. Das $ rootScope ist identisch. Angulars mock 'module' sammelt die definierten Module vor dem Erstellen des Injektors, woraufhin $ rootScope als übergeordneter Bereich für die" Anwendung "existiert. – scarlz