I'm using Mocha to test an asynchronous function that returns a promise.
What's the best way to test that the promise resolves to the correct value?
Mocha has built-in Promise support as of version 1.18.0 (March 2014). You can return a promise from a test case, and Mocha will wait for it:
it('does something asynchronous', function() { // note: no `done` argument
return getSomePromise().then(function(value) {
expect(value).to.equal('foo');
});
});
Don't forget the return keyword on the second line. If you accidentally omit it, Mocha will assume your test is synchronous, and it won't wait for the .then function, so your test will always pass even when the assertion fails.
If this gets too repetitive, you may want to use the chai-as-promised library, which gives you an eventually property to test promises more easily:
it('does something asynchronous', function() {
return expect(getSomePromise()).to.eventually.equal('foo');
});
it('fails asynchronously', function() {
return expect(getAnotherPromise()).to.be.rejectedWith(Error, /some message/);
});
Again, don't forget the return keyword!
Then 'returns' a promise which can be used to handle the error. Most libraries support a method called done which will make sure any un-handled errors are thrown.
it('does something asynchronous', function (done) {
getSomePromise()
.then(function (value) {
value.should.equal('foo')
})
.done(() => done(), done);
});
You can also use something like mocha-as-promised (there are similar libraries for other test frameworks). If you're running server side:
npm install mocha-as-promised
Then at the start of your script:
require("mocha-as-promised")();
If you're running client side:
<script src="mocha-as-promised.js"></script>
Then inside your tests you can just return the promise:
it('does something asynchronous', function () {
return getSomePromise()
.then(function (value) {
value.should.equal('foo')
});
});
Or in coffee-script (as per your original example)
it 'does something asynchronous', () ->
getSomePromise().then (value) =>
value.should.equal 'foo'
Related
This is a rather general question about spyOn when writing unit tests with functions that return promises in a Vue component.
The way I write test is the following:
// Basically this function gets data from a service and sets some data in the component.
function getSomething() {
ServiceX.getSomething().then(response =>
this.x = response.x
)
}
the test:
describe('test', () => {
beforeEach() => {
vm = shallowMount(VueComponent)
spyOn(serviceX, 'getSomething).and.returnValue(promsie.resolve(data));
}
it('should set X', () =>{
vm.getSomething()
expect(vm.X).toBe(X);
})
}
The issue is, when I do the test this way, the variable X is not set yet, but if I do the "it" statement async and await for the getSomething() method it works.
I wonder if there is another way to do this.
Because your original method returns a promise, and your spy is also returning a promise (even an already resolved) you should use the then or the async await as you commented in the question.
So, the other way of doing is:
it('should set X', (done) => {
vm.getSomething().then(() => {
expect(vm.X).toBe(X);
done();
});
})
Using the done parameter from jasmine to notify this unit test is asynchronous and will complete when this callback is called.
i know it's a little bit too late and you probably already found a solution, but in case you didn't i think that the solution i will propose will work for you. You can use the combination of fakeAsync and tick to test your asynchronous function, bellow is my solution.
describe('test', () => {
...
it('should set X', fakeAsync(() =>{
vm.getSomething();
tick();
expect(vm.X).toBe(X);
}));
...
}
I am needing to spyOn window.location.assign for my unit test. But when I run the test I get this error.
Cannot spy the assign property because it is not a function; undefined given instead
Here is my code:
jest.spyOn(window.location, "assign");
Could anyone give me some hints or solutions on this case?
Since Jest v25 (Which uses a newer version of JSDOM) you will get the following error:
TypeError: Cannot assign to read only property 'assign' of object '[object Location]'
This is not a Jest/JSDOM bug by the way. This is normal browser behaviour and JSDOM tries to act like a real browser.
A workaround is to remove the location object, create your own one and after running your tests you should reset it to the original location object:
describe('My awesome unit test', () => {
// we need to save the original object for later to not affect tests from other files
const realLocation = global.location
beforeAll(() => {
delete global.location
global.location = { assign: jest.fn() }
// or even like this if you are also using other location properties (or if TypeScript complains):
// global.location = { ...realLocation, assign: jest.fn() }
})
afterAll(() => {
global.location = realLocation
})
it('should call location.assign', () => {
// ...your test code
expect(global.location.assign).toHaveBeenCalled()
// or even better:
// expect(global.location.assign).toHaveBeenCalledWith('/my_link')
})
})
As window can only be accessed through the global keyword in jest tests and window.location.assign is not implemented in jsdom, you can try
jest
.spyOn(global.location, "assign")
.mockImplementation(url => console.log(url))
I am testing a service method that returns a promise; I want to verify that it catches an error, calls an error-reporting service, and then rethrows the error. I thought that since this error handler was a callback, I should run the catch code in Ember.run, but that is making my tests fail after upgrading from Ember 2.3 to Ember 2.13. Removing the Ember.run fixes the tests but I assume uses the autorun loop, which the guides discourage. What am I doing wrong?
service.js
// .... service boilerplate
doAsyncThing() {
return get(this, 'someService').postV2Request().catch((e) => {
// tests fail unless I remove Ember.run
Ember.run(() => {
let logError = new Error('foobar error');
this.reportError(logError);
throw(e);
});
});
}
// ... service boilerplate
test.js
test('doAsyncThing reports an error if there is one', function(assert) {
assert.expect(3);
let done = assert.async();
let deferred = RSVP.defer();
let apiService = mock({
postV2Request: sinon.stub().returns(deferred.promise)
});
let reportErrorStub = sinon.stub();
let service = this.subject({
apiService,
reportError: reportErrorStub
});
service.doAsyncThing('foo', 'bar', 'baz').catch(() => {
assert.ok(true, 'The exception is rethrown');
assert.ok(reportErrorStub.called, 'reportError was called');
assert.equal(reportErrorStub.args[0][0].message, 'foobar error', 'format of message is ok');
done();
});
deferred.reject();
});
As it turns out, promise callbacks are automatically wrapped in run loops by Ember, so my extra call to run was probably getting scheduled later. More details here https://github.com/emberjs/ember.js/issues/11469 but the TLDR was, the Ember.run was superflous and made Ember think the exception was uncaught due to specifics about Ember's testing mode.
I'm writing an angular2 app and having trouble understanding how to write tests for async code using jasmine. For whatever reason, I'm not seeing a lot of examples that seem terribly applicable to my situation.
I'm currently trying to test a service (not a component) that has a async dependency on another service. I'm not 100% sure at what point in the test it's valid to check the results of the async call. Can you just call expect() inside the async handler for your service?
service.foo()
.then((data) => {
//do I check the results in here?
expect(data).toEqual({ a: 1, b: 2 });
expect(mockDep.get).toHaveBeenCalled();
});
Here is the full test.
import { TestBed, inject } from '#angular/core/testing';
import { MyService } from './my.service.ts';
import { MyDependency } from './dependency.service.ts';
class MockDependency {
doSomething(): Promise<any> {
throw Error('not implemented');
};
}
describe('some tests', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
MyService,
{
provide: MyDependency, useClass: MockDependency
}
]
});
});
});
it('should do something', inject([MyService, MyDependency], (service: MyService, mockDep: MyDependency) => {
spyOn(mockDep, 'doSomething').and.callFake(function () {
return Promise.resolve({ a: 1, b: 2 });
});
service.foo()
.then((data) => {
//do I check the results in here?
expect(data).toEqual({ a: 1, b: 2 });
expect(mockDep.get).toHaveBeenCalled();
});
}));
There's two aspects of dealing with async tests that you have to concern yourself about if you want to ensure your tests are actually reliable.
First, you have to ensure that if a result is retrieved asynchronously, that you wait until the result is available before you try to test for it.
Hence, if the asynchronous result is a promise, for eg, you can put your expect in your then handler, just as you indicated in your question.
The second issue that you have to concern yourself with is forcing your test itself to wait for your expectations to execute before giving a positive (or negative) result. If you don't deal with this, you can have cases where your expectation fails, but because your test did not wait for your asynchronous action to complete before finishing, the test reports a false positive.
There are several ways of 'making your test wait'.
The pure jasmine way is to pass a done handler into your it function. Then jasmine will wait until that done handler is called before considering the test complete.
eg.
it('tests an async action', (done) => {
asyncAction().then(result => {
expect(result).toEqual(true);
done();
});
});
However, angular's testing framework adds two other options to this. The first is easier to grasp if you are comfortable with async programming.
it('tests an async action', async(() => {
asyncAction().then(result => {
expect(result).toEqual(true);
});
}));
in this case, you basically wrap your test handler in an async function. This function will force the test to wait for any async results (eg promises, observables etc) to return a result, before allowing the test to complete.
The second method is to use fakeAsync, which allows you to hide the async nature of your test entirely.
it('tests an async action', fakeAsync(() => {
let myResult;
asyncAction().then(result => {
myResult = result;
});
tick(); <--- force all async actions to complete
expect(myResult).toEqual(true);
}));
fakeAsync hooks into all async functions and allows you to treat them as synchronous. You can use the tick() function to 'force your test to wait' for async tasks to complete before continuing.
(It's not quite doing that, but conceptually, you can think of it that way).
See the Angular docs to learn more
I currently have a working test using Gherkin steps and Jasmine async done() for basic navigation as such:
User_Navigates_To_URL(urlPath: string) {
return this.router.navigateByUrl(urlPath);
}
User_Will_Be_On_URL(expectedPath: string) {
expect(this.location.path()).toBe(expectedPath);
}
it('Scenario: User should be able to navigate', (done) => {
When.User_Navigates_To_URL('/associates/your-team').then(() => {
Then.User_Will_Be_On_URL('/associates/your-team');
});
done();
});
But what I'm trying to accomplish is to write this test using fakeAsync instead of jasmine's done() method. This way all async actions will be resolved in the zone and I wont have to nest the assertion step as a callback of the promise. Therefore, I'm attempting to do something like so:
it('Scenario: User should be able to navigate', <any>fakeAsync(() => {
When.User_Navigates_To_URL('/associates/your-team');
tick();
Then.User_Will_Be_On_URL('/associates/your-team');
}));
After weeks of research, the only thing close I found helpful was this question: Does fakeAsync guarantee promise completion after tick/flushMicroservice. But even when I've tried to implement my navigation promise in his snippet, it never resolves.
User_Navigates_To_URL(urlPath: string) {
let currNav:Promise<Router> = this.router.navigateByUrl(urlPath);
let handleNavigation = function (p:Promise<Router>) {
p.then(() => {
console.log('navigation complete')
});
};
let p = Promise.resolve(currNav);
handleNavigation(p);
tick();
}
This is my first question on here so please let me know if my quesiton is confusing or if I need to provide any more details.