Testowanie jednostkowe routera Angular UI (stany na adresy URL)

81

Mam problemy z testowaniem jednostkowym routera w mojej aplikacji, która jest zbudowana na routerze Angular ui. Chcę sprawdzić, czy przejścia stanów odpowiednio zmieniają adres URL (później będą bardziej skomplikowane testy, ale od tego zaczynam).

Oto odpowiednia część mojego kodu aplikacji:

angular.module('scrapbooks')
 .config( function($stateProvider){
    $stateProvider.state('splash', {
       url: "/splash/",
       templateUrl: "/app/splash/splash.tpl.html",
       controller: "SplashCtrl"
    })
 })

A kod testowy:

it("should change to the splash state", function(){
  inject(function($state, $rootScope){
     $rootScope.$apply(function(){
       $state.go("splash");
     });
     expect($state.current.name).to.equal("splash");
  })
})

Podobne pytania dotyczące Stackoverflow (i oficjalnego kodu testowego routera interfejsu użytkownika) sugerują, że zawinięcie wywołania $ state.go w $ apply powinno wystarczyć. Ale zrobiłem to i stan nadal się nie aktualizuje. $ state.current.name pozostaje puste.

Terrence
źródło
Okay, rozgryzłem to (w pewnym sensie). Jeśli zdefiniuję fałszywy router z wbudowanymi szablonami zamiast szablonów URL, przejście się powiedzie.
Terrence,
Czy możesz zamieścić swój działający kod jako odpowiedź?
Levi Hackwith,
2
Zadałem to pytanie prawie rok temu. Obecnie uważam, że najlepszym sposobem rozwiązania tego problemu jest użycie preprocesora ng-template-to-js w Karmie.
Terrence,
A dokładniej: problem polega na tym, że jeśli pobieranie szablonu nie powiedzie się w teście (tj. Ponieważ nie ma serwera), zmiana stanu nie powiedzie się. Jeśli jednak nie obserwujesz zdarzenia $ stateChangeError, nie zobaczysz błędu. Niemniej jednak, ponieważ zmiana stanu nie powiedzie się, $ state.current.name nie zostanie zaktualizowany.
Terrence

Odpowiedzi:

125

Miałem również ten problem i w końcu wymyśliłem, jak to zrobić.

Oto przykładowy stan:

angular.module('myApp', ['ui.router'])
.config(['$stateProvider', function($stateProvider) {
    $stateProvider.state('myState', {
        url: '/state/:id',
        templateUrl: 'template.html',
        controller: 'MyCtrl',
        resolve: {
            data: ['myService', function(service) {
                return service.findAll();
            }]
        }
    });
}]);

Poniższy test jednostkowy obejmie testowanie adresu URL z parametrami i wykonywanie rozwiązań, które wstrzykują własne zależności:

describe('myApp/myState', function() {

  var $rootScope, $state, $injector, myServiceMock, state = 'myState';

  beforeEach(function() {

    module('myApp', function($provide) {
      $provide.value('myService', myServiceMock = {});
    });

    inject(function(_$rootScope_, _$state_, _$injector_, $templateCache) {
      $rootScope = _$rootScope_;
      $state = _$state_;
      $injector = _$injector_;

      // We need add the template entry into the templateCache if we ever
      // specify a templateUrl
      $templateCache.put('template.html', '');
    })
  });

  it('should respond to URL', function() {
    expect($state.href(state, { id: 1 })).toEqual('#/state/1');
  });

  it('should resolve data', function() {
    myServiceMock.findAll = jasmine.createSpy('findAll').and.returnValue('findAll');
    // earlier than jasmine 2.0, replace "and.returnValue" with "andReturn"

    $state.go(state);
    $rootScope.$digest();
    expect($state.current.name).toBe(state);

    // Call invoke to inject dependencies and run function
    expect($injector.invoke($state.current.resolve.data)).toBe('findAll');
  });
});
Philip Chen
źródło
1
Świetny post, oszczędność czasu, jeśli używasz ciągów znaków do zdefiniowania usługi, użyj get zamiast invoke. oczekiwać ($ injector.get ($ state.current.resolve.data)). toBe ('findAll');
Alan Quigley
2
Postępowałem zgodnie z powyższym kodem z dostosowaniami do tych, o których andReturnmowa w powyższym komentarzu. Jednak moja $ state.current.name zwraca pusty ciąg. Czy ktoś wie, dlaczego?
Vicky Leong
5
@Philip Mam ten sam problem $state.current.name- pusty ciąg.
Joy
1
@Joy @VLeong Miałem ten sam problem, ale zdałem sobie sprawę, że jest to spowodowane tym, że narzędzie ui-router, o którym piszę, używało obietnic ES6 zamiast $q. Wszystko musi wykorzystywać $qobietnice, $rootScope.$digest()aby spełnić wszystkie obietnice. Mój przypadek jest prawdopodobnie dość wyjątkowy, ale pomyślałem, że na wszelki wypadek podzielę się nim.
Stiggler
1
@Joy Otrzymałem ten sam problem z $ state.current.name zwracającym pusty ciąg. Musiałem zamienić $ rootScope. $ Digest () na $ httpBackend.flush (). Po tej zmianie dostałem to, czego się spodziewałem.
Jason Buchanan
18

