Better way to disable console inside unit tests - javascript

I wonder if there is a better way to disable console errors inside a specific Jest test (i.e. restore the original console before/after each test).
Here is my current approach:
describe("Some description", () => {
let consoleSpy;
beforeEach(() => {
if (typeof consoleSpy === "function") {
consoleSpy.mockRestore();
}
});
test("Some test that should not output errors to jest console", () => {
expect.assertions(2);
consoleSpy = jest.spyOn(console, "error").mockImplementation();
// some function that uses console error
expect(someFunction).toBe("X");
expect(consoleSpy).toHaveBeenCalled();
});
test("Test that has console available", () => {
// shows up during jest watch test, just as intended
console.error("test");
});
});
Is there a cleaner way of accomplishing the same thing? I would like to avoid spyOn, but mockRestore only seems to work with it.

For particular spec file, Andreas's is good enough. Below setup will suppress console.log statements for all test suites,
jest --silent
(or)
To customize warn, info and debug you can use below setup
tests/setup.js or jest-preload.js configured in setupFilesAfterEnv
global.console = {
...console,
// uncomment to ignore a specific log level
log: jest.fn(),
debug: jest.fn(),
info: jest.fn(),
// warn: jest.fn(),
// error: jest.fn(),
};
jest.config.js
module.exports = {
verbose: true,
setupFilesAfterEnv: ["<rootDir>/__tests__/setup.js"],
};

If you want to do it just for a specific test:
beforeEach(() => {
jest.spyOn(console, 'warn').mockImplementation(() => {});
});

As every test file runs in its own thread there is no need to restore it if you want to disable it for all test in one file. For the same reason you can also just write
console.log = jest.fn()
expect(console.log).toHaveBeenCalled();

I found that the answer above re: suppressing console.log across all test suites threw errors when any other console methods (e.g. warn, error) were called since it was replacing the entire global console object.
This somewhat similar approach worked for me with Jest 22+:
package.json
"jest": {
"setupFiles": [...],
"setupTestFrameworkScriptFile": "<rootDir>/jest/setup.js",
...
}
jest/setup.js
jest.spyOn(global.console, 'log').mockImplementation(() => jest.fn());
Using this method, only console.log is mocked and other console methods are unaffected.

To me a more clear/clean way (reader needs little knowledge of the jest API to understand what is happening), is to just manually do what mockRestore does:
// at start of test you want to suppress
const consoleLog = console.log;
console.log = jest.fn();
// at end of test
console.log = consoleLog;

beforeAll(() => {
jest.spyOn(console, 'log').mockImplementation(() => {});
jest.spyOn(console, 'error').mockImplementation(() => {});
jest.spyOn(console, 'warn').mockImplementation(() => {});
jest.spyOn(console, 'info').mockImplementation(() => {});
jest.spyOn(console, 'debug').mockImplementation(() => {});
});

Here's all the lines you may want to use. You can put them right in the test:
jest.spyOn(console, 'warn').mockImplementation(() => {});
console.warn("You won't see me!")
expect(console.warn).toHaveBeenCalled();
console.warn.mockRestore();

