Is there a jest config that will fail tests on console.warn? - jestjs

How do I configure jest tests to fail on warnings?
console.warn('stuff');
// fail test

You can use this simple override :
let error = console.error
console.error = function (message) {
error.apply(console, arguments) // keep default behaviour
throw (message instanceof Error ? message : new Error(message))
}
You can make it available across all tests using Jest setupFiles.
In package.json :
"jest": {
"setupFiles": [
"./tests/jest.overrides.js"
]
}
Then put the snippet into jest.overrides.js

For those using create-react-app, not wanting to run npm run eject, you can add the following code to ./src/setupTests.js:
global.console.warn = (message) => {
throw message
}
global.console.error = (message) => {
throw message
}
Now, jest will fail when messages are passed to console.warn or console.error.
create-react-app Docs - Initializing Test Environment

I implemented this recently using jest.spyOn introduced in v19.0.0 to mock the warn method of console (which is accesses via the global context / object).
Can then expect that the mocked warn was not called, as shown below.
describe('A function that does something', () => {
it('Should not trigger a warning', () => {
var warn = jest.spyOn(global.console, 'warn');
// Do something that may trigger warning via `console.warn`
doSomething();
// ... i.e.
console.warn('stuff');
// Check that warn was not called (fail on warning)
expect(warn).not.toHaveBeenCalled();
// Cleanup
warn.mockReset();
warn.mockRestore();
});
});

There is a useful npm package that helps you to achieve that: jest-fail-on-console
It's easily configurable.
Install:
npm i -D jest-fail-on-console
Configure:
In a file used in the setupFilesAfterEnv option of Jest, add this code:
import failOnConsole from 'jest-fail-on-console'
failOnConsole()
// or with options:
failOnConsole({ shouldFailOnWarn: false })

I decided to post a full example based on user1823021 answer
describe('#perform', () => {
var api
// the global.fetch is set to jest.fn() object globally
global.fetch = jest.fn()
var warn = jest.spyOn(global.console, 'warn');
beforeEach(function() {
// before every test, all mocks need to be resetted
api = new Api()
global.fetch.mockReset()
warn.mockReset()
});
it('triggers an console.warn if fetch fails', function() {
// In this test fetch mock throws an error
global.fetch.mockImplementationOnce(() => {
throw 'error triggered'
})
// I run the test
api.perform()
// I verify that the warn spy has been triggered
expect(warn).toHaveBeenCalledTimes(1);
expect(warn).toBeCalledWith("api call failed with error: ", "error triggered")
});
it('calls fetch function', function() {
// I create 2 more mock objects to verify the fetch parameters
const url = jest.fn()
const config = jest.fn()
api.url = url
api.config = config
// I run the test
api.perform()
// I verify that fetch has been called with url and config mocks
expect(global.fetch).toHaveBeenCalledTimes(1)
expect(global.fetch).toBeCalledWith(url, config)
expect(warn).toHaveBeenCalledTimes(0)
});
})
the #perform method I am testing
class Api {
constructor(auth) {
this._credentials = auth
}
perform = async () => {
try {
return await fetch(this.url, this.config)
} catch(error) {
console.warn('api call failed with error: ', error)
}
}
}

