Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Jasmine 2.1 beforeAll/afterAll methods cause errors #10238

Closed
chesleybrown opened this issue Nov 26, 2014 · 36 comments
Closed

Jasmine 2.1 beforeAll/afterAll methods cause errors #10238

chesleybrown opened this issue Nov 26, 2014 · 36 comments

Comments

@chesleybrown
Copy link

Jasmine has released beforeAll and afterAll methods with 2.1 which allow executing a before function only once for all the following specs. Angular mocks doesn't seem to play nice with this concept.

When using beforeAll or afterAll methods, I encounter the following errors.

TypeError: 'null' is not an object (evaluating 'currentSpec.$modules')
        at workFn (/Users/chesleybrown/Sites/bln-web/app/components/angular-mocks/angular-mocks.js:2323)
        at /Users/chesleybrown/Sites/bln-web/node_modules/karma-jasmine/lib/boot.js:71
        at /Users/chesleybrown/Sites/bln-web/node_modules/karma-jasmine/lib/adapter.js:174
        at http://localhost:9876/karma.js:185
        at http://localhost:9876/context.html:168
    TypeError: 'null' is not an object (evaluating 'currentSpec.$modules')
        at workFn (/Users/chesleybrown/Sites/bln-web/app/components/angular-mocks/angular-mocks.js:2323)
        at /Users/chesleybrown/Sites/bln-web/node_modules/karma-jasmine/lib/boot.js:71
        at /Users/chesleybrown/Sites/bln-web/node_modules/karma-jasmine/lib/adapter.js:174
        at http://localhost:9876/karma.js:185
        at http://localhost:9876/context.html:168
    Expected { 0: HTMLNode, length: 1, prevObject: { 0: HTMLNode, length: 1 }, context: undefined, selector: '.loading-indicator' } to have class 'ng-hide'.
    Error: Expected { 0: HTMLNode, length: 1, prevObject: { 0: HTMLNode, length: 1 }, context: undefined, selector: '.loading-indicator' } to have class 'ng-hide'.
        at /Users/chesleybrown/Sites/bln-web/test/directives/about-product.directive.spec.js:40
        at /Users/chesleybrown/Sites/bln-web/node_modules/karma-jasmine/lib/boot.js:71
        at /Users/chesleybrown/Sites/bln-web/node_modules/karma-jasmine/lib/adapter.js:174
        at http://localhost:9876/karma.js:185
        at http://localhost:9876/context.html:168
@chesleybrown
Copy link
Author

I posted this bug on Jasmine as well because I wasn't sure which code was in the wrong: jasmine/jasmine#714

They replied with this:

Because a beforeAll runs outside any particular spec context, there probably isn't going to be a currentSpec object that they'll be able to reference.

Closing. It looks like this is probably an issue with angular and/or angular-mocks and not jasmine itself.

@shahata
Copy link
Contributor

shahata commented Nov 28, 2014

@chesleybrown it seems to me that you are trying to do something like:

beforeAll(inject(function ($rootScope) {
}));

If this is the case, then this is the expected behavior since angular needs to create a new injector in each test. I don't think we want to be able to have a single injector for all the tests in a suite. I guess it can be done, but I don't think we should unless you have a good use case for this...

