Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Karma test breaks after using ui-router. #212

Closed
wpoosanguansit opened this issue Jun 30, 2013 · 59 comments
Closed

Karma test breaks after using ui-router. #212

wpoosanguansit opened this issue Jun 30, 2013 · 59 comments
Labels

Comments

@wpoosanguansit
Copy link

Hi,

I just tested out ui-router and ran my tests. It shows an error which didn't with normal router:

Error: Unexpected request: GET /views/templates/default-template.html
No more request expected
at Error ()

Is there anything special needed to be setup to work with Karma tests? Thanks for your help.

@nateabele
Copy link
Contributor

This has to do with the fact that the state machine fires off GET requests for templates, but in the test suite, all HTTP requests must be mocked out. The simplest solution would be to add $httpBackend.expectGET("/views/templates/default-template.html")....

@jeme, @ksperling Do we need some kind of integration with ngMock to get this to work out of the box?

@jeme
Copy link
Contributor

jeme commented Jul 1, 2013

@nateabele what may be woth noting that @wpoosanguansit says it worked with the old router, which AFAIK makes the same GET request. So i expect he already knows that, so there must be some subtle difference.

@wpoosanguansit Maybe if you could generate some example data for us to investigate, E.e. couple of plunkers or something. (one with angular own routing and one using ui-router)

@nateabele
Copy link
Contributor

Interestingly, I can't seem to find anything in the ngRoute to bind with ngMock, or vice versa. @wpoosanguansit If you can include your original router code & spec in a plunkr, I'll trace it and see how it handles template requests, so that we can replicate it here.

@nateabele nateabele reopened this Jul 1, 2013
@wpoosanguansit
Copy link
Author

Hi,

I have been trying to put the code in plnkr.co but I still have no clue how to make it run with Karma test. I have posted the code at:

http://plnkr.co/edit/KURAyKztD5a183YxRS1d?p=preview

The error is raised when $digest is called:

scope.$digest();

Error: Unexpected request: GET /views/templates/default-template.html
No more request expected
    at Error (<anonymous>)
    at $httpBackend (/test/vendor/angular-mocks.js:887:9)

Thank you for your help.

@nateabele
Copy link
Contributor

Hi, sorry for not being clear. I was actually looking for your ngRoute equivalent test so I could trace it and see how they handled template loading. Would it be possible for you to post that?

@wpoosanguansit
Copy link
Author

Hi, I hope I understand it correctly. I only have this to handling routing:

$stateProvider
.state('init', {
abstract: true,
controller: 'IndexController'
})
.state('default', {
abstract: true,
templateUrl: '/views/templates/default-template.html'
})
.state('first', {
url: "",
parent: 'default',
views: {
"header": {
templateUrl: '/viewst/default/header.html',
controller: 'IndexController'
},
"content": {
templateUrl: '/views/default/content.html',
controller: 'IndexController'
},
"footer": {
templateUrl: '/views/default/footer.html',
controller: ''
}
}
});

Is this the ngRoute? If there is a sample test case with services in the sample project, it would be much clearer to see how things are setup and what is needed to handle the promised produced by the service. At the moment, I only see the error with the call to $digest, which in my understanding, is needed to resolve the promised received.

Thanks

On Jul 1, 2013, at 11:42 PM, Nate Abele [email protected] wrote:

Hi, sorry for not being clear. I was actually looking for your ngRoute equivalent test so I could trace it and see how they handled template loading. Would it be possible for you to post that?


Reply to this email directly or view it on GitHub.

@nateabele
Copy link
Contributor

By ngRoute I was referring to the code/tests you said you implemented on top of the built-in Angular router.

@wpoosanguansit
Copy link
Author

I must have miscommunicate that I think. I only have the code snippet to handle the routing. And in the test, I did not reference any state at all.

On Jul 2, 2013, at 12:21 AM, Nate Abele [email protected] wrote:

By ngRoute I was referring to the code/tests you said you implemented on top of the built-in Angular router.


Reply to this email directly or view it on GitHub.

@nateabele
Copy link
Contributor

I was referring to this:

I just tested out ui-router and ran my tests. It shows an error which didn't with normal router:

By which I took to mean that you were initially using Angular's built-in $routeProvider. Is that not the case?

@wpoosanguansit
Copy link
Author

I see what you meant now. It was the demo app test created with yeoman that I started out testing angular. I then added hi-router to test and I ran the same test and got the error. But that was resolved when I changed the controller to be not abstract state. And I ran into the error again when I need to call $digest. As I mentioned earlier, I am not sure if that is related. I am sorry for the confusion.