You can set the environment variable CI=true before running jest which will cause it to fail tests on warnings in addition to errors.
Example which runs all test files in the test folder:
CI=true jest ./test
Automated CI/CD pipelines such as Github Actions set CI to true by default, which can be one reason why a unit test will pass on your local machine when warnings are thrown, but fail in the pipeline.
(Here is the Github Actions documentation on default environment variables: https://docs.github.com/en/actions/learn-github-actions/environment-variables#default-environment-variables)

Related

Jest test fail: "● default root route"

I'm trying to write Jest tests for a Fastify project. But I'm stuck with the example code failing with an ambiguous error: "● default root route".
// root.test.ts
import { build } from '../helper'
const app = build()
test('default root route', async () => {
const res = await app.inject({
url: '/'
})
expect(res.json()).toEqual({ root: true })
})
// helper.ts
import Fastify from "fastify"
import fp from "fastify-plugin"
import App from "../src/app"
export function build() {
const app = Fastify()
beforeAll(async () => {
void app.register(fp(App))
await app.ready()
})
afterAll(() => app.close())
return app
}
// console error:
FAIL test/routes/root.test.ts (8.547 s)
● default root route
A worker process has failed to exit gracefully and has been force exited. This is likely caused by tests leaking due to improper teardown. Try running with --detectOpenHandles to find leaks. Active timers can also cause this, ensure that .unref() was called on them.
What am I doing wrong?
After running --detectOpenHandles, Jest reported that open ioredis connections were timing out.
I hooked up ioredis instances to Fastify lifecycle with fastify-redis and the test passed.

Jest initialize and shared objects once per test suite and across test cases

I want to use shared resources between jest test suites. I read in the internet and found that this could be the solution. But the setup is invoked per each test file.
I have two test files links.test.js and 'subscritpions.test.js'. I usually call them with one command jest and that all.
The problem is that the setup function of my custom environment custom-environment.js:
const NodeEnvironment = require('jest-environment-node');
const MySql = require('../../lib/databases/myslq/db');
class CustomEnvironment extends NodeEnvironment {
constructor(config) {
super(config)
}
async setup() {
await super.setup();
console.log(`Global Setup !!!!!!!!!`);
this.global.gObject = "I am global object"
this.global.liveUsers = await new MySql("Live Users");
this.global.stageUsers = await new MySql("Stage Users");
}
async teardown() {
console.log(`Global terdown !!!!!!!!!`);
await super.teardown();
this.global.gObject = "I am destroyed";
this.global.liveUsers.closeConnection();
this.global.stageUsers.closeConnection();
}
runScript(script) {
return super.runScript(script)
}
}
module.exports = CustomEnvironment;
is called twice for each test:
Global Setup !!!!!!!!!
Global Setup !!!!!!!!!
ERROR>>> Error: listen EADDRINUSE: address already in use 127.0.0.1:3306
So it tries to establish second connection to the same port - while I could simply use the existing connection.
The way it works seems to me makes no difference from defining
beforeAll(async () => {
});
afterAll(() => {
});
hooks.
So to wrap up, the question is: Using jest command (thus running all test suits), how can I invoke setup function once for all test and share global objects across them?
setup and teardown are indeed executed for each test suite, similarly to top-level beforeAll and afterAll.
Test suites run in separate processes. Test environment is initialized for each test suite, e.g. jsdom environment provides fake DOM instance for each suite and cannot be cross-contaminated between them.
As the documentation states,
Note: TestEnvironment is sandboxed. Each test suite will trigger setup/teardown in their own TestEnvironment.
The environment isn't suitable for global setup and teardown. globalSetup and globalTeardown should be used for that. They are appropriate for setting up and shutting down server instances, this is what documentation example shows:
// setup.js
module.exports = async () => {
// ...
// Set reference to mongod in order to close the server during teardown.
global.__MONGOD__ = mongod;
};
// teardown.js
module.exports = async function () {
await global.__MONGOD__.stop();
};
Since this happens in parent process, __MONGOD__ is unavailable in test suites.

Jest test passed but get Error: connect ECONNREFUSED 127.0.0.1:80 at the end

I'm using node with TypeScript on my back end and Jest and Supertest as my test framework on my back end.
When I'm trying to test I have the result pass but I get an error at the end. Here's the result:
PASS test/controllers/user.controller.test.ts
Get all users
✓ should return status code 200 (25ms)
console.log node_modules/#overnightjs/logger/lib/Logger.js:173
[2019-12-05T04:54:26.811Z]: Setting up database ...
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 3.284s
Ran all test suites.
server/test/controllers/user.controller.test.ts:32
throw err;
^
Error: connect ECONNREFUSED 127.0.0.1:80
at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1104:14)
npm ERR! Test failed. See above for more details.
Here's my test code:
import request from "supertest";
import { AppServer } from '../../config/server';
const server = new AppServer();
describe('Get all users', () => {
it('should return status code 200', async () => {
server.startDB();
const appInstance = server.appInstance;
const req = request(appInstance);
req.get('api/v1/users/')
.expect(200)
.end((err, res) => {
if (err) throw err;
})
})
})
Here's my server setup. I'm using overnightjs on my back end.
I created a getter to get the Express instance. This is coming from overnight.js.
// this should be the very top, should be called before the controllers
require('dotenv').config();
import 'reflect-metadata';
import { Server } from '#overnightjs/core';
import { Logger } from '#overnightjs/logger';
import { createConnection } from 'typeorm';
import helmet from 'helmet';
import * as bodyParser from 'body-parser';
import * as controllers from '../src/controllers/controller_imports';
export class AppServer extends Server {
constructor() {
super(process.env.NODE_ENV === 'development');
this.app.use(helmet());
this.app.use(bodyParser.json());
this.app.use(bodyParser.urlencoded({ extended: true }));
this.setupControllers();
}
get appInstance(): any {
return this.app;
}
private setupControllers(): void {
const controllerInstances = [];
// eslint-disable-next-line
for (const name of Object.keys(controllers)) {
const Controller = (controllers as any)[name];
if (typeof Controller === 'function') {
controllerInstances.push(new Controller());
}
}
/* You can add option router as second argument */
super.addControllers(controllerInstances);
}
private startServer(portNum?: number): void {
const port = portNum || 8000;
this.app.listen(port, () => {
Logger.Info(`Server Running on port: ${port}`);
});
}
/**
* start Database first then the server
*/
public async startDB(): Promise<any> {
Logger.Info('Setting up database ...');
try {
await createConnection();
this.startServer();
Logger.Info('Database connected');
} catch (error) {
Logger.Warn(error);
return Promise.reject('Server Failed, Restart again...');
}
}
}
I read this question - that's why I called the method startDB.
So I figured out and the solution is quite easy. I can't explain why though.
This req.get('api/v1/users/') should be /api/v1/users - you need a leading /.
For Frontend...
If you are making use of axios and come across this error, go to the testSetup.js file and add this line
axios.defaults.baseURL = "https://yourbaseurl.com/"
This worked for me. So, typically, this is a baseURL issue.
I had this error in my React frontend app tests.
I was using React testing library's findBy* function in my assert:
expect(await screen.findByText('first')).toBeInTheDocument();
expect(await screen.findByText('second')).toBeInTheDocument();
expect(await screen.findByText('third')).toBeInTheDocument();
After I changed it to:
await waitFor(async () => {
expect(await screen.findByText('first')).toBeInTheDocument();
expect(await screen.findByText('second')).toBeInTheDocument();
expect(await screen.findByText('third')).toBeInTheDocument();
});
the error is gone.
I don't know exactly why, but maybe it will help someone
UPDATE: I was mocking fetch incorrectly, so my test called real API and caused that error
I put this line in my setupTests file:
global.fetch = jest.fn()
It mocks fetch for all tests globally. Then, you can mock specific responses right in your tests:
jest.mocked(global.fetch).mockResolvedValue(...)
// OR
jest.spyOn(global, 'fetch').mockResolvedValue(...)
Slightly different issue, but same error message...
I was having this error when using node-fetch when trying to connect to my own localhost (http://localhost:4000/graphql), and after trying what felt like everything under the sun, my most reliable solution was:
using this script in package.json: "test": "NODE_ENV=test jest --watch"
If the terminal shows connection error I just go to the terminal with Jest watching and press a to rerun all tests and they pass without any issue.
¯\_(ツ)_/¯
Success rate continued to improve by renaming the testing folder to __tests__ and moving my index.js to src/index.js.
Very strange, but I am too exhausted to look at the Jest internals to figure out why.
The rules for supertest are the same as the rules for express. OvernightJS does not require any leading or ending "/" though.
For anyone landing on this, but not having issues with trailing slashes:
jest can also return a ECONNREFUSED when your express app takes some time (even just a second) to restart/init. If you are using nodemon like me, you can disable restarts for test files like --ignore *.test.ts.
This error also occurs if you have not set up a server to catch the request at all (depending on your implementation code and your test, the test may still pass).
I didn't get to the bottom of this error - it wasn't related to the (accepted) leading slash answer.
However, my "fix" was to move the mocks up into the suite definition - into beforeAll and afterAll for cleanup between tests).
Before, I was mocking (global.fetch) in each test, and it was the last test in the suite to use the mock that would cause the error.
In my case, the issue was related to package react-inlinesvg. Package makes a fetch request to get the svg file and since server is not running, it gets redirected to default 127.0.0.1:80.
I mocked react-inlinesvg globally to output props including svg filename to assert in testing.
jest.mock('react-inlinesvg', () => (props) => (
<svg data-testid="mocked-svg">{JSON.stringify(props)}</svg>
));

