How to run OS-agnostic Jest test files that check paths? - node.js

Let's say I have the following:
expect(func).toHaveBeenCalledWith('/path/to/file');
This would work fine on NIX operating systems. However, this test would fail on Windows platform because I should instead have
expect(func).toHaveBeenCalledWith('\path\to\file');
What's the best way to write tests so they are OS agnostic? I was looking at this article but that is basically saying write different tests for different OSes.

In general, you can extend expect to add the matching behaviour you want, there are lots of examples in jest-extended. For this case, perhaps using the tools available in path to test against the appropriate path for whatever OS the tests are running on:
import { matcherHint, printExpected, printReceived } from "jest-matcher-utils";
import * as path from "path";
expect.extend({
toMatchPath: (actual, expected) => {
const normalised = path.join(...expected.split("/"));
return actual === normalised
? { pass: true, message: passMessage(actual, normalised) }
: { pass: false, message: failMessage(actual, normalised) };
},
});
const passMessage = (actual, expected) => () => `${matcherHint(".not.toMatchPath")}
Expected value not to match:
${printExpected(expected)}
Received:
${printReceived(actual)}`;
const failMessage = (actual, expected) => () => `${matcherHint(".toMatchPath")}
Expected value to match:
${printExpected(expected)}
Received:
${printReceived(actual)}`;
In your tests you then always write POSIX-style paths /path/to/thing, and path takes care of providing the appropriate path separator for the current OS. In use:
describe("path matching", () => {
const actual = path.join("path", "to", "thing");
it("normalises paths for matching", () => {
expect(actual).toMatchPath("path/to/thing");
});
it("can be negated", () => {
expect(actual).not.toMatchPath("path/to/other/thing");
});
it("can be used asymmetrically", () => {
const fn = jest.fn();
fn(actual);
expect(fn).toHaveBeenCalledWith(expect.toMatchPath("path/to/thing"));
});
it("fails usefully", () => {
const fn = jest.fn();
fn(actual);
expect(fn).toHaveBeenCalledWith(expect.not.toMatchPath("path/to/thing"));
});
});
Output:
path matching
✓ normalises paths for matching (3 ms)
✓ can be negated
✓ can be used asymmetrically (2 ms)
✕ fails usefully (3 ms)
● path matching › fails usefully
expect(jest.fn()).toHaveBeenCalledWith(...expected)
Expected: not.toMatchPath<path/to/thing>
Received: "path/to/thing"
Number of calls: 1
44 | const fn = jest.fn();
45 | fn(actual);
> 46 | expect(fn).toHaveBeenCalledWith(expect.not.toMatchPath("path/to/thing"));
| ^
47 | });
48 | });
at Object.toHaveBeenCalledWith (server/demo.test.js:46:14)
Test Suites: 1 failed, 1 total
Tests: 1 failed, 3 passed, 4 total
Snapshots: 0 total

Related

How to customize the format of the test output in Node.js native testing module with existing Node tap formatters?

I read here an intriguing snippet:
Back to the Node.js test runner, it outputs in TAP, which is the “test anything protocol”, here’s the specification: testanything.org/tap-specification.html.
That means you can take your output and pipe it into existing formatters and there was already node-tap as a userland runner implementation.
The default native Node.js test output is terrible, how can I wire up a basic hello-world test using a community TAP formatter?
This tap-parser seems like a good candidate, but what I'm missing is how to connect that around the Node.js node:test module. Can you show a quick hello world script on how to write a test and customize the output formatting using something like this parser, or any other mechanism?
I have this in a test file:
import { run } from 'node:test'
const files = ['./example.test.js']
// --test-name-pattern="test [1-3]"
run({ files }).pipe(process.stdout)
And I have in example.test.js:
import test from 'node:test'
test('foo', () => {
console.log('start')
})
However, I am getting empty output:
$ node ./test.js
TAP version 13
# Subtest: ./example.test.js
ok 1 - ./example.test.js
---
duration_ms: 415.054557
...
1..1
# tests 1
# pass 1
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 417.190938
Any ideas what I'm missing? When I run the file directly, it works:
$ node ./example.test.js
TAP version 13
# Subtest: foo
ok 1 - foo
---
duration_ms: 1.786378
...
1
# tests 1
# pass 1
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 210.586371
It looks like if I throw an error in the nested example.test.js test, the error will surface in the test.js file. However, how do I get access to the underlying test data?
If anyone else is interested, I found another (a bit hacky) solution, to get all the Tap data as string (and then do whatever withit):
// getTapData.js
import {run} from 'node:test';
const getTapDataAsync = (testFiles) => {
let allData = '';
return new Promise((resolve, reject) => {
const stream = run({
files: testFiles,
});
stream.on('data', (data) => (allData += data.toString()));
stream.on('close', (data) => resolve(allData));
stream.on('error', (err) => reject(err));
});
};
Then, you can call that function and get all of the Tap/Node unit test data as string:
// testRunner.js
const tapAsString = await getTapDataAsync(testFiles);
console.log(tapAsString);
Seems the best way so far is to just go low-level like this:
import cp from 'child_process'
import { Parser } from 'tap-parser'
const p = new Parser(results => console.dir(results))
p.on('pass', data => console.log('pass', data))
p.on('fail', data => console.log('fail', data))
const child = cp.spawn(
'node',
['./host/link/test/parser/index.test.js'],
{
stdio: [null, 'pipe', 'inherit'],
},
)
child.stdout.pipe(p)

