I'm seeing some strange results when testing what I thought were simple saga's with redux-saga.
Take this saga for example:
export function* initSaga() {
yield call(window.clearTimeout, runner)
yield call(PubSub.publishSync, 'RESET_CORE')
}
My mental model of the test reads, first check it called window.clearTimeout with a parameter which matches the value of runner then test it called the publishSync method of PubSub with the value 'RESET_CORE'
My test for the first part reads:
describe('when testing the init saga', () => {
const saga = initSaga()
const runner = null
it('first clears the draw loop for the canvas', () => {
const result = saga.next().value
const expected = call(window.clearInterval, runner)
console.log(result)
console.log(expected)
expect(result).to.deep.equal(expected)
})
})
What's frustrating is that the error message reads:
AssertionError: expected { Object (##redux-saga/IO, CALL) } to deeply equal { Object (##redux-saga/IO, CALL) }
And my console logs read:
console.log src\tests\sagas\simulatorSaga.test.js:25
{ '##redux-saga/IO': true,
CALL:
{ context: null,
fn: [Function: bound stopTimer],
args: [ null ] } }
console.log src\tests\sagas\simulatorSaga.test.js:26
{ '##redux-saga/IO': true,
CALL:
{ context: null,
fn: [Function: bound stopTimer],
args: [ null ] } }
Which to me look identical. I'm presuming there is something simple I'm missing here, some kind of deep equal object reference kind of stuff but I'm not sure bearing in mind the saga and test how I could make this any more cut down.
I'm aware that the whole window + pubsub isn't exactly how saga's are meant to be used, but the app is interfacing with a canvas for running a simulation so we kind of have to do it that way.
I know that that's what the official docs suggest but I don't like such testing. It looks a little bit like testing if redux-saga does its job. What you are really interested in is if clearInterval and publishSync are executed with correct parameters. You don't care what the generator returns. So here's what I'm suggesting:
const executeSaga = function (saga) {
var result = saga.next();
while(!result.done) {
result = saga.next();
}
}
describe('when testing the init saga', () => {
const runner = null;
it('first clears the draw loop for the canvas', () => {
sinon.stub(window, 'clearInterval');
sinon.stub(PubSub, 'publishSync');
executeSaga(initSaga());
expect(window.clearInterval)
.to.be.calledOnce
.and.to.be.calledWith(runner);
expect(PubSub.publishSync)
.to.be.calledOnce
.and.to.be.calledWith('RESET_CORE');
window.clearInterval.restore();
PubSub.publishSync.restore();
})
})
It involves sinonjs. We are stubbing both functions, run the whole saga and then expect the stubs.
Related
I'm quite new with jest testing and I'm having trouble to understand how Jest deals with the functions I'm trying to test. Here's my problem:
I'm trying to test the following, quite simple function, which will receive a bookId and will find the object within an array that containa such id. All is vanilla js, no react.
function catchSelectedBook(bookId) {
const objectSearchAsString = localStorage.getItem('objecttransfer');
const booksObject = JSON.parse(objectSearchAsString);
const currentBook = booksObject.filter((book) => book.id === bookId);
return currentBook;
}
The unit test Jest code is the following:
describe('Given a function that is given as argument the id', () => {
test('When invoked, it finds the book that matches such id', () => {
const returnMokObject = {
kind: 'books#volumes',
totalItems: 1080,
items: [
{
kind: 'books#volume',
id: 'HN1dzQEACAAJ',
}],
};
mockLocalstorageJest();
const answer = catchSelectedBook('HN1dzQEACAAJ');
expect(answer.id).toBe('HN1dzQEACAAJ');
});
});
The function mockLocalstorageJest sends to the local storage an object so that it can be get when the function catchSelectedBook is tested
I export my function like this:
module.exports = {
catchSelectedBook,mockLocalstorageJest,
};
And I import the function into the test file like this:
const { catchSelectedBook, mockLocalstorageJest} = require('./book-details.js');
Whenever I run the test, I got the following error message:
enter image description here
Does that mean that Jest doesn't have the array method "filter" defined?
Thanks!
I think that this instruction is returning null
const objectSearchAsString = localStorage.getItem('objecttransfer');
JSON.parse(null) returns null so bookObject is also null.
I believe that your problem is that you are not setting up the local storage correctly in mockLocalstorageJest().
newbie testing here and I ran into this error
module.js
import reducer from './reducer';
export default function getGModalModule({ domain }) {
return {
id: `GModal-module-${domain}`,
reducerMap: {
[domain]: reducer({
domain,
}),
},
};
}
module.test.js
import getGModalModule from '../module';
import reducer from '../reducer';
describe('getGModalModule', () => {
it('returns the correct module', () => {
const reducers = reducer({
domain: 'demo'
})
const expectVal = getGModalModule({
domain: 'demo'
});
const received = {
id: `GModal-module-demo`,
reducerMap: {
demo: reducers,
},
}
console.log("expectVal", expectVal)
console.log("received", received)
expect(expectVal).toEqual(received);
});
});
The error says something like this:
Expected: {"id": "GModal-module-demo", "reducerMap": {"demo": [Function gModalReducer]}}
Received: serializes to the same string
I've tried to log out the result and they look exactly the same, any idea why this doesn't pass the test ?
expectVal { id: 'GModal-module-demo', reducerMap: { demo: [Function: gModalReducer] } }
received { id: 'GModal-module-demo', reducerMap: { demo: [Function: gModalReducer] } }
Thanks in advance,
Jest cannot compare functions. Even if they are serialized into the same string, even if their source code is the same, function may refer to different variables through closures or bound to different this so we cannot say if functions are equal.
expect(function aaa() {}).toEqual(function aaa() {}); // fails
You may want to exclude methods before comparison explicitly or create custom matcher that will do that for you. But much better if you test what functions does instead. In your case you probably have separate test for ./reducer.js, right? You can just join both test: that one testing reducer in isolation and this one that checks object key names.
Trying to unit test my controller, but when I do so I'm getting the following error.
I'm open to answers with a different way of testing my controller.
Error:
TypeError: expected sinon object
const test = require('sinon-test');
describe('index (get all)', function() {
beforeEach(function() {
res = {
json: sinon.spy(),
status: sinon.stub().returns({ end: sinon.spy() })
};
expectedResult = [{}, {}, {}];
});
it(
'should return array of vehicles or empty array',
test(() => {
this.stub(Vehicle, 'find').yields(null, expectedResult);
Controller.index(req, res);
sinon.assert.calledWith(Vehicle.find, {});
sinon.assert.calledWith(res.json, sinon.match.array);
})
);
});
First of all, when asking a StackOverflow question, it makes sense to post a fully runnable example and stating all dependencies. Basically, I used more than an hour trying to test this out because both were missing.
This is the fully expanded example, just with dummy implementations of your two main objects.
var sinon = require("sinon");
var sinonTest = require("sinon-test");
var test = sinonTest(sinon);
const Vehicle = {
find() {}
};
const Controller = {
index() {}
};
describe("index (get all)", function() {
let expectedResult, res, req;
beforeEach(function() {
res = {
json: sinon.spy(),
status: sinon.stub().returns({ end: sinon.spy() })
};
expectedResult = [{}, {}, {}];
});
it(
"should return array of vehicles or empty array",
test(function() {
this.stub(Vehicle, "find").yields(null, expectedResult);
Controller.index(req, res);
sinon.assert.calledWith(Vehicle.find, {});
sinon.assert.calledWith(res.json, sinon.match.array);
})
);
});
Now, to your question, which was why you were getting an error. The first thing to test is: does the bug appear when I update to the latest versions of the dependencies of the test? The answer is, no, it does not appear. So basically, this is about you using the sinon-test version 2.0, which had a compatibility bug with Sinon 3. This is from the changelog:
2.1.0 / 2017-08-07
==================
Fix compatibility with Sinon 3 (#77)
2.0.0 / 2017-06-22
==================
* Simplify configuration API (#74)
So, given that has been fixed, and the example below is being used, the test is fully runnable:
mocha mytest.js
index (get all)
1) should return array of vehicles or empty array
0 passing (6ms)
1 failing
1) index (get all)
should return array of vehicles or empty array:
AssertError: expected find to be called with arguments
The error here is of course not really an error, but simply a byproduct of me not having the full implementation of your controller and vehicle classes.
This is a follow up question to this question.
I have a function called assertTruthy for Jest. assertTruthy(msg, fn, args), expects a message, a function and arguments and should pass if the thing that is returned when the function is invoked with the arguments is truthy and fail if its not.
I want to extend it to also support Jest's only and skip.
Here is what I wrote:
assertTruthy.skip = ({
message = '',
fn = undefined,
args,
} = {}) => {
it.skip(message, () => {
expect(fn(args)).toBeTruthy();
});
};
assertTruthy.only = ({
message = '',
fn = undefined,
args,
} = {}) => {
it.only(message, () => {
expect(fn(args)).toBeTruthy();
});
};
How would I test these functions?
Here is what I tried, which works, but I'm not sure if this is correct.
describe('skip()', () => {
test('it skips the function', () => {
it.skip = jest.fn();
assertTruthy.skip({
message: 'something',
fn: () => true,
args: undefined,
});
expect(it.skip).toHaveBeenCalledTimes(1);
});
});
This looks like a fair enough test that your assertTruthy skip and only call Jest's it skip and only methods.
You might want to assert that it also calls them with the arguments you expect using toHaveBeenCalledWith.
Can you please elaborate more, what you want to achieve and I did't get it properly why you want to extend jest skip and only methods to achieve the same thing that you are already testing.
But if you only want to test if a function not been invoked/executed with toHaveBeenCalledTimes() based on the arguments to be truthy/falsy then you are doing it right.
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");
});