Code example
This is probably better explained in code, so I have included a detailed, abstracted example below - where my question is repeated in the comments.
Example Summary
For example, you mock out a reasonably complex mock service, and in the first test you want the mock service to give a positive result for one function (by return code 200 in the example below). In the second test, you want the same function to return a negative result, say 500.
I found that I can simply inject the provider and overwrite the method, but is that the correct way? See "test 2" below.
describe('CommentArea', function() {
var controller
beforeEach(function() {
module('app', function($provide) {
// Base mock definition
$provide.provider('comments', function() {
this.$get = function() {
return {
create: function() {
return new Promise(function(resolve) {
resolve({ code: 200 })
})
},
delete: function() {
return new Promise(function(resolve) {
resolve({ code: 200 })
})
}
}
}
})
})
inject(function($componentController) {
controller = $componentController('CommentArea')
})
})
// Start tests
//
// Test 1 - Use the above mocked service "as is" - happy path scenario
//
it('appends comment when successful', function() {
expect(controller.commentCount).toEqual(0)
controller.create() // uses the comments service create method
expect(controller.commentCount).toEqual(1)
})
// Test 2 - Use the above mocked service with a slight modification
// This time the create function will fail
//
it('does not append comment when unsuccessful', inject(function(comments) {
// overwrite for failure condition here
// QUESTION - is this acceptable? It seems odd just overwriting it
// so directly
comments.create = function () {
return new Promise(function(resolve) {
resolve({ code: 500 })
})
}
expect(controller.commentCount).toEqual(0)
controller.create() // uses the comments service create method
expect(controller.commentCount).toEqual(0)
}))
})
Usually this means that mocked return values shouldn't be defined in beforeEach.
One of the ways is to make functions return local variables that are defined in common function scope (describe block):
var createValue;
...
beforeEach(module('app', { comments: {
create: function () { return createValue }, ...
}))
...
createValue = Promise.resolve(200);
comments.create();
Local variables have a downside. If createValue wasn't redefined by mistake in next test, tests may become cross-contaminated.
Moreover, this kind of mocks doesn't solve an important task; mocked functions aren't spied.
This is exactly the task that Jasmine spies are supposed to solve:
beforeEach(function () {
module('app', { comments: jasmine.createSpyObj('comments', ['create', 'delete']) });
});
module should be wrapped with a function in this case because a new spy object is supposed to be created on each beforeEach call.
Then return values are defined in-place:
comment.create.and.returnValue(Promise.resolve(200))
comments.create();
Related
EDIT: Added extra code in the filterEvents snippet for more context.
I'm not quite understanding what's going on with my code. I'm trying to pass an array into an action function inside of my Vuex store. If I return a Promise inside of that action function, then the parameter being passed isn't of type Array and is instead an Object, which results in the reject() error that I have for the Promise.
Here's some code for context:
filterEvents({ commit }, events) {
console.log(Array.isArray(events)); //this ends up false
console.log(events);
return new Promise((resolve, reject) => {
if (!Array.isArray(events)) {
reject("Invalid argument: is not of type Array.");
}
let filtered = events.filter((event) => {
let now = new Date();
let event_stop = new Date(event.stop_time);
if (event_stop >= now || event_stop == null) {
return event;
}
});
resolve(filtered);
});
}
Here's where I call filterEvents; inside of getEvents;
getEvents({ state, commit, dispatch }, searchParams) {
.....
eventful.getEvents(searchParams).then(async (res) => {
.....
console.log(Array.isArray(res.data.events.event)); //this ends up true
console.log(res.data.events.event);
/* where I call it */
await dispatch("filterEvents", res.data.events.event).then((res) => {
.....
});
}).catch((err) => {
.....
});
}
Here's the output from the Chrome developer console. First two outputs are from getEvents and last two are from filterEvents
Would really like an explanation as to why this is the case. I'm going to bet it's something small, but it's 3 a.m. at the moment and my brain can't wrap around why it's not of type Array when passed into filterEvents.
I always try to check the length prop of the array which helps me out in such cases.
...
return new Promise((resolve, reject) => {
if (!Array.isArray(events) && !events.length) {
reject("Invalid argument: is not of type Array.");
}
.....
});
...
I finally understood what my issue was after taking another look at the object that was being logged on the console. I did not know that Vuex actions HAD to have two arguments if you want to pass in a payload into that function. For example, I initially did this
filterEvents(events) {
.....
}
but what I really needed to do was
filterEvents(context, events) {
.....
}
The context argument is the object that allows you to do things such as commit and dispatch. I usually destructure the context object (i.e. { commit, dispatch} ), so I for some reason never thought twice about it. You don't have to destructure the context object to use commit and dispatch; if you don't it would just be like
context.commit('function', payload);
I am trying to mock a non-exported function via 'jest' and 're-wire'.
Here I am trying to mock 'iAmBatman' (no-pun-intended) but it is not exported.
So I use rewire, which does it job well.
But jest.mock doesn't work as expected.
Am I missing something here or Is there an easy way to achieve the same ?
The error message given by jest is :
Cannot spy the property because it is not a function; undefined given instead
service.js
function iAmBatman() {
return "Its not who I am underneath";
}
function makeACall() {
service.someServiceCall(req => {
iAmBatman();
});
return "response";
}
module.export = {
makeACall : makeACall;
}
jest.js
const services = require('./service');
const rewire = require('rewire');
const app = rewire('./service');
const generateDeepVoice = app.__get__('iAmBatman');
const mockDeepVoice = jest.spyOn(services, generateDeepVoice);
mockDeepVoice.mockImplementation(_ => {
return "But what I do that defines me";
});
describe(`....', () => {
test('....', done => {
services.makeACall(response, () => {
});
});
})
It is not entirely clear what your goal is, but if you look at the documentation of jest.spyOn, you see that it takes a methodName as the second argument, not the method itself:
jest.spyOn(object, methodName)
This explains your error: you didn't give the function name, but the function itself.
In this case, using jest.spyOn(services, 'iAmBatman') wouldn't work, since iAmBatman is not exported, and therefore services.iAmBatman is not defined.
Luckily, you don't need spyOn, as you can simply make a new mock function, and then inject that with rewire's __set__ as follows:
(note that I deleted the undefined service.someServiceCall in your first file, and fixed some typos and redundant imports)
// service.js
function iAmBatman() {
return "Its not who I am underneath";
}
function makeACall() {
return iAmBatman();
}
module.exports = {
makeACall: makeACall
}
// service.test.js
const rewire = require('rewire');
const service = rewire('./service.js');
const mockDeepVoice = jest.fn(() => "But what I do that defines me")
service.__set__('iAmBatman', mockDeepVoice)
describe('service.js', () => {
test('makeACall should call iAmBatman', () => {
service.makeACall();
expect(mockDeepVoice).toHaveBeenCalled();
});
})
Another option would be to restructure your code with iAmBatman in a seperate module, and then mock the module import with Jest. See documentation of jest.mock.
In NodeJS, I have an object like,
var scope = { word: "init" };
Using Object.defineProperty as described in MDN I re-write the get() function to be like this,
Object.defineProperty(scope, 'word', {
get: function() {
return Math.random();
}
});
Which correctly returns a new random each time I scope.word in the console. However the function must also get data from a function with a callback. So it works pretty much like a setTimeout,
Object.defineProperty(scope, 'word', {
get: function() {
setTimeout(() => {
return Math.random();
}, 1000)
}
});
Now everytime I do scope.word I get,
undefined
Because the get() function is synchronous. This can be of course solved by returning a Promise,
Object.defineProperty(scope, 'word', {
get: function() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(Math.random());
}, 1000)
});
}
});
But then I would need to do scope.word.then(...) but the whole idea behind what we are building is that the developer only has to scope.word like if it was a plain easy-to-use variable. Like an Angular's $scope or a VUE.js 'data'.
How can I make the get() function return an actual value, not a Promise? Is it possible to workaround using async / await? How?
One of the solutions is to pass the callback function like this.
const scope = {}
Object.defineProperty(scope, 'word', {
value: (cb)=>{
setTimeout(() => {
cb(Math.random())
}, 1000)
}
});
scope.word(res=>console.log(res))
I used to have the following code:
function makeCall(userInfo) {
api.postUser(userInfo).then(response => {
utils.redirect(response.url);
})
// other logic
return somethingElse;
}
And I was able to write a test that looked like this:
const successPromise = Promise.resolve({ url: 'successUrl' })
beforeEach(function() {
sinon.stub(api.postUser).returns(successPromise);
}
afterEach(function() {
api.postUser.restore();
}
it "calls API properly and redirects" do
makeCall({});
expect(api.postUser).calledWith(userInfo).toBe(true);
successPromise.then(() => {
expect(utils.redirect.calledWith('successUrl')).toBe(true);
done();
}
emd
And everything was green.
Now, I had to add another promise to make another external call, before doing the api postUser call, so my code looks like this:
function makeCall(names) {
fetchUserData(names).then(userData => {
return api.postUser(userData).then(response => {
utils.redirect(response.url);
})
})
// other logic
return somethingElse;
}
where fetchUseData is a chain of many promises, such like:
function fetchNames(names) {
// some name regions
return Promise.all(names);
}
function fetchUserData(names) {
fetchUsersByNames(names).then(users => {
// For now we just choose first user
{
id: users[0].id,
name: users[0].name,
}
});
}
And the tests I had fail. I am trying to understand how to change my tests to make sure that I am still testing that I do the final API call properly and the redirect is also done. I want to stub what fetchUserData(names), to prevent doing that HTTP call.
You're not using promises correctly. Your code doesn't have a single return statement, when it should have several (or it should be using arrow functions in such a way that you don't need them, which you're not doing).
Fix your code:
function makeCall(names) {
// v---- return
return fetchUserData(names).then(userData => {
// v---- return
return api.postUser(userData).then(response => {
utils.redirect(response.url);
})
})
}
function fetchUserData(names) {
// v---- return
return fetchUsersByNames(names).then(users => {
// For now we just choose first user
// v---- return
return {
id: users[0].id,
name: users[0].name,
}
});
}
Once you've done that, you can have your test wait for all of the operations to finish.
Test code:
makeCall(['name']).then(() =>
expect(api.postUser).calledWith(userInfo).toBe(true);
expect(utils.redirect.calledWith('successUrl')).toBe(true);
done();
});
You should add a return statement, otherwise you are not returning promises nowhere:
function fetchNames(names) {
// some name regions
return Promise.all(names);
}
function fetchUserData(names) {
return fetchUsersByNames(names).then(users => {
// For now we just choose first user
{
id: users[0].id,
name: users[0].name,
}
});
}
So when you are using Promise.all(), then you will have as result of the promise an array with all the value returned by all the promises.
So then this method should look like this when called:
fetchNames(names).then((arrayOfResolvedPromises) => {
// here you will have all your promised resolved and the array holds all the results
});
So inside your test you can move your done inside the block where all the promises will be resolved.
In addition, I strongly suggest you to use a library as chai-as-promised for testing promises.
It has a lot of nice methods for testing your promises.
https://github.com/domenic/chai-as-promised
Is there any way in Jest to mock global objects, such as navigator, or Image*? I've pretty much given up on this, and left it up to a series of mockable utility methods. For example:
// Utils.js
export isOnline() {
return navigator.onLine;
}
Testing this tiny function is simple, but crufty and not deterministic at all. I can get 75% of the way there, but this is about as far as I can go:
// Utils.test.js
it('knows if it is online', () => {
const { isOnline } = require('path/to/Utils');
expect(() => isOnline()).not.toThrow();
expect(typeof isOnline()).toBe('boolean');
});
On the other hand, if I am okay with this indirection, I can now access navigator via these utilities:
// Foo.js
import { isOnline } from './Utils';
export default class Foo {
doSomethingOnline() {
if (!isOnline()) throw new Error('Not online');
/* More implementation */
}
}
...and deterministically test like this...
// Foo.test.js
it('throws when offline', () => {
const Utils = require('../services/Utils');
Utils.isOnline = jest.fn(() => isOnline);
const Foo = require('../path/to/Foo').default;
let foo = new Foo();
// User is offline -- should fail
let isOnline = false;
expect(() => foo.doSomethingOnline()).toThrow();
// User is online -- should be okay
isOnline = true;
expect(() => foo.doSomethingOnline()).not.toThrow();
});
Out of all the testing frameworks I've used, Jest feels like the most complete solution, but any time I write awkward code just to make it testable, I feel like my testing tools are letting me down.
Is this the only solution or do I need to add Rewire?
*Don't smirk. Image is fantastic for pinging a remote network resource.
As every test suite run its own environment, you can mock globals by just overwriting them. All global variables can be accessed by the global namespace:
global.navigator = {
onLine: true
}
The overwrite has only effects in your current test and will not effect others. This also a good way to handle Math.random or Date.now.
Note, that through some changes in jsdom it could be possible that you have to mock globals like this:
Object.defineProperty(globalObject, key, { value, writable: true });
The correct way of doing this is to use spyOn. The other answers here, even though they work, don't consider cleanup and pollute the global scope.
// beforeAll
jest
.spyOn(window, 'navigator', 'get')
.mockImplementation(() => { ... })
// afterAll
jest.restoreAllMocks();
Jest may have changed since the accepted answer was written, but Jest does not appear to reset your global after testing. Please see the testcases attached.
https://repl.it/repls/DecentPlushDeals
As far as I know, the only way around this is with an afterEach() or afterAll() to clean up your assignments to global.
let originalGlobal = global;
afterEach(() => {
delete global.x;
})
describe('Scope 1', () => {
it('should assign globals locally', () => {
global.x = "tomato";
expect(global.x).toBeTruthy()
});
});
describe('Scope 2', () => {
it('should not remember globals in subsequent test cases', () => {
expect(global.x).toBeFalsy();
})
});
If someone needs to mock a global with static properties then my example should help:
beforeAll(() => {
global.EventSource = jest.fn(() => ({
readyState: 0,
close: jest.fn()
}))
global.EventSource.CONNECTING = 0
global.EventSource.OPEN = 1
global.EventSource.CLOSED = 2
})
If you are using react-testing-library and you use the cleanup method provided by the library, it will remove all global declarations made in that file once all tests in the file have run. This will then not carry over to any other tests run.
Example:
import { cleanup } from 'react-testing-library'
afterEach(cleanup)
global.getSelection = () => {
}
describe('test', () => {
expect(true).toBeTruthy()
})
If you need to assign and reassign the value of a property on window.navigator then you'll need to:
Declare a non-constant variable
Return it from the global/window object
Change the value of that original variable (by reference)
This will prevent errors when trying to reassign the value on window.navigator because these are mostly read-only.
let mockUserAgent = "";
beforeAll(() => {
Object.defineProperty(global.navigator, "userAgent", {
get() {
return mockUserAgent;
},
});
});
it("returns the newly set attribute", () => {
mockUserAgent = "secret-agent";
expect(window.navigator.userAgent).toEqual("secret-agent");
});