On Jul 2, 2013, at 8:37 AM, Nate Abele [email protected] wrote:

I was referring to this:

I just tested out ui-router and ran my tests. It shows an error which didn't with normal router:

By which I took to mean that you were initially using Angular's built-in $routeProvider. Is that not the case?


Reply to this email directly or view it on GitHub.

@Vratislav
Copy link

I also ran into this issue when I tried to test my ui-router based controllers with karma unit tests. For testing my controllers I implemented a simple $state mock which overrides the state transition (and thus template downloading) completely. The added benefit is that I can verify that correct transitions happen.

Here is the mock snippet (untested):

angular.module('stateMock',[]);
angular.module('stateMock').service("$state", function(){
    this.expectedTransitions = [];
    this.transitionTo = function(stateName){
        if(this.expectedTransitions.length > 0){
            var expectedState = this.expectedTransitions.shift();
            if(expectedState !== stateName){
                throw Error("Expected transition to state: " + expectedState + " but transitioned to " + stateName );
            }
        }else{
            throw Error("No more transitions were expected!");
        }
        console.log("Mock transition to: " + stateName);
    }

    this.expectTransitionTo = function(stateName){
        this.expectedTransitions.push(stateName);
    }


    this.ensureAllTransitionsHappened = function(){
        if(this.expectedTransitions.length > 0){
            throw Error("Not all transitions happened!");
        }
    }
});

My test then looks like:

'use strict';
describe('Controller: LoginCtrl', function () {

    // load the controller's module
    beforeEach(module('myApp'));
    //load the mock module thus overriding the $state service
    beforeEach(module('stateMock'));

    var LoginCtrl,
        scope,
        state;


    // Initialize the controller and a mock scope
    beforeEach(inject(function ($controller,$state, $rootScope) {
        scope = $rootScope.$new();
        state = $state;

        LoginCtrl = $controller('LoginCtrl', {
            $scope: scope
        });
    }));

    it('It should transition to mainScreen state if login is successful', function () {
        state.expectTransitionTo("mainScreen");

        //Other test logic goes here....

        //At the end of the test
        state.ensureAllTransitionsHappened();
    });

});

In my case this is enough to test my ui-router based controllers. Maybe it will be useful to others as well.

@pkieltyka
Copy link

Hello, I'm just wondering what the status of this is? ui-router gets in the way of doing $httpBackend tests on services. Besides just the templateUrl stuff which is easy to pass through, if you do a resolve in a state provider that does an http call, then you're screwed.. I feel this is something that should be solved and packaged with ui-router. Thoughts?

@pkieltyka
Copy link

I just created a uiRouterNoop module that I inject in specific tests that I do not want the uiRouter to be triggered.. seems to work well.

// uiRouterNoop.js helper module
mod = angular.module('uiRouterNoop', []);
mod.service('$state', function() { return {} });
mod.service('$urlRouter', function() { return {} });

// in a spec where $state and $urlRouter should be skipped -- ie. a http model
beforeEach(module('uiRouterNoop'));

@nateabele
Copy link
Contributor

That seems pretty overly-simplistic, but if you have any suggestions, I'm all ears.

@pkieltyka
Copy link

Yea it's definitely too simplistic, but when testing just model code that only interacts with some backend then ui-router can be noop'ed.

I'm actually not sure what the best thing to do is.. it's correct for ui-router to be executing that code, but I think having more control over when that code is run so someone can anticipate the calls and then test for them.. ie. generally, how would someone write tests for functions that are defined in the config of providers?

.. it's definitely not easy to do

On 2013-09-19, at 3:25 PM, Nate Abele [email protected] wrote:

That seems pretty overly-simplistic, but if you have any suggestions, I'm all ears.


Reply to this email directly or view it on GitHub.

@nateabele
Copy link
Contributor

Well, requests for templates and resolves shouldn't actually be fired until you call $state.transitionTo(), then $rootScope.$apply().

@jwall
Copy link

jwall commented Oct 25, 2013

If you are testing a promise then to kick it off you need to call $digest or $apply, and this is when there can be issues. For me so far @Vratislav solution works to avoid the exceptions and has the added benefit of transition verification.

@biofractal
Copy link

Exactly as @jwall says, if you are mocking out promises then the necessary call to $digest triggers the unexpected template load.

Personally I am happy to simply expect the call like so:

$http.whenGET('template/login.htm').respond {}

@nateabele
Copy link
Contributor

