Using environment variables in Node and Jest - node.js

I have a NodeJS app. For a long time I've been using Mocha to run tests. I simply had a package.json like
"dev": "node index --env=dev",
"start": "node index",
"test": "mocha './test/**/*.spec' --env=dev"
Now I am switching to Jest for a variety of reasons, and the test script becomes simply
"test":"jest"
Within my test suites, I spin up an instance of my app. In functions throughout my app there are decisions based on the environment. Simplified example:
const ProcessArgs = require("minimist")(process.argv);
export function uploadImage(image){
if(ProcessArgs.env === "dev"){
return writeToLocalTestFolder(image);
}
else {
return uploadToCloud(image);
}
}
So this used to look at the --env=dev arg, but that's no longer there. (If I try to add --env=dev to Jest, well that's an entirely different issue). I do in fact properly use Heroku's secrets for my actual important env variables like API keys, etc, but I now seem to be stuck here on how I should tell Jest what env it is in, since it needs env=node to work properly.

Mocking out environment variables in Jest is a little bit more tricky as there really is no way to mock them out before the code that you want to import gets evaluated. However, there is a handy workaround. You can mock out the library that is contains the environment variables and have it return specifically what you need.
Referring to your example, you can mock out what environment variables are returned by minimist by using the following code (make sure it is used before the describe of your test suite:
jest.mock('minimist', () => {
return () => ({ env: 'dev'});
});
The if(ProcessArgs.env === "dev"){ conditional check should run as expected.

Related

Can you test CDK constructs with Mocha instead of Jest?

The developer guide makes it sound like you can only use jest with the cdk. However, our projects currently use mocha. We could use jest for the cdk and keep everything else the same, but we're wondering if anyone has had any luck using mocha to test the cdk.
EDIT:
So far, it seems to have worked with this simple set up:
installed ts-node, mocha
ran with mocha -r ts-node/register file.test.ts
file.test.ts:
import { expect as expectCDK, matchTemplate, MatchStyle } from '#aws-cdk/assert';
import * as cdk from '#aws-cdk/core';
import * as Infrastructure from '../lib/infrastructure-stack';
it('Empty Stack', () => {
const app = new cdk.App();
// WHEN
const stack = new Infrastructure.InfrastructureStack(app, 'MyTestStack');
// THEN
expectCDK(stack).to(matchTemplate({
"Resources": {}
}, MatchStyle.EXACT))
});
Has anyone tried more complicated tests?
I believe the cdk assert library was written with jest in mind but I couldn't find any classes that will only work with jest so it should work with any test framework since they provide their own expectations function, cdkExpect

Jest Mock Globally From a Node Module

I am writing a series of Node modules that require a bunch of common Jest mocks to be setup before I can write unit tests.
I am trying to refactor the unit test setup into a separate module so I can avoid rewriting the setup each time.
The problem is that when I import the following code as a module it no longer mocks the other libraries, while it works just fine when the mocks are set up in a utility file.
Working Code:
jest.mock('#actions/core')
jest.mock('#actions/github')
const { GitHub, context} = require('#actions/github')
const core = require('#actions/core')
GitHub.mockImplementation(() => {
return {
{
repos: {
getContents: jest.fn()
}
}
}
}
module.exports = { core, GitHub, context }
I keep this in a utils.js file next to my test files and import it like const { core, GitHub, context } = require('./utils.js') and everything mocks as I expect. I can run expect().toHaveBeenCalledTimes() and get the numbers I expect.
The problem appears when I move utils.js to another module and require it.
I know that at bottom of the jest documentation it says "...Another file that imports the module will get the original implementation even if it runs after the test file that mocks the module." But I am seeing this work inconsistently with the external file.
Anyone know how to get mocking setup in external modules?

How to enable Intl in node for tests using Jest?

I am trying to test currency formatting functions and while they work in the browser, they are not working in my test environment. I understand that intl does not come standard with node, so I have added intl and full-icu to my devDependencies, but this has not helped
var IntlPolyfill = require('intl');
require('full-icu');
Intl.NumberFormat = IntlPolyfill.NumberFormat;
Intl.DateTimeFormat = IntlPolyfill.DateTimeFormat;
expect((4000).toLocaleString('fr-ca',{style: 'currency',currency: 'CAD',currencyDisplay: 'symbol'})).toBe('4 000,00 $')
/* test output:
Expected: "4 000,00 $"
Received: "CA$ 4,000.00"
*/
In IE11, Chrome and Firefox I get the expected result ("14 337,00 $") but in Node I am not.
I have found little help online, but did find reference to the 2 libraries I am using here. What do I need to do for the node environment used by Jest to have access to the proper Intl functions. The function is there, but seems to be returning the US formatting for every locale.
First comes my answer that immediately came into my mind and solves the problem, but it in fact is an anti-pattern - I simply leave it here so others can learn from it. The good solution follows further down in my answer:
You simply can import the node module intl into you Jest test and pass it to the JS you are testing:
import Intl from 'intl';
import CurrencyFormatter from '../../../js/modules/CurrencyFormatter.js';
describe('CurrencyFormatter', () => {
let formatter = null;
beforeEach(() => {
formatter = new CurrencyFormatter(Intl);
});
test([...]);
});
And in your JS you conditionally use the intl module if passed by Jest:
export default class CurrencyFormatter {
constructor(optionalIntl) {
// Needed for Jest, because it doesn't support Intl natively:
if (optionalIntl != null) {
global.Intl = optionalIntl;
}
[...]
}
[...]
}
This way you keep the superfluous intl module out of your production code and make it available to Jest for tests only. But as you can clearly see, this means adjusting the production code for the sole purpose of satisfying the test's needs. This is called an anti-pattern. The production code should always serve production needs (and readability/understandability for other devs, ofc), not the testing environment.
So for that reason you should rather follow the second solution, which adidtionally has the advantage of fixing the issue for all similar tests in the suite and of using much less code changes. In fact it does not change the code but only the configuration:
[Edit:]
As a colleague mentioned, this indeed is an anti-pattern - do not adapt your production code in order to pass the test. Yet, there is a better option, using the node module full-icu by passing it to node in the scripts entry for jest, i.e. do something like this in your package.json:
[...]
"scripts": {
"test": "node --icu-data-dir=node_modules/full-icu node_modules/jest/bin/jest.js --version --config=./jest.config.js"
},
[...]

Jest global variables that persist

I see that global variables can be specified across Jest tests but that:
mutation will not be persisted across test runs for other test files.
Is there way to make changes to global variables that persist across all test files?
You can use Environment Variables to carry those variables across tests, even if you have multiple test files. Official docs states below that any global variable cannot be accessed through tests:
Note: Any global variables that are defined through globalSetup can only be read in globalTeardown. You cannot retrieve globals defined here in your test suites.
But the tests use the same environment so you can set and change variables accordingly.
Create globalSetup in config with following file:
// setup.js
module.exports = async () => {
process.env.TEST_VAR = "my-test-var";
};
You can use this variable in your tests:
// my-test.js
// ...
it("should read the test var from environment", async (done) => {
expect(process.env.TEST_VAR).toEqual("my-test-var");
process.env.TEST_VAR = "new-var";
expect(process.env.TEST_VAR).toEqual("new-var");
done();
});
// ...
You can unset this variable ones the test are complete with globalTeardown config with the following file:
// teardown.js
module.exports = async () => {
delete process.env.TEST_VAR;
};
Remember that setting and not properly using environment variables are risky that may alter your test results. I use it to pass around a persistent variable.
I do not think there is a way to do that. I also do not think there should be a way to do this. Test cases should be able to run independently from one another. Allowing some mutation to a global variable to be persisted across all test files might create a situation in which a test case will only succeed if it is run after another test case has mutated some variable. This situation might result in very fragile tests and should be avoided.
If you need a global variable (or any other variable for that matter) to be in a certain state for a test to succeed. You should look into the setup and teardown methods of Jest.

Changing mocha tests to jest tests

I'm having trouble changing my mocha tests to jest tests.
I have three test files with three classes: FirstTestGroup, SecondTestGroup and ThirdTestGroup, each with a static execute method, that contains some tests, like this:
class FirstTestGroup {
execute(params) {
describe('some tests', function tests() {
it('one test', () => {
// uses params
});
...
});
...
}
}
Each of those execute methods use the same parameters. These parameters are created in an async before call, like shown bellow.
describe('My Tests', function testSuite() {
let params;
before('param creation', async function asyncFunc() {
// creates params asynchronously
});
it('should pass all', () => {
FirstTestGroup.execute(params);
SecondTestGroup.execute(params);
ThirdTestGroup.execute(params);
});
});
The it('should pass all', ...) is needed because everything inside a describe is run instantly, so params would be passed as null without it. This works in mocha because "it"s can be nested, but apparently this is not the case for jest.
I could make the beforeAll (equivalent of before in jest) be called each time before a test group is run, but I didn't want to do that as this seems inefficient.
I could also place the code inside the before call before the describe('My Tests', ...) is defined. This seems wrong as this should be part of the describe only.
I couldn't find anything in the jest documentation that could help me with that.
Am I doing something wrong? Is there a way to achieve this using jest, even if I have to restructure the tests? But I'd like to keep the tests in different files.
This is my first question here, so please tell me if more info is needed as I'm not used writting here.
Have you tried Jest-Codemods yet?
Jest-Codemods allows you to convert your Mocha, AVA, Jasmine tests into equivalent Jest tests. It would also help you migrate assertion libraries such as Chai and Expect.
The way you do it:
Install jest-codemods with npm install -g jest-codemods
Go to your project and execute jest-codemods
It will ask you Which test library would you like to migrate from?
Select Mocha by using arrow keys (As you want to migrate from Mocha)
Next, It will ask you Will you be using Jest on Node.js as your test runner?
Select Yes, use the globals provided by Jest (recommended)
Last, You need to provide file name on which you are working to migrate from Mocha to Jest.
And you are done!
It will automatically migrate your code from Mocha to Jest. No need to touch code. That's one of the most powerful feature of Jest which would save your time and you don't need to worry about changing mocha tests to jest tests.

Resources