(of course I'm just assuming that this is what you did from looking at the stack trace and it is possible you did something else entirely :D)

@lgalfaso
Copy link
Contributor

After two months and no feedback, I made the assumption that the issue is what @shahata commented. Closing this, but if there is anything else that needs to be handled, then feel free to reopen the issue

@scriby
Copy link

scriby commented Feb 11, 2015

The use case I have for using beforeAll looks something like this:

describe('some expensive to create object', function() {
  var object;

  beforeAll(function() {
    object = createExpensiveObject();
  });

  it('has property A', function() {
    expect(object).toHavePropertyA();
  });

  it('has property B', function() {
    expect(object).toHavePropertyB();
  });

  ...
});

I like being able to split up the assertions into more granular chunks instead of just listing them one after another in a single it block.

I think this is still achievable with beforeEach (just initialize object if it's null), it's just not quite so straightforward.

@xml
Copy link

xml commented Jul 8, 2015

So... is the bottom line that Angular doesn't support Jasmine's beforeAll, and developers should avoid it? In my case, I spent an hour chasing unclear errors, only to eventually track down this issue...

@lgalfaso
Copy link
Contributor

lgalfaso commented Jul 8, 2015

It is not possible to use the injector at beforeAll as there is no cross-application nor cross multiple bootstrap injector. If there is some need to make Jasmine work in a different way, I would recommend you to open an issue at the karma-jasmine plugin repo

@timruffles
Copy link
Contributor

@lgalfaso not sure why this would be an Jasmine issue? Jasmine's beforeAll (and mocha's before) works fine, it's the implementation of angular-mocks that stops it working for angular tests.

As @xml said, seems a shame to talk about Jasmine as the most popular framework in the docs, and not support a very core feature for writing good tests in the style @scriby demonstrated.

@lgalfaso
Copy link
Contributor

@timruffles each test must have its own clean $injector. This is the only way to warranty that the right configuration is present for each test. This implies that anything that needs an injector must be executed once per test.
Jasmine beforeAll syntax is very powerful, but given that it runs once for all tests, then it cannot have access to the $injector (and subsequently it cannot be used in conjunction with inject).

Loose comments stating this is a shame do not help. The project karma-jasmine adds some functionality to Jasmine to make it possible for it to be used within karma to test Angular apps. My comment above does not claim that there is a need to change how Jasmine works, it is a great library that works in the expected way. That does not mean that all the functionality provided by Jasmine can be used with the functionality provided by karma-jasmine. One of the limitation is that you do not have access to $injector at beforeAll. If you find a solution to this problem, then open a PR on karma-jasmine and someone will review it.

@timruffles
Copy link
Contributor

@lgalfaso sometimes code is a better way to explain. I'm a GDE so I'd be happy to get started with contribution by productionizing this implementation, rather than just doing talks :)

Here is a repo which demonstrates how it is possible to support this functional changing only ngMock. This demonstrates it's ngMock's jasmine/mocha support that is the limiting factor here.

Rather than tying injector state to currentSpec, a currentContext is created on demand, and cleared on demand.

As you can see, it supports beforeEach/beforeAll and the standard use of injectors.

@lgalfaso
Copy link
Contributor

@timruffles can you make your variation of ngMock work with this simple currently working example

describe('something', function() {
  beforeEach(module(function($interpolateProvider) {
    $interpolateProvider.startSymbol('[[');
    $interpolateProvider.endSymbol(']]');
  }));

  it('should interpolate using [[ ]]', inject(function($interpolate) {
    expect($interpolate('[[foo]]')({foo: 'bar'})).toBe('bar');
  }));
});

describe('something else', function() {
  it('should interpolate using {{ }}', inject(function($interpolate) {
    expect($interpolate('{{foo}}')({foo: 'bar'})).toBe('bar');
  }));
});

@timruffles
Copy link
Contributor

Thanks for the example, yes I can! I added opt in so you can selectively turn on the new behaviour for given tests. Your example passes without modification, I updated my test with the opt in beforeAll(module.enableManualInjector).

Run karma start modifiedNgMock.conf.js to see your tests passing successfully, alongside tests using beforeAll.

@lgalfaso
Copy link
Contributor

Good, I think we are going somewhere, but at the end the main point is not clear.

  1. Existing test, just like the one in the example, must work unmodified
  2. Adding beforeAll(inject(...)) should work with the existing examples

This is, the following code should work

beforeAll(inject(function($http) {}));

// Paste here the previous example code

@timruffles
Copy link
Contributor

Great.

  1. This is achieved - your example works unmodified.
  2. beforeAll(inject(function($http) {})) doesn't work with the current version of angular/angular-mock. [1]