Yeah, there's definitely some stuff we could do to make testing more fluid, but it's hard to have a one-size-fits-all solution like mocking out template-handling, when some people might be writing integration tests that depend on templates loading.

@wilsonwc
Copy link

I updated the state mock from @Vratislav to support returning a promise (so you can test $state.go('state2').then()) and aliased $state.go to point to $state.transitionTo

https://gist.github.com/wilsonwc/8358542

A question has also been answered on SO for this issue and how to implement the above gist:
http://stackoverflow.com/questions/23655307/ui-router-interfers-with-httpbackend-unit-test-angular-js/23670198?noredirect=1#comment36365838_23670198

@nateabele
Copy link
Contributor

@wilsonwc Nice! I starred your gist and will see about integrating it into the main library.

@olivierpichon
Copy link

It is worth noticing that we don't get this Error: Unexpected request: GET /views/... in our karma tests when we don't declare any run loop in our routes.

However, as soon as the run loop is declared:
.run(['$state', '$rootScope', ...

Those errors are raised. Is there any plan to fix this rather than mocking each and every request to get those templates?

@nateabele
Copy link
Contributor

@olivierpichon Yup, would love to. Not sure when it'll happen though. Let me know if you'd like to submit a pull request in the meantime.

@rramsden
Copy link

@olivierpichon Having same problem. Removing $state from my run blocks removes the test errors

@olivierpichon
Copy link

@rramsden, thanks mate!
I needed the $state to do a redirection, I used the lower level $window.location.href instead.

@nateabele I'd love to when I'll get to know the codebase better.

@kencaron
Copy link

Thanks a ton to @wilsonwc @Vratislav. After some wrangling I was able to figure this out, but definitely would have been lost if I didn't find this issue.

@nateabele What are the odds that stateMock will get integrated into UI.Bootstrap, or better yet, Karma tests showcased in the sample app? I think it'd be a great resource for those of us new to TDD.

@faisalferoz
Copy link

same issue. pkieltyka's solution works.

@christopherthielen
Copy link
Contributor

Those of you experiencing this issue, please add $urlRouterProvider.deferIntercept() to your test initialization code and report back.

@danse
Copy link

danse commented Jan 13, 2015

That does not seem to help in my case

@christopherthielen
Copy link
Contributor

@danse when you run $digest(), what url is requested? Is it your app's default state's template?

@christopherthielen
Copy link
Contributor

Initial State

When the $state service is loaded, UI-Router tries to load the default route, and part of that process is fetching the default route's template. If you are getting an unexpected GET for one of the templates of your default route, you should tell UI-Router to defer intercept of the URL. This stops UI-Router from trying to synchronize the URL to the state, thus skipping loading the default route/state.

beforeEach(module(function($urlRouterProvider) {
  $urlRouterProvider.deferIntercept();
})));

Tests involving state transitions

If your unit tests involve ui-router transitions, UI-Router will attempt to fetch the templates required for the transition. You will need to either register each template fetch with the $httpBackend mock, or pre-load all your templates into $templateCache.

To register a template fetch with $httpBackend mock:

beforeEach(function($httpBackend) {
  $httpBackend.expectGET("templates/templateUrl.html").respond("<div>mock template</div>");
}));

To preload your templates, use a karma pre-loading tool such as the one @ghost linked in this comment: #212 (comment)

If the templates are preloaded into the template cache by such a tool, $httpBackend is never invoked by UI-Router when it requests the template, and your state can be transitioned to in a test-friendly synchronous manner.

To preload your templates manually, do something like the following:

beforeEach(function($templateCache) {
  $templateCache.put('templates/dashboard.html','<div>blank or whatever</div>');
}));

Ionic Users

I've recently discovered that ionic preloads all UI-Router templates into Ionic's own template cache service. Disable that in your tests by either preloading, as mentioned above, or by disabling the $ionicTemplateCache service.

To disable the $ionicTemplateCache service, set the maximum number of templates to 0:

beforeEach(module(function($ionicConfigProvider) {
  $ionicConfigProvider.templates.maxPrefetch(0);
})));

If that doesn't work, nuke the $ionicTemplateCache service by stubbing it out:

  beforeEach(module(function($provide) {
    $provide.value('$ionicTemplateCache', function(){} );
  }));

@eve-amandine
Copy link

It's maybe because I'm a noob with unit tests but it still not working for me.

I've tried adding the code @christopherthielen commented but I've the same error:
Error: Unexpected request: GET modules/onboarding/signup-A.html No more request expected
(it's the template linked with the controller I'm testing)