why can't you mock a re-exported primitive value?

I'm trying to change the value of a primitive config object during tests. One of my files under test re-exports a primitive that is conditional on the config values.
I'm finding that when the value is wrapped in a function, then mocking it and asserting on it works perfectly.
However when the value is re-exported as a primitive, the value is not mocked, and is undefined.
Simplified example:
config.ts
export const config = {
environment: 'test'
};
app.ts
import { config } from './config';
export const app = () => config.environment;
export const environment = config.environment;
app.spec.ts
import { app, environment } from './app';
import * as config from './config';
jest.mock('./config', () => ({
config: {},
}));
beforeEach(() => {
jest.resetAllMocks();
});
const mockConfig = config.config as jest.Mocked<typeof config.config>;
test('app', () => {
mockConfig.environment = 'prod';
expect(app()).toEqual('prod');
});
test('environment', () => {
mockConfig.environment = 'nonprod';
expect(environment).toEqual('nonprod');
});
The first test passes, but the second test "environment" fails. Why?
✕ environment (3 ms)
● environment
expect(received).toEqual(expected) // deep equality
Expected: "nonprod"
Received: undefined
19 | test('environment', () => {
20 | mockConfig.environment = 'nonprod';
> 21 | expect(environment).toEqual('nonprod');
| ^
22 | });
23 |
at Object.<anonymous> (config/app.spec.ts:21:29)
The problem could be related with the order files are read. The app file is the first one to be read, and then the config file is read because its imported on the app one. But, probably the app code run first, so the variable was set as undefined (because the config one had not a value at the time).
The same does not happen with the app function, because it reads the config variable only after the function is called. And at that time, the variable already was set.

Response.body is empty in this test that uses supertest and node.js

Using supertest to test a node.js api for unit and integration testing but the test for integration keep failing; I don't know why I keep receiving an empty object in the reponse.body while I am expecting a result of 30 from the AddNum object. the error comes from the file app.test.js line 19
This is my code for app.test.js
const supertest = require('supertest');
const server = require('../../app');
describe("Calculate", () => {
it('POST /calculate: action: sum', async () => {
const AddNum = {
action: 'sum',
num1: 20,
num2: 10
}
const response = await supertest(server).post("/calculate").send(AddNum)
console.log({response})
expect(response.status).toBe(200)
const expectResult = {result:30}
expect(response.body).toBe(expectResult)
})
})
This is my error from running the test in the command line.
at Object.log (test/integration/app.test.js:15:17)
● Calculate › POST /calculate: action: sum
expect(received).toBe(expected) // Object.is equality
- Expected - 3
+ Received + 1
- Object {
- "result": 30,
- }
+ Object {}
17 |
18 | const expectResult = {result:30}
> 19 | expect(response.body).toBe(expectResult)
| ^
20 | })
21 | })
at Object.toBe (test/integration/app.test.js:19:31)
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.
Test Suites: 1 failed, 1 passed, 2 total
Tests: 1 failed, 4 skipped, 1 passed, 6 total
Snapshots: 0 total
Time: 2.556 s
Ran all test suites.

ignore setup mock for one file