Weirdly the answers above (except Raja's great answer but I wanted to share the weird way the others fail and how to clear the mock so no one else wastes the time I did) seem to successfully create the mock but don't suppress the logging to the console.
Both
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
and
global console = {
warn: jest.fn().mockImplementation(() => {});
}
successfully install the mock (I can use expect(console.warn).toBeCalledTimes(1) and it passes) but it still outputs the warning even though the mock implementation seemingly should be replacing the default (this is in a jsdom environment).
Eventually I found a hack to fix the problem and put the following in the file loaded with SetupFiles in your config (note that I found sometimes global.$ didn't work for me when putting jquery into global context so I just set all my globals this way in my setup).
const consoleWarn = jest.spyOn(console, 'warn').mockImplementation(() => {});
const consoleLog = jest.spyOn(console, 'log').mockImplementation(() => {});
const consoleDebug = jest.spyOn(console, 'debug').mockImplementation(() => {});
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
Object.defineProperty(global, 'console', {value: {
warn: consoleWarn,
log: consoleLog,
debug: consoleDebug,
error: consoleError}});
It feels ugly and I then have to put code like the following in each test file since beforeEach isn't defined in the files referenced by SetupFiles (maybe you could put both in SetupFilesAfterEnv but I haven't tried).
beforeEach(() => {
console.warn.mockClear();
});

Since jest.spyOn doesn't work for this (it may have in the past), I resorted to jest.fn with a manual mock restoration as pointed out in Jest docs. This way, you should not miss any logs which are not empirically ignored in a specific test.
const consoleError = console.error
beforeEach(() => {
console.error = consoleError
})
test('with error', () => {
console.error = jest.fn()
console.error('error') // can't see me
})
test('with error and log', () => {
console.error('error') // now you can
})

If you are using command npm test to run test then change the test script in package.json like below
{
....
"name": "....",
"version": "0.0.1",
"private": true,
"scripts": {
"android": "react-native run-android",
"ios": "react-native run-ios",
"start": "react-native start",
"test": "jest --silent", // add --silent to jest in script like this
"lint": "eslint ."
},
...
}
Or else you can directly run command npx jest --silent to get rid of all logs and errors when testing

Kudos to #Raja's top answer. Here is what I am using (I would comment, but can't share a multi-line code block in a comment).
With jest v26, I'm getting this error:
We detected setupFilesAfterEnv in your package.json.
Remove it from Jest configuration, and put the initialization code in src/setupTests.js:
This file will be loaded automatically.
Therefore, I had to remove the setupFilesAfterEnv from my jest config, and add this to src/setupTests.js
// https://stackoverflow.com/questions/44467657/jest-better-way-to-disable-console-inside-unit-tests
const nativeConsoleError = global.console.error
global.console.error = (...args) => {
if (args.join('').includes('Could not parse CSS stylesheet')) {
return
}
return nativeConsoleError(...args)
}

Another approach is to use process.env.NODE_ENV. This way one can selectively choose what to show (or not) while running tests:
if (process.env.NODE_ENV === 'development') {
console.log('Show output only while in "development" mode');
} else if (process.env.NODE_ENV === 'test') {
console.log('Show output only while in "test" mode');
}
or
const logDev = msg => {
if (process.env.NODE_ENV === 'development') {
console.log(msg);
}
}
logDev('Show output only while in "development" mode');
This will require this configuration to be placed on package.json:
"jest": {
"globals": {
"NODE_ENV": "test"
}
}
Note that this approach is not a direct solution to the original question, but gives the expected result as long as one has the possibility to wrap the console.log with the mentioned condition.

Related

Unexpected behavior from Jest mockImplementation

I have code that I am writing tests for to refactor later, as such I cannot change the code or any of the dependencies. Here is the problem:
// foo.js
Knex = require('knex')
module.exports ={func}
// calling this inside func will
// have the expected {a:4}
client = Knex()
async function func(){
console.log(client)
return true
}
// foo.spec.js
const foo = require('./foo')
const Knex = require('knex')
jest.mock('knex', ()=>jest.fn())
describe('jest.mockImplementation',()=>{
it('should mock knex',async ()=>{
Knex.mockImplementation(()=>({a:4}))
// alternative, I can put
// const foo = require('./foo')
// here
await foo.func()
})
})
// jest.config.js
module.exports={
"verbose": true,
"testEnvironment": "node",
}
//package.json
{
"dependencies": {
"jest": "^26.6.3",
"knex": "0.19.3"
}
}
I run:
$ jest --config jest.config.js --runInBand foo.spec.js and I expect there to be a console log of { a : 4}, but it is undefined. Note however, if I move the client inside func then it will log {a : 4}
alternatively, if I leave the client where it is and require foo in spec.js after mockImplementation, it will again have the expected console log.
I would have expected to see the correct behavior with the client being created outside of func and without needing to require foo after mockImplementation.
Why is this happening and how can I have the desired behavior without moving the client? also requireing inside the function is not the best.
I created this repl.it for experimentation; please don't update it for others' use:
https://replit.com/join/xmlwttzl-eminarakelian1
The code of the module scope will be executed immediately when the module is required, so it is too late to provide the mock implementation in the test case.
jest.mock() will be hoisted to the top of the test file. It will be executed before the require statement, so when the module is required, the mock implementation provided in jest.mock() will be used.
Provide a mock implementation inside jest.mock() like this:
const foo = require('./foo');
jest.mock('knex', () => jest.fn(() => ({ a: 4 })));
describe('jest.mockImplementation', () => {
it('should mock knex', async () => {
await foo.func();
});
});
test result:
PASS examples/66881537/foo.spec.js (6.347 s)
jest.mockImplementation
✓ should mock knex (15 ms)
console.log
{ a: 4 }
at Object.<anonymous> (examples/66881537/foo.js:8:11)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 6.789 s

How to get localStorage working in vue testing

I am trying to test vue components.
I have a vue single file component which uses vuex. My states are stored in store.js which makes use of localStorage. However, when I run npm test I get error that reads:
WEBPACK Compiled successfully in 9416ms
MOCHA Testing...
RUNTIME EXCEPTION Exception occurred while loading your tests
ReferenceError: localStorage is not defined
Tools I am using for testing:
#vue/test-utils, expect, jsdom, jsdom-global, mocha, mocha-webpack
How I run the tests:
"test": "mocha-webpack --webpack-config node_modules/laravel-mix/setup/webpack.config.js --require tests/JavaScript/setup.js tests/JavaScript/**/*.spec.js"
A sample test, order.spec.js:
require('../../resources/assets/js/store/store');
require('../../resources/assets/js/app');
import { mount } from '#vue/test-utils';
import Order from '../../resources/assets/js/views/order/List.vue';
import expect from 'expect';
describe('Order', ()=>{
it('has alert hidden by default', () => {
let wrapper = mount(Order);
expect(wrapper.vm.alert).toBe(false);
})
})
In setup.js file I am loading jsdom like this:
require('jsdom-global')();
How do I fix this?
jsdom-global is using an old version of jsdom. jsdom has supported localStorage since 11.12.0.
To use jsdom 11.12+ with localStorage support, you can add jsdom window properties to the global scope yourself in a test setup file that runs before your tests:
/* setup.js */
const { JSDOM } = require('jsdom');
const jsdom = new JSDOM('<!doctype html><html><body></body></html>');
const { window } = jsdom;
function copyProps(src, target) {
Object.defineProperties(target, {
...Object.getOwnPropertyDescriptors(src),
...Object.getOwnPropertyDescriptors(target),
});
}
global.window = window;
global.document = window.document;
global.navigator = {
userAgent: 'node.js',
};
global.requestAnimationFrame = function (callback) {
return setTimeout(callback, 0);
};
global.cancelAnimationFrame = function (id) {
clearTimeout(id);
};
copyProps(window, global);

Load a web page into a headless Jasmine spec running PhantomJS

How do I read in a page from localhost into a headless Jasmine spec so test cases can work on the DOM elements?
My Gulp task is successfully running Jasmine specs for unit testing, and now I need to build integration tests to verify full web pages served from localhost. I'm using the gulp-jasmine-browser plugin to run PhantomJS.
Example:
gulpfile.js
var gulp = require('gulp');
var jasmineBrowser = require('gulp-jasmine-browser');
function specRunner() {
gulp.src(['node_modules/jquery/dist/jquery.js', 'src/js/*.js', 'spec/*.js'])
.pipe(jasmineBrowser.specRunner({ console: true }))
.pipe(jasmineBrowser.headless());
}
gulp.task('spec', specRunner);
spec/cart-spec.js
describe('Cart component', function() {
it('displays on the gateway page', function() {
var page = loadWebPage('http://localhost/'); //DOES NOT WORK
var cart = page.find('#cart');
expect(cart.length).toBe(1);
});
});
There is no loadWebPage() function. It's just to illustrate the functionality I believe is needed.
End-to-End testing frameworks like a Selenium, WebdriverIO, Nightwatch.js, Protractor and so on are more suitable in such case.
The gulp-jasmine-browser plugin still is about the Unit testing in the browser environment. It is not possible to navigate between pages.
I put together the following code that appears to work. Please feel free to check out my repo and confirm in your own environment.
package.json
{
"name": "40646680",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "gulp jasmine"
},
"devDependencies": {
"gulp": "^3.9.1",
"gulp-jasmine-browser": "^1.7.1",
"jasmine": "^2.5.2",
"phantomjs": "^2.1.7"
}
}
gulpfile.js
(() => {
"use strict";
var gulp = require("gulp"),
jasmineBrowser = require("gulp-jasmine-browser");
gulp.task("jasmine", () => {
return gulp.src("test/*.js")
.pipe(jasmineBrowser.specRunner({
console: true
}))
.pipe(jasmineBrowser.headless());
});
})();
test/sampleJasmine.js
describe("A suite", function() {
it("contains spec with an expectation", function() {
expect(true).toBe(true);
});
it("contains failing spec with an expectation", function() {
expect(true).toBe(false);
});
});
Execution
Bob Chatman#CHATBAG42 F:\Development\StackOverflow\40646680
> npm test
> 40646680#1.0.0 test F:\Development\StackOverflow\40646680
> gulp jasmine
[21:56:44] Using gulpfile F:\Development\StackOverflow\40646680\gulpfile.js
[21:56:44] Starting 'jasmine'...
[21:56:44] Jasmine server listening on port 8000
.F
Failures:
1) A suite contains failing spec with an expectation
1.1) Expected true to be false.
2 specs, 1 failure
Finished in 0 seconds
[21:56:49] 'jasmine' errored after 4.26 s
[21:56:49] Error in plugin 'gulp-jasmine-browser'
Message:
1 failure
npm ERR! Test failed. See above for more details.
Dependencies
node 7.2
npm 3.9.3
jasmine 2.5.2
phantomjs 2.1.7
gulp 3.9.1
jsdom to the rescue!
It turns out it's pretty easy to load a web page into a headless Jasmine spec... but you need to swap out PhantomJS for jsdom.
Strategy:
Use Jasmine's beforeAll() to call a function that will run JSDOM.fromURL() to request the web page.
Once the web page has been loaded into the DOM, expose window and jQuery for use in your test cases.
Finally, call done() to indicate the tests are now ready to run.
Make sure to close the window after the tests have run.
spec.js
const url = 'http://dnajs.org/';
const { JSDOM } = require('jsdom');
let window, $;
function loadWebPage(done) {
function handleWebPage(dom) {
function waitForScripts() {
window = dom.window;
$ = dom.window.jQuery;
done();
}
dom.window.onload = waitForScripts;
}
const options = { resources: 'usable', runScripts: 'dangerously' };
JSDOM.fromURL(url, options).then(handleWebPage);
}
function closeWebPage() { window.close(); }
describe('The web page', () => {
beforeAll(loadWebPage);
afterAll(closeWebPage);
it('has the correct URL', () => {
expect(window.location.href).toBe(url);
});
it('has exactly one header, main, and footer', () => {
const actual = {
header: $('body >header').length,
main: $('body >main').length,
footer: $('body >footer').length
};
const expected = { header: 1, main: 1, footer: 1 };
expect(actual).toEqual(expected);
});
});
Test output
Note: Above screenshot is from a similar Mocha spec since Mocha has a nice default reporter.
Project
It's on GitHub if you want try it out yourself:
https://github.com/dnajs/load-web-page-jsdom-jasmine
EDITED: Updated for jsdom 11