I'm not sure what the intention is in supporting inject() without module()? The use cases people are asking about above, and that the changes support, are all about using beforeAll() with module() then inject(), e.g:

describe('some expensive to create object', function() {
  var object;
  beforeAll(module("someModule"))

  beforeAll(inject(function(createExpensiveObject) {
    object = createExpensiveObject();
  }));

  it('has property A', function() {
    expect(object).toHavePropertyA();
  });

  it('has property B', function() {
    expect(object).toHavePropertyB();
  });

});

[1] e.g,

beforeAll(inject(function($http) {}));

describe('something', function() {
  beforeEach(module(function($interpolateProvider) {
    $interpolateProvider.startSymbol('[[');
    $interpolateProvider.endSymbol(']]');
  }));

  it('should interpolate using [[ ]]', inject(function($interpolate) {
    expect($interpolate('[[foo]]')({foo: 'bar'})).toBe('bar');
  }));
});

describe('something else', function() {
  it('should interpolate using {{ }}', inject(function($interpolate) {
    expect($interpolate('{{foo}}')({foo: 'bar'})).toBe('bar');
  }));
});

throws this with the current master version of angular-mock:

Chrome 46.0.2490 (Mac OS X 10.9.5) something should interpolate using [[ ]] FAILED
    TypeError: Cannot read property '$modules' of null
        at Object.workFn (/Users/timruffles/dev/oss/angular-jasmine-with-before-all/node_modules/angular-mocks/angular-mocks.js:2413:32)
Chrome 46.0.2490 (Mac OS X 10.9.5) something else should interpolate using {{ }} FAILED
    TypeError: Cannot read property '$modules' of null
        at Object.workFn (/Users/timruffles/dev/oss/angular-jasmine-with-before-all/node_modules/angular-mocks/angular-mocks.js:2413:32)

@timruffles
Copy link
Contributor

In case you're reading in email, I edited the comment above with an example of the use cases for beforeAll().

@timruffles
Copy link
Contributor

@lgalfaso I'm up for writing this up into a PR if we're agreed on beforeAll support via opt-in.

@lgalfaso
Copy link
Contributor

I was not able look at your approach, should have some time tomorrow. That
said, I am not fond on having an opt-in behavior

On Saturday, October 31, 2015, Tim Ruffles [email protected] wrote:

@lgalfaso https://github.com/lgalfaso I'm up for writing this up into a
PR if we're agreed on beforeAll support via opt-in.


Reply to this email directly or view it on GitHub
#10238 (comment)
.

@lgalfaso
Copy link
Contributor

@timruffles I am reading your comment on the proposal and I am not sure how you are proposing this to work. Let me write down what my understanding on your proposal is, and if this is not correct, please write the comprehensive idea.

When there is a beforeAll that makes use of $injector then all the tests in the describe context that beforeAll is, will share a unique $injector. If there is a global beforeAll that makes use of $injector, then all the tests share this unique $injector.

Eg.

describe('X', function() {
  // Here, each test has its own $injector.
  it('X1', inject(function(...) {}));
  it('X2', inject(function(...) {}));
  it('X3', inject(function(...) {}));
});

describe('Y', function() {
  // Here, all test share the same $injector.
  beforeAll(inject(function(...) {}));

  it('Y1', inject(function(...) {}));
  it('Y2', inject(function(...) {}));
  it('Y3', inject(function(...) {}));
});

@timruffles
Copy link
Contributor

Really it's not tied to beforeAll at all! Instead we allow users to control when an injector is created, which will enable a lot of good testing practises currently not supported.

The implementation is exposing a enableManualInjector switch. This happens to works very well with beforeAll(), and supports the typical use cases for beforeAll() that people were discussing above:

  • clean tests: one action, many expectations
  • speeding up slow tests, where one expensive action is taken and many assertions are made about it

A complete use-case would be:

describe('some expensive to create object', function() {
  var object;

  beforeAll(module.enableManualInjector)
  beforeAll(module("someModule"))

  afterAll(module.disableManualInjector)

  beforeAll(inject(function(createExpensiveObject) {
    object = createExpensiveObject();
  }));

  it('has property A', function() {
    expect(object).toHavePropertyA();
  });

  it('has property B', function() {
    expect(object).toHavePropertyB();
  });

});

@lgalfaso
Copy link
Contributor

lgalfaso commented Nov 3, 2015

@timruffles I think this conversation is not moving forward. Exposing a test configuration that has global side-effects and that depends on the good will of developers to undo this configuration, is a bad practice. Test should be isolated and independent by default and must be impossible for some configuration in another file, left behind by some sloppy developer or just because some other test did (not) run before to affect the stability or result of any other test that did not explicitly opted-in.
This is about the minimum that anybody expects from a testing library.

Now, there is a need to allow beforeAll to perform some expensive operation and for the result of this expensive operation to be shared between multiple tests. The result of allowing this would improve the speed that tests are executed. This is, allowing a group of tests to be coupled and interdependent with each other to gain some speed. Anyhow, this behavior must not affect any other test, and this must be regardless if the developer added afterAll(module.disableManualInjector()) or not.

Any proposal must follow these constraints. If you are willing to work on a PR within these constraints, then I am ok in continuing with this discussion. If you still think that following these constraints are not what you have in mind, then I would feel more comfortable with some other core member team to continue on my behalf.

@timruffles
Copy link
Contributor

Great points. I've updated the solution to expose only one method: module.sharedInjector(). This shares an injector for the current describe block only - leaving the rest of the suite unaffected.

That avoids the danger you mentioned where other tests would be affected, and doesn't require developers to manually cleanup.

Please let me know if there are other constraints I've missed. Otherwise I'll take a look at that PR.

describe('allowing shared injectors, without affecting other tests', function() {

  describe('tests with sharedInjector', function() {
    // within here, we've got 1 injector across all beforeEach, tests etc
    module.sharedInjector();

    beforeAll(module("main", function($provide) {
      $provide.value("stubMe", {
        service: "stubbed",
      })
    }))
  })

  describe('tests with isolation', function() {
    // within here, we've got isolated injectors again

    beforeEach(module("main", function($provide) {
      $provide.value("stubMe", {
        service: "stubbed",
      })
    }))

  })
});

@timruffles
Copy link
Contributor

@lgalfaso hopefully I've addressed your points above? Was just reminded of this work by this related issue on karma-mocha being closed.

@lgalfaso
Copy link
Contributor

@timruffles sorry for taking this long to answer, I know I still have to look into this. At the API level, I think this is fine, but did not have the time to look at the code/test/docs. Hopefully will have some time next week

@petebacondarwin
Copy link
Contributor

Hello @timruffles and @lgalfaso - I am late to this discussion but have just popped in.
It seems to me that it would be better if we could just get beforeAll(inject(...)) working by itself without the developer having to "know" about the manualInjector switch. This would follow the path of least surprise and be a better experience for developers in general.
Can we not identify if inject was called in beforeAll and if so then create a shared context for all the it clauses but also automatically define an afterAll call to remove this shared injector at the end of the tests?

@petebacondarwin petebacondarwin modified the milestones: 1.6.x, Backlog Nov 23, 2015
@gkalpak
Copy link
Member

gkalpak commented Nov 23, 2015

It would still be a big surprize if all of a sudden my it blocks start having a shared injector.
So, if all this is happening behind the scenes, it should be very clearly documented imo.

@timruffles
Copy link
Contributor

Good point @gkalpak, but as before()/beforeAll() (mocha/jasmine) are both currently broken when used with module(), I don't think there can be any existing users to be surprised.

Very clear documentation is absolutely important tho! I'll update the docs at the same time in the PR.

@petebacondarwin
Copy link
Contributor