Is it okay to make API requests to set up tests in Cypress when testing serverless applications?

I'm writing a serverless React app using AWS Amplify. I do my E2E tests using Cypress.
Before each test, I log the user in via the AWS Amplify CLI. Afterwards, I clear all data on the development server and create some new data using fixtures. This way I always have controlled state for each test (see code below).
My question is: Is this a good practice? Or is it bad to make that many requests against the server in before each test? If it is bad, how would you do that if you do not have direct access to the server (again serverless) to run commands like cy.exec('npm run db:reset && npm run db:seed')? Cypress does warn me in the console about the use of promises:
Cypress detected that you returned a promise in a test, but also invoked one or more cy commands inside of that promise.
Here is the code I use:
import API, { graphqlOperation } from '#aws-amplify/api';
import Auth from '#aws-amplify/auth';
import Amplify from 'aws-amplify';
import * as R from 'ramda';
import config from '../../src/aws-exports';
import { contacts } from '../../src/contacts/fixtures';
import { replaceEmptyStringsWithNull } from '../../src/contacts/helpers';
import {
createContact as addMutation,
deleteContact as deleteMutation
} from '../../src/graphql/mutations';
import { listContacts } from '../../src/graphql/queries';
Amplify.configure(config);
const deleteContact = input =>
API.graphql(graphqlOperation(deleteMutation, { input }));
const createContact = input =>
API.graphql(graphqlOperation(addMutation, { input }));
describe('Contactlist', () => {
beforeEach(async () => {
await Auth.signIn(Cypress.env('email'), Cypress.env('password'));
const allContacts = await API.graphql(graphqlOperation(listContacts));
await Promise.all(
R.map(
R.pipe(
R.prop('id'),
id => ({ id }),
deleteContact
)
)(allContacts.data.listContacts.items)
);
await Promise.all(
R.map(
R.pipe(
R.dissoc('id'),
replaceEmptyStringsWithNull,
createContact
)
)(contacts)
);
});
// ... my tests
It would be exactly the way I would perform the test. I love to start with a fully controlled state, even if that means having multiple API-calls as a before()

Use jest.run() or jest.runCLI() to run all tests or ways to run jest programmatically

How do I use jest.run() or jest.runCLI() to run all tests programmatically? What am I suppose to feed as an argument?
I tried to find documentation regarding them but fail.
And if the above functions don't work, what am I supposed to call if I want to run jest programmatically?
Jest is not supposed to be run programmatically. Maybe it will in the future.
Try to run following:
const jest = require("jest");
const options = {
projects: [__dirname],
silent: true,
};
jest
.runCLI(options, options.projects)
.then((success) => {
console.log(success);
})
.catch((failure) => {
console.error(failure);
});
As success in then callback an object will be passed, containing globalConfig and results keys. Have a look on them, maybe it will help you.
From what I have experienced so far, utilizing run() requires to you define a static config and then pass arguments to Jest much like you would normally using the Jest CLI.
Utilizing runCLI() allows you to dynamically create a config and provide it to Jest.
I opted for the former just because I wanted to only expose a few of the Jest CLI options for a global configuration:
import jest from "jest";
import { configPaths } from "../_paths";
import { Logger } from "../_utils";
process.env.BABEL_ENV = "test";
process.env.NODE_ENV = "test";
const defaultArgs = ["--config", configPaths.jestConfig];
const log = new Logger();
const resolveTestArgs = async args => {
let resolvedArgs = [];
if (args.file || args.f) {
return [args.file || args.f, ...defaultArgs];
}
// updates the snapshots
if (args.update || args.u) {
resolvedArgs = [...resolvedArgs, "--updateSnapshot"];
}
// tests the coverage
if (args.coverage || args.cov) {
resolvedArgs = [...resolvedArgs, "--coverage"];
}
// runs the watcher
if (args.watch || args.w) {
resolvedArgs = [...resolvedArgs, "--watch"];
}
// ci arg to update default snapshot feature
if (args.ci) {
resolvedArgs = [...resolvedArgs, "--ci"];
}
// tests only tests that have changed
if (args.changed || args.ch) {
resolvedArgs = [...resolvedArgs, "--onlyChanged"];
}
return [...defaultArgs, ...resolvedArgs];
};
export const test = async cliArgs => {
try {
const jestArgs = await resolveTestArgs(cliArgs);
jest.run(jestArgs);
} catch (error) {
log.error(error);
process.exit(1);
}
};
Here's example from my post on How to run Jest programmatically in node.js (Jest JavaScript API).
This time using TypeScript.
Install the dependencies
npm i -S jest-cli
npm i -D #types/jest-cli #types/jest
Make a call
import {runCLI} from 'jest-cli';
import ProjectConfig = jest.ProjectConfig;
const projectRootPath = '/path/to/project/root';
// Add any Jest configuration options here
const jestConfig: ProjectConfig = {
roots: ['./dist/tests'],
testRegex: '\\.spec\\.js$'
};
// Run the Jest asynchronously
const result = await runCLI(jestConfig as any, [projectRootPath]);
// Analyze the results
// (see typings for result format)
if (result.results.success) {
console.log(`Tests completed`);
} else {
console.error(`Tests failed`);
}
Also, regarding #PeterDanis answer, I'm not sure Jest will reject the promise in case of a failed tests. In my experience it will resovle with result.results.success === false.
If all your configs are in the jest.config.js, you can just code like this:
const jest = require('jest')
jest.run([])

Resources