Setup
I want to unit test my electron app with jest. For this I have the following setup:
I use jest and use #kayahr/jest-electron-runner to run it with electron instead of node. Additionally, since it is a typescript project, I use ts-jest.
My jest.config.js looks like this:
module.exports = {
collectCoverage: true,
coverageDirectory: 'coverage',
coverageProvider: 'v8',
preset: 'ts-jest',
runner: '#kayahr/jest-electron-runner/main',
testEnvironment: 'node',
};
The test is expected to run in the main process. I have reduced my code to the following example function:
import { app } from 'electron';
export function bar() {
console.log('in bar', app); //this is undefined when mocked, but I have a real module if not mocked
const baz = app.getAppPath();
return baz;
}
The test file:
import electron1 from 'electron';
import { bar } from '../src/main/foo';
console.log('in test', electron1); //this is undefined in the test file after import
// jest.mock('electron1'); -> this does just nothing, still undefined
const electron = require('electron');
console.log('in test after require', electron); //I have something here yay
jest.mock('electron'); //-> but if I comment this in -> it is {} but no mock at all
it('should mock app', () => {
bar();
expect(electron.app).toBeCalled();
});
What do I want to do?
I want to mock electron.app with jest to look whether it was called or not.
What is the problem?
Mocking electron does not work. In contrast to other modules like fs-extra where jest.mock() behaves as expected.
I don't understand the behavior happening here:
Importing "electron" via import in the file containing the tests (not the file to be tested!) does not work (other modules work well), but require("electron") does.
I do have the electron module if not mocked in bar(), but after mocking not
while jest.mock("fs-extra") works, after jest.mock("electron") electron is only an empty object, not a mock
I would really like to understand what I did wrong or what the problem is. Switching back to #jest-runner/electron does not seem to be an option, since it is not maintained anymore. Also I don't know if this is even the root of the problem.
Has anyone seen this behavior before and can give me a hint where to start searching?
A bit fuzzy title, but could not describe it more clearly.
I have a repository containing reusable functions and React components which we use in other React applications. We use jest & testing library to test our React applications. When testing the application the re-usable functions are mocked.
jest.mock('#myorg/reusable-fnandcomp', () => ({
// #ts-ignore
...jest.requireActual('#myorg/reusable-fnandcomp'),
submitEvent: jest.fn(),
}));
In the repo containing the reusable functions and components, the components make use of the same reusable functions. I import the functions using relative paths
import { submitEvent } from '../index';
Instead of
import { submitEvent } from '#myorg/reusable-fnandcomp';
Because the submitEvent cannot be imported if it is not published in the repository. (typical chicken-and-egg situation).
This works fine, except when the submitEvent needs to be mocked in the React application. Because the submitEvent in the reusable component is imported with ../index, the jest.mock in the test of the application does not match with #myorg/reusable-fnandcomp. Therefore the actual implementation of submitEvent is invoked instead of the mock.
In pseudo code;
reusable repo
../components/SomeComp.JSX
import { submitEvent } from '../index';
export const SomeComp = () => {
submitEvent()
}
../functions/submitEvent.TS
export const submitEvent = () => {
// do something here
}
React application
../tests/sometest.spec.JSX
import {
SomeComp,
submitEvent
} from '#myorg/reusable-fnandcomp';
jest.mock('#myorg/reusable-fnandcomp', () => ({
// #ts-ignore
...jest.requireActual('#myorg/reusable-fnandcomp'),
submitEvent: jest.fn(),
}));
render(<SomeComp/>)
expect(submitEvent).toHaveBeenCalled() // actual implementation called here
I understand why the actual implementation is invoked. A resolution would be by adding the #myorg/reusable-fnandcomp as a development dependency to the project. Then first publish this repository and then use import { submitEvent } from '#myorg/reusable-fnandcomp'; where i currently use import { submitEvent } from '../index'; and re-publish this. But this seems to be a bit ugly. Are there suggestions how to resolve this better/properly ?
Comment
From my personal view the expect(submitEvent).toHaveBeenCalled() should not be part of the test of the React component. It should have been tested in the repository containing the function. But since it is an existing repository which i got under control i was wondering if there is not a better approach.
I've tried a few different ways of doing this.
I can't set Winston as the default logger for NestJS at the moment because it complains about "getTimestamp" function not being in the instance.
So - for controllers in NestJS - I have used dependency injection - which works fine for the api ( REST endpoints ).
The problem is that I have moved away from OOP - so all of my libraries are written in typescript as functions. Not pure functions but better than an OOP approach ( many less bugs! )
My question is - how do I get access to the main winston logger within my libraries that don't have classes.
I am using the library nest-winston.
Have you tried this?
create the logger outside of the application lifecycle, using the createLogger function, and pass it to NestFactory.create (nest-winston docs)
You can have a separate file that creates the logging instance, then import that into your modules/libraries as well as import it into your main.ts
// src/logger/index.ts
import { WinstonModule } from 'nest-winston';
export const myLogger = WinstonModule.createLogger({
// options (same as WinstonModule.forRoot() options)
})
// src/myLib/index.ts
import { myLogger } from '#/logger' // I like using aliases
export const myLib = () => {
// ...
myLogger.log('Yay')
}
// src/main.ts
import { myLogger } from '#/logger'
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
logger: myLogger
});
}
bootstrap();
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>
));
I implemented basic caching functionality for a project and ran into a problem during the testing.
I test using jest and redis-mock and all the tests pass.
The problem is when I import a file which imports the redis-file.
The test-file doesn't exit.
Example:
index.test.js
import redis from 'redis'
import redis_mock from 'redis-mock'
jest.spyOn(redis, 'createClient').mockImplementation(red_mock.createClient)
import fileUsingRedis from './index'
describe('index', () => {
it('should pass', () => expect(true).toBeTruthy())
}
index.js
import {set} from './redis'
export default function ...
redis.js
import redis from 'redis'
const client = redis.createClient()
export function set(key, value) {...}
'1 passed'...'Ran all test suites matching ...'
But then it keeps waiting, I assume because the redis.createClient() is async or something or other. Seeing as it happens on the import I can't just resolve it.
Do I have to close the redis-instance connection after every test?
What is the solution/best-practice here?
So yeah, closing the instance did it.
index.test.js
import redis from './redis'
...
afterAll(() => redis.closeInstance())
redis.js
export function closeInstance(callback) {
client.quit(callback)
}