@gkalpak it would only be all of a sudden if you actually had a beforeAll(inject(...)) block already defined.
How about banning the use of inject(...) if a shared injector has been defined already? A clear error message (e.g. "You cannot use inject here as a shared injector has already been defined in a previous beforeAll block). That would make it very clear to the programmer and would minimize surprise.
The developer using beforeAll would then be responsible for moving services into closure variables to ensure they were available to other beforeEach and it blocks, which I think would be reasonable compromise.

@gkalpak
Copy link
Member

gkalpak commented Nov 23, 2015

"all of a sudden" was not in the sense of a BC, but more like:

  1. I've been writing test in Angular for several months. Everything works fantastic. I know that a new injector is created for every it block, so I don't have to clear up after each.
  2. Aha, new shiny beforeAll feature. Let's put that into the mix.
  3. Ooops, tests break, because of dirty state left over form previous tests.

Banning the use of inject(...) won't solce this issue I think.

All I'm saying is that going from "1 injector per block" to "1 injector for all blocks" is not explicitly clear just because I used a different method and it should be clearly documented (be it one the dev guide, the changelog or wherever appropriate).

@petebacondarwin
Copy link
Contributor

@gkalpak Do you have any suggestions to improve the situation?

@gkalpak
Copy link
Member

gkalpak commented Nov 23, 2015

All I am saying is, if we will have it work automagically (i.e. without the user's having to opt-in with something like the manualInjector switch) - which is fine by me btw - we should clearly document the implications of using beforeAll(), e.g. in the Unit Testing section of the guide and also mention them in the commit message, so it is also mentioned in the changelog.
(I.e. not just say: Yay, added support for beforeAll(), but something like Yay, added support for beforeAll(). If you use it, this and that will happen.)

That's all 😃

@petebacondarwin
Copy link
Contributor

How about an explicit API: beforeAll(sharedInject(...));?

@gkalpak
Copy link
Member

gkalpak commented Nov 23, 2015

It sounds pretty good to me. (But you still have to document it - there's no escaping that 😛)

@timruffles
Copy link
Contributor

beforeAll(module.sharedInjector("someModule", function($provide) {})) certainly works for me as an API!

There are decisions to be made about interacting with non-shared injector code. I think for safety it's best to disallow this:

describe
  beforeEach1 module
  T1
  describe 
     beforeAll1 module.sharedInjector
     T2

This could technically work, as we'd see the following calls:

beforeEach1, T1
beforeAll1, beforeEach1, T2

However, I imagine a lot of people will get into nasty situations where their beforeEach's in outer contexts assume they always have an isolated injector. The ordering of the beforeAll means that wouldn't be the case, and the same beforeEach code would run in isolated and shared contexts.

So for safety I think we should only support beforeAll(module.sharedInjector in a clean context. There's no way to opt out of it within a context, so you can't nest isolated within shared. e.g

// ok
beforeAll module.sharedInjector
describe
  beforeEach inject()

// not ok
beforeEach inject()
describe
  beforeAll module.sharedInjector

As desiring a shared injector is probably the exception rather than the rule, this restriction seems ok!

If this looks ok with you @petebacondarwin, I'll rebase my work into a PR.

petebacondarwin pushed a commit that referenced this issue Mar 1, 2016
Allow to opt-in to using a shared injector within a context. This allows  hooks to be
used in Jasmine 2.x.x/Mocha

Closes #14093
Closes #10238
petebacondarwin pushed a commit that referenced this issue Mar 1, 2016
Allow to opt-in to using a shared injector within a context. This allows  hooks to be
used in Jasmine 2.x.x/Mocha

Closes #14093
Closes #10238
@bluecollarcoder
Copy link

Just ran into this problem. Was this issue resolved with v1.6.1?

@timruffles
Copy link
Contributor

See the docs here on how to use beforeAll().

@bluecollarcoder
Copy link

@timruffles Thanks!

@gkalpak
Copy link
Member

gkalpak commented Jan 11, 2017

(This has been fixed since v1.5.1 btw.)

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

10 participants