Jeśli chcesz sprawdzić tylko nazwę bieżącego stanu, jest to łatwiejsze w użyciu $state.transitionTo('splash')

it('should transition to splash', inject(function($state,$rootScope){
  $state.transitionTo('splash');
  $rootScope.$apply();
  expect($state.current.name).toBe('splash');
}));
amatyas001
źródło
4
Dla uproszczenia uważam, że ta odpowiedź jest najbardziej akceptowalna. Posiadanie testu jest fajne iw ogóle, ale konieczność napisania testu dłuższego niż cała moja definicja trasy interfejsu użytkownika tylko w celu przetestowania pojedynczego punktu końcowego jest po prostu nieefektywna. W każdym razie głosuję za tym
Thomas,
15

Zdaję sobie sprawę, że jest to nieco poza tematem, ale przyszedłem tutaj z Google, szukając prostego sposobu na przetestowanie szablonu trasy, kontrolera i adresu URL.

$state.get('stateName')

da tobie

{
  url: '...',
  templateUrl: '...',
  controller: '...',
  name: 'stateName',
  resolve: {
    foo: function () {}
  }
}

w twoich testach.

Więc twoje testy mogą wyglądać mniej więcej tak:

var state;
beforeEach(inject(function ($state) {
  state = $state.get('otherwise');
}));

it('matches a wild card', function () {
  expect(state.url).toEqual('/path/to/page');
});

it('renders the 404 page', function () {
  expect(state.templateUrl).toEqual('views/errors/404.html');
});

it('uses the right controller', function () {
  expect(state.controller).toEqual(...);
});

it('resolves the right thing', function () {
  expect(state.resolve.foo()).toEqual(...);
});

// etc
weltschmerz
źródło
1

Do statetego bez resolve:

// TEST DESCRIPTION
describe('UI ROUTER', function () {
    // TEST SPECIFICATION
    it('should go to the state', function () {
        module('app');
        inject(function ($rootScope, $state, $templateCache) {
            // When you transition to the state with $state, UI-ROUTER
            // will look for the 'templateUrl' mentioned in the state's
            // configuration, so supply those templateUrls with templateCache
            $templateCache.put('app/templates/someTemplate.html');
            // Now GO to the state.
            $state.go('someState');
            // Run a digest cycle to update the $state object
            // you can also run it with $state.$digest();
            $state.$apply();

            // TEST EXPECTATION
            expect($state.current.name)
                .toBe('someState');
        });
    });
});

UWAGA:-

W przypadku stanu zagnieżdżonego może być konieczne dostarczenie więcej niż jednego szablonu. Na przykład. jeśli mamy stan zagnieżdżony core.public.homei każdy state, tj core, core.publici core.public.homema templateUrlokreślone, będziemy musieli dodać $templateCache.put()do każdego państwa templateUrlklucza: -

$templateCache.put('app/templates/template1.html'); $templateCache.put('app/templates/template2.html'); $templateCache.put('app/templates/template3.html');

Mam nadzieję że to pomoże. Powodzenia.

Aakash
źródło
1

Możesz użyć, $state.$current.locals.globalsaby uzyskać dostęp do wszystkich rozstrzygniętych wartości (zobacz fragment kodu).

// Given
$httpBackend
  .expectGET('/api/users/123')
  .respond(200, { id: 1, email: '[email protected]');
                                                       
// When                                                       
$state.go('users.show', { id: 123 });
$httpBackend.flush();                            
       
// Then
var user = $state.$current.locals.globals['user']
expact(user).to.have.property('id', 123);
expact(user).to.have.property('email', '[email protected]');

W ui-router 1.0.0 (obecnie beta) możesz spróbować wywołać $resolve.resolve(state, locals).then((resolved) => {})w specyfikacji. Na przykład https://github.com/lucassus/angular-webpack-seed/blob/9a5af271439fd447510c0e3e87332959cb0eda0f/src/app/contacts/one/one.state.spec.js#L29

luacassus
źródło
1

Jeśli nie interesuje Cię nic w treści szablonu, możesz po prostu mockować $ templateCache:

beforeEach(inject(function($templateCache) {
        spyOn($templateCache,'get').and.returnValue('<div></div>');
}
Jelmer Jellema
źródło