I use ionic with the ui-router and when I put a $scope.digest() (for testing promises), I've this error even if I quit the $state of the controller (and I don't have any action with ui-router in the controller). In addition, with the promise I don't use $httpBackend nor other things (the service mocked with promise doesn't call an Api).

The only thing it's working is when I add this in the inject function:
$httpBackend = $injector.get('$httpBackend'); $httpBackend.expectGET('modules/onboarding/signup-A.html').respond(200);

But I would like to find a solution that works "automatically" without adding these lines in each unit tests controllers. Any idea?

@ltowarek
Copy link

Let's try the following lines as they work for me (ionic v1.0.0-beta.14):

  beforeEach(module(function($provide) {
    $provide.value('$ionicTemplateCache', function(){} );
  }));

  beforeEach(module(function($urlRouterProvider) {
    $urlRouterProvider.deferIntercept();
  }));

@eve-amandine
Copy link

Thanks!!!! Finally it works! :D
I had been trying that but it was added in an incorrect order.
For the noobs like me, you need to put these lines in this order: beforeEach with your controller's module - these lines commented by @ltowarek - beforeEach with the inject.

@thebigredgeek
Copy link

Seems a bit ridiculous that the Ionic team didn't even consider testing when writing that. Only the nuking method worked for me.

@brentonmcs
Copy link

Thanks - @christopherthielen - your Ionic section fixed up the issues I was having!!!

@gruppjo
Copy link

gruppjo commented Jul 2, 2015

@ghost, @christopherthielen thanks. That worked brilliantly!

@eddiemonge
Copy link
Contributor

@dchacke
Copy link

dchacke commented Aug 12, 2015

+1 @christopherthielen

beforeEach(module(function($urlRouterProvider) {
  $urlRouterProvider.deferIntercept();
}));

does the trick for me. Also, thanks for the explanation. It's good to know that $state attempts to load the default route's template, and that this is why you would get unexpected GET requests in your tests.

shanghaiese pushed a commit to shanghaiese/new-ui-0911 that referenced this issue Nov 2, 2015
@jakub-g
Copy link

jakub-g commented Nov 20, 2015

Ionic + ng-describe

We're using ionic and ng-describe and nothing provided here so far worked directly for us, but I managed to find a solution based on @christopherthielen answer in #212 (comment)

When using beforeEach(module(function(...))) along the tests (it) we were getting Injector already created, can not register a module! error
(beforeEach alone works fine, but it seems you can not use module inside tests function defined by ng-describe, because this is too late in the lifecycle).

This worked for us

angular.module('IonicTemplateCacheDisabler', [])
  .config(function($provide) {
    $provide.value('$ionicTemplateCache', function() {});
  })
  .run(function($ionicConfig) {
    $ionicConfig.templates.maxPrefetch(0);
  });

ngDescribe({
  name: '...',
  modules: [... 'IonicTemplateCacheDisabler'],
  inject: [...],
  tests: function(deps) {
    it('...', function(){...})
    it('...', function(){...})
    it('...', function(){...})
  }
});

This way IonicTemplateCacheDisabler's config and run are invoked before each of tests, just as if beforeEach() was declared.

@LukeSkyw
Copy link

@christopherthielen's solution does not work for me. The GET request keeps coming are met with 404 continuously until a 5 seconds timeout occurs:

16 01 2016 19:55:49.350:WARN [web-server]: 404: /header.html
16 01 2016 19:55:49.355:WARN [web-server]: 404: /footer.html
16 01 2016 19:55:49.359:WARN [web-server]: 404: /header.html
16 01 2016 19:55:49.367:WARN [web-server]: 404: /footer.html
16 01 2016 19:55:49.367:WARN [web-server]: 404: /header.html
16 01 2016 19:55:49.451:WARN [web-server]: 404: /footer.html
16 01 2016 19:55:49.455:WARN [web-server]: 404: /header.html
PhantomJS 1.9.8 (Linux 0.0.0) LoginService shall call login on the server and resolve the promise FAILED
        Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.
PhantomJS 1.9.8 (Linux 0.0.0): Executed 21 of 31 (1 FAILED) (0 secs / 5.176 secs)
16 01 2016 19:55:49.465:WARN [web-server]: 404: /header.html
16 01 2016 19:55:49.468:WARN [web-server]: 404: /footer.html

@ErikAGriffin
Copy link

While I am able to resolve the issue of GET requests via the nghtml2js preprocessor, I'm wondering, what is the best way to test state transitions? Is using the stateMock above the only way?