how can i mock a dynamically loaded, virtual module in jest?

I have a virtual javascript file in a Jest unit test with the path '/widgets/1.0.js'. I have mocked the fs module to simulate its existence.
Now i would like to dynamically load it to invoke a method 'foo()'. I thought it would be a case of using a virtual mock:
index.test.js
jest.mock('/widgets/1.0.js', () => {foo: jest.fn(() => {console.log('foo!')})}, {virtual: true});
The code which calls the mock:
index.js
let module = require('/widgets/1.0.js');
module.foo();
When i run the test:
Cannot find module '/widgets/1.0.js' from 'index.js'
at Resolver.resolveModule (node_modules/jest-resolve/build/index.js:151:17)
at processWidgets (src/index.js:115:2418)
at Object.<anonymous> (src/__tests__/index.test.js:99:73)
I think it should be possible. Any ideas?
thanks!
It appears to be a problem with the module path. This works:
index.test.js
jest.mock('1.0', () => {
return {
foo: () => {return 42;}
}
}, {virtual: true});
index.js
const module = require('1.0');
let retval = module.foo();
console.log('retval: ', retval);
If i use '/widgets/1.0' it does not. Hope it helps..

How to use localStorage in unit tests with Chai

I have an app with react and redux. My test engine is - Chai
In my reducer (src/my_reducer.js), I try to get token from localStorage like this:
const initialState = {
profile: {},
token: window.localStorage.getItem('id_token') ? window.localStorage.getItem('id_token') : null,
}
In my tests file (test/reducer_spec.js) I have import 'my_reducer' before test cases:
import myReducer from '../src/my_reducer'
And I have an error, if I try to run test - localStorage (or window.localStorage) - undefined.
I need to mock localStorage? If I need, where is the place for it?
I presume you are running your tests with mocha?
mocha tests run in node.js, and node.js does not have a global window variable. But you can easily create one in your tests:
global.window = {};
You can even add the localStorage to it immediately:
global.window = { localStorage: /* your mock localStorage */ }
The mock depends on what you store in your local storage, but for the example code above this might be a reasonable mock object:
var mockLocalStorage = {
getItem: function (key) {
if( key === 'id_token' ){ return /* a token object */; }
return null;
}
}
Of course, for different tests you can have different mocks, e.g. another mock might always return null to test the case that the key cannot be found.
I solve problem with mock-local-storage
My run test command is:
mocha -r mock-local-storage --compilers js:babel-core/register --recursive
For testing purposes I recommend not to make any calls which may have side effects or call external modules in declarations.
Because requiring / importing your reducer implicitly calls window.localStorage.getItem(...) clean testing gets hard.
I'd suggest to wrap your initialization code with a init method so nothing happens if you require/import your module before calling init. Then you can use beforeEach afterEach to cleanly setup mocks/sandboxes.
import myReducer from '../src/my_reducer'
describe('with faked localStorage', function() {
var sandbox
beforeEach(function() {
sandbox = sinon.sandbox.create()
// fake window.localStorage
})
afterEach(function() {
sandbox.restore()
})
describe('the reducer', function() {
before(function() {
myReducer.init()
})
})
})
The second best solution is to postpone the import and use require within the before test hook.
describe('with fake localStorage', function() {
var sandbox
beforeEach(function() {
sandbox = sinon.sandbox.create()
// fake window.localStorage
})
afterEach(function() {
sandbox.restore()
})
describe('the reducer', function() {
var myReducer
before(function() {
myReducer = require('../src/my_reducer')
})
})
})
It is because you are not running Chai in a browser environment.
Try:
// Make sure there is a window object available, and that it has localstorage (old browsers don't)
const initialState = {
profile: {},
// window.localStorage.getItem('id_token') will return null if key not found
token: window && window.localStorage ? window.localStorage.getItem('id_token') : null,
}

Categories