I have a decorator that I use on a lot of my methods. To not have to mock it each time, I have added a mock on jest.setup.js:
jest.mock('src/something', () => {
someMethod: jest.fn.mockImplementation(/*some implementation*/)
})
This works fine, but now I want to unit test this one method (the someMethod in this example) and I can't, since it brings up the mock. How can I ignore this mock for only this file/test?
You can use jest.unmock(moduleName) in the test file of a module. So that your module under test will use the real module rather than the mocked version.
Let's see an example:
./src/stackoverflow/73129547/something.ts:
export const someMethod = () => 1;
This is the module we want to mock.
./jest.setup.js:
jest.setTimeout(5 * 1000);
jest.mock('./stackoverflow/73129547/something', () => ({
someMethod: jest.fn().mockReturnValue(0),
}));
jest.config.js:
module.exports = {
preset: 'ts-jest/presets/js-with-ts',
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['./jest.setup.js'],
};
We set up the mock using jest.mock() in the jest.setup.js file. So that every module under test will use the mocked version of this module.
Suppose our project has two modules: a.ts and b.ts, they use the someMethod exported from the something module.
./src/stackoverflow/73129547/a.ts:
import { someMethod } from './something';
export const a = someMethod;
./src/stackoverflow/73129547/b.ts:
import { someMethod } from './something';
export const b = someMethod;
The test suites of a and b modules:
./src/stackoverflow/73129547/a.test.ts:
import { a } from './a';
describe('73129547 - a', () => {
test('should pass', () => {
expect(a()).toBe(0);
});
});
./src/stackoverflow/73129547/b.test.ts:
import { b } from "./b";
describe('73129547 - b', () => {
test('should pass', () => {
expect(b()).toBe(0);
});
});
Test result:
PASS stackoverflow/73129547/b.test.ts
PASS stackoverflow/73129547/a.test.ts (9.829 s)
Test Suites: 2 passed, 2 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 10.681 s
As you can see the expected execution result. Both the a and b modules use the mocked someMethod which has a mock return value: 0.
Now, we want to test the someMethod of the something module, we should test the real someMethod rather than the mocked one. Test mock implementations make no sense.
./src/stackoverflow/73129547/something.test.ts:
import { someMethod } from './something';
jest.unmock('./something');
describe('73129547 - something', () => {
test('should pass', () => {
expect(someMethod()).toBe(1);
});
});
Test result:
PASS stackoverflow/73129547/a.test.ts
PASS stackoverflow/73129547/b.test.ts
PASS stackoverflow/73129547/something.test.ts
Test Suites: 3 passed, 3 total
Tests: 3 passed, 3 total
Snapshots: 0 total
Time: 1.955 s, estimated 14 s
As you can see, the return value of real someMethod is 1. jest.unmock(moduleName) will unmock the module and return the real module.

Jest automatic-mocks: funny example outcome

this refers to the facebook example tutorials
// utils.js
// Copyright 2004-present Facebook. All Rights Reserved.
export default {
authorize: () => 'token',
isAuthorized: (secret) => secret === 'wizard',
};
below is the test file. Instead of adding auto mock at the config file, I added inside the code to show the differences.
import utils from './utils';
jest.enableAutomock();
test('implementation created by automock', () => {
expect(utils.authorize('wizzard')).toBeUndefined();
expect(utils.isAuthorized()).toBeUndefined();
});
outcome:
TypeError: Cannot read property 'default' of undefined
6 |
7 | test('implementation created by automock', () => {
> 8 | expect(utils.authorize('wizzard')).toBeUndefined();
| ^
9 | expect(utils.isAuthorized()).toBeUndefined();
10 | });
11 |
at Object.utils (__tests__/example/automatic-mocks/genMockFromModule.test.js:8:10)
Why is that? it happens to another file automock.test.js. The error message is the same.
// Copyright 2004-present Facebook. All Rights Reserved.
import utils from './utils';
jest.enableAutomock();
test('if utils are mocked', () => {
expect(utils.authorize.mock).toBeTruthy();
expect(utils.isAuthorized.mock).toBeTruthy();
});
test('mocked implementation', () => {
utils.authorize.mockReturnValue('mocked_token');
utils.isAuthorized.mockReturnValue(true);
expect(utils.authorize()).toBe('mocked_token');
expect(utils.isAuthorized('not_wizard')).toBeTruthy();
});
Below example works for me, I use jestjs with typescript and ts-jest.
the docs say:
Note: this method was previously called autoMockOn. When using babel-jest, calls to enableAutomock will automatically be hoisted to the top of the code block. Use autoMockOn if you want to explicitly avoid this behavior.
utils.ts:
const utils = {
getJSON: data => JSON.stringify(data),
authorize: () => 'token',
isAuthorized: secret => secret === 'wizard'
};
export default utils;
utils.spec.ts:
jest.enableAutomock();
import utils from './utils';
describe('automatic mocks test suites', () => {
it('should mock all methods of utils', () => {
expect((utils.getJSON as jest.Mock).mock).toBeTruthy();
expect(jest.isMockFunction(utils.authorize)).toBeTruthy();
expect(jest.isMockFunction(utils.isAuthorized)).toBeTruthy();
});
test('implementation created by automock', () => {
expect(utils.authorize()).toBeUndefined();
expect(utils.isAuthorized('wizard')).toBeUndefined();
});
it('mocked implementation', () => {
(utils.getJSON as jest.Mock).mockReturnValue(123);
expect(utils.getJSON({ name: 'test' })).toBe(123);
});
});
Unit test result:
PASS src/automatic-mocks/utils.spec.ts (17.906s)
automatic mocks test suites
✓ should mock all methods of utils (4ms)
✓ implementation created by automock (2ms)
✓ mocked implementation (1ms)
Test Suites: 1 passed, 1 total
Tests: 3 passed, 3 total
Snapshots: 0 total
Time: 22.923s, estimated 23s
Source code: https://github.com/mrdulin/jest-codelab/tree/master/src/automatic-mocks

Resources