The example code on this website this website shows the use of $state.go and $state.current in a test, but for me this does not work. $state.go seems to have no effect and the $rootScope.$digest() always loads the default state into $state.current.

Code:

    // Test whether our state activates correctly
    it('should activate the state', function() {
        $state.go('state');
        $rootScope.$digest();
        expect($state.current.name).toBe('state');
    });

@guylhermetabosa
Copy link

Thanks a lot @Vratislav. You saved my day.

mleanos added a commit to mleanos/mean that referenced this issue Oct 11, 2016
Fixes the client-side tests after the removal of the <base/> tag from
the main layout.

These fixes aren't ideal. At the moment, they will suffice. This comment
(angular-ui/ui-router#212 (comment)),
among others in that issue, led me to choose this method as the fix to
avoid having to change any other core code.
@PRossetti
Copy link

PRossetti commented Apr 24, 2017

Hello everybody! I solved it using @jdart solution, but I'd like to implement @christopherthielen once since it's the one with more votes.
My problem is that I don't understand how to implement it in my code. When I literally copy & paste the next in my unit test code:

beforeEach(module(function($urlRouterProvider) {
  $urlRouterProvider.deferIntercept();
}));

I get this error:

Error: Injector already created, can not register a module! in www/lib/angular-mocks/angular-mocks.js (line 2256)
workFn@www/lib/angular-mocks/angular-mocks.js:2256:80

And when I try to inject $urlRouterProvider in my working beforeEach() block:

beforeEach(inject(function('service1','service2', 'etc', $urlRouterProvider) {
  //myStaff
  $urlRouterProvider.deferIntercept();
}))

I get this other error:

Error: [$injector:unpr] Unknown provider: $urlRouterProviderProvider <- $urlRouterProvider
http://errors.angularjs.org/1.5.3/$injector/unpr?p0=%24urlRouterProviderProvider%20%3C-%20%24urlRouterProvider (line 17765)
www/lib/ionic/js/ionic.bundle.js:17765:86
getService@www/lib/ionic/js/ionic.bundle.js:17918:46
www/lib/ionic/js/ionic.bundle.js:17770:48
getService@www/lib/ionic/js/ionic.bundle.js:17918:46
injectionArgs@www/lib/ionic/js/ionic.bundle.js:17942:68
invoke@www/lib/ionic/js/ionic.bundle.js:17964:31
workFn@www/lib/angular-mocks/angular-mocks.js:2404:26

Could you help me please? Thank you!!

@Atul-sac
Copy link

Atul-sac commented Dec 3, 2017

Hi,
I am using $routeProvider and i have started facing same issue from 10 days back. same test cases were working before approx 10 days, but when i did fresh installation of all dependencies mentioned in package.json. my all test cases started failing which are dependent on $http service call and also which are based on $watch (basically where $apply() is called manually).
I am getting basically these two types of error -

  1. Error: Unexpected request: GET templates/home.html No more request expected
  2. Error: Unexpected request: GET API_URL No more request expected

It seems that few wrong commits has been made recently on KARMA/JASMINE, which are causing these issues, as mine old code base is working fine, only after installing fresh dependencies these test cases are failing now.

I am using below dependencies -

"devDependencies": {
"angular": "^1.6.5",
"angular-mocks": "^1.6.5",
"angular-route": "^1.6.5",
"browserify-istanbul": "^2.0.0",
"grunt": "^1.0.1",
"grunt-browserify": "^5.0.0",
"grunt-contrib-connect": "^1.0.2",
"grunt-contrib-copy": "^1.0.0",
"grunt-contrib-jshint": "^1.1.0",
"grunt-karma": "^2.0.0",
"istanbul": "^0.4.5",
"jasmine-core": "^2.6.4",
"karma": "^1.7.0",
"karma-bro": "^0.11.1",
"karma-browserify": "^5.1.1",
"karma-chrome-launcher": "^2.2.0",
"karma-coverage": "^1.1.1",
"karma-jasmine": "^1.1.0",
"karma-phantomjs-launcher": "^1.0.4"
}

Please help me out.
Thanks!

lupinthethirdgentleman pushed a commit to lupinthethirdgentleman/mean-dashboard that referenced this issue Aug 5, 2021
Fixes the client-side tests after the removal of the <base/> tag from
the main layout.

These fixes aren't ideal. At the moment, they will suffice. This comment
(angular-ui/ui-router#212 (comment)),
among others in that issue, led me to choose this method as the fix to
avoid having to change any other core code.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests