SyntaxHighlighter

Wednesday, April 15, 2020

Integration Tests in Jest - Specific Group Serialization

Jest is a cool testing framework for NodeJS, and was adopted by my favorite API framework actionheroJS. In past projects I used Mocha, but to try something new I'm using Jest.

Background

I like to write integration tests when testing my APIs (I find them the most meaningful) which typically has a group of API calls in a single test suite. User Permissions, Client Configurations, Business Processes, etc are tested together.  Most times a folder contains the group of test for that suite, and tests are expected to run sequentially because the data created for the "create" api calls could be used for the "update" "read" and "destroy" API calls as well.

Enter Jest. Every test file is able to run in parallel and the state is independent of the other tests. Serial in a single file and parallel across many files. Great for running all the tests, but makes it hard to share state between them. To group my API calls I could either:
  1. Put them all in a single file to test out the entire suite. Or...
  2. Build up some testing data before each suite runs. 
I did not like #1 because the files would get huge. Each group might have 4-15 API calls that need exercised, plus setup/teardown and a few error cases. That's a lot of lines. Code folding in my IDE?

I did not like #2 too much either - custom testing code to "build the data" at the start of each test, plus a 5 sec or so boot time for a backend API instance to start on the server. Smaller files leads to a lot of boots and a lot of time (even with extra cores and Jest workers). Perhaps a DB seed file could ensure certain data would be available, but that's as dynamic as building on the fly and it's one more thing to maintain for the test suite.

What Does Not Work

To find a solution, I learned some things.

First, you cannot create a single, shared global to use across all your test suites. Lots of chatter about that here, but the summary is only JSON-serializable things can be global values. API tokens or WebSocket URLs? Sure. Instantiated objects with functions and all sorts of other initialized data, like an actionhero api instance?  No way. Each test suite is running in its own worker thread, meaning it has its own scope and cannot "talk to" the others (and don't try because they're all running parallel which could lead to craziness!).

Second, and this is true for many Node test frameworks (Mocha for sure), is that test setup (the describes) is synchronous while test running (it(), test(), beforeEach(), etc) is asynchronous. For Jest, you cannot instantiate a variable inside a describe() block that you want to use in your tests. You can initiate a variable, but must instantiate inside a test() or beforeAll() function.  The describe() functions all run at the start of testing synchronously, to setup what tests will run. The actual tests and variable instantiation happen when the test run, so if the variable was assigned in a describe() block, and not a test block, that assignment took place in the setup phase and likely does not contain what you expect it to (often leading to "cannot read property X of undefined" errors).

Solution

Let's jump into the solution, and it was as "simple" as sharing the API instance in the worker process scope in the same way that the api instance is used in the actionhero api.
Here we have a single actionhero instance started before all the test suites are run, and stopped after. Describe blocks are run in the order they are defined, so we describe the suites here, but the contents are in other files and can define their own beforeAll() or afterEach() functions. The convention is that are in their own describe() block.

Be sure to put that __test__/lib directory in your ignore patterns, or Jest will try to run them.

"jest": {
"testPathIgnorePatterns": [
"./__tests__/lib/*/*"
],

It is possible to change "describe('Create', createTests)" to "createTests()" by changing the export of the create.ts to be like this:

export default () => {
return describe('Create', () => {

I do not prefer it that way because some information would be hidden in 00-template-tests.ts, but it's an option!

You could probably do some fancy "walked directory" imports to find all the files in ABC order and include them in your suite - depending how large your suite grows or how annoying you find it to update the main test file.

That's how I'm running Jest tests in sequential groups, but maintaining parallel suites for all part of my API. Working for now, and I'll update this post if something goes terribly wrong : )