Skip to main content

Command Palette

Search for a command to run...

Incremental Testing Library Migration: A Step-by-Step Guide: Part 1

Combining Jest and Jasmine: Seamless Integration and Coverage Report Merging

Updated
β€’11 min read
Incremental Testing Library Migration: A Step-by-Step Guide: Part 1

At some point in your career, you’ll encounter a project developed by numerous developers who are no longer with the company. In such cases, you’ll likely discover a variety of tools used across the repositories, installed by different people over time, often serving the same purpose.

That was my situation working for a client with a multi-repo project using numerous testing tools. We had Jest, Jasmine with Karma, and the trio: Mocha, Sinon, and Chai; for unit and integration tests in different repositories, plus other supporting tools like rewire and proxyquire, and mock utilities.

The Problem

Some problems with this setup are quite clear:

  • Switching between libraries, even for the same task, because you need to work with code in different repos.

  • Many unnecessary package updates due to new features and outdated packages.

  • No unified way to write tests.

  • Constantly needing to check different documentation pages whenever you write a test.

It was something:

❌ Difficult to work with

❌ Tedious

❌ Exhausting

For us, using a single testing library seemed a better option.

The Decision

As a team, we decided to standardize the testing tools across the repositories by migrating all tests to a single tool and removing the others.

We chose Jest because it is modern, stable, and has excellent documentation. We appreciated that it includes everything we need, like expectations and mock utilities, and offers many customization options if required. Additionally, our current team has extensive experience with Jest.

Another advantage is that it’s widely used by developers, making it easier to find someone who can start writing tests right away if needed.

🚧 Disclaimer

For simplicity, in this article, I used Jasmine as the old testing tool we wanted to migrate from in favor of Jest. However, the same principles and strategies discussed here can be, and in our case were, applied to mocha and other testing tools as well.

The Plan

If we were going to do this, we needed to make a few agreements.

Since we had hundreds of test files per repository, and migrating those files wasn't automatic or our main priority, we had to ensure they were migrated to Jest incrementally.

βœ… Allow incremental migration

As the migration would happen slowly and incrementally, we had to ensure the project was never left unprotected. Regardless of the migration stage, the old and new testing tools should coexist and work together until the entire migration is completed.

βœ… Never leave the project unprotected

Another consideration was maintaining a coverage threshold as a check in the CI pipeline. Therefore, during migration, we needed to keep a single coverage report to set the CI check, regardless of the migration progress.

βœ… Keep a single coverage report

After considering the above rules, the resulting plan was divided into four main stages:

  1. Set up β†’ Install Jest and assign each tool a portion of the tests: new tests will run with Jest and old tests with Jasmine. Also, add a script to run both testing tools together with a single command.

  2. Merging β†’ Combine testing results from Jest and the old testing library into one coverage report.

  3. Migrating β†’ Gradually move old tests to Jest and remove them from the old test runner.

  4. Cleaning β†’ Once all tests have been migrated, delete the old configurations and uninstall the old testing library and dependencies.

This article will focus on the first two steps, as they allow us to safely begin migrating incrementally without leaving the project unprotected.

Setting up the two testing libraries to work together as one

The idea is straightforward: we want to move from Jasmine to Jest. From now on, new tests should be written to work with Jest, while existing tests will continue to be handled by Jasmine until they have been successfully migrated to Jest.

First, install Jest and create a jest.config.js file at the root directory.

// jest.config.js

module.exports = {
  collectCoverage: true,
  coverageDirectory: '.coverage',
  coverageReporters: ['json', 'text', 'text-summary'],
  collectCoverageFrom: ['<rootDir>/src/**/*.js'],
  testPathIgnorePatterns: ['/node_modules/'],
  roots: ['<rootDir>/src/']
};

To let these testing libraries know which tests they should handle, it's important to define a way to distinguish new tests from old ones and apply this distinction in the testing libraries' config files.

There are several ways to do this, so it's important to choose the right strategy to direct the test execution to be handled by both libraries.

Subfolders strategy + tests folder

One strategy, if your project already has a tests folder outside the source code, is to create a subfolder for each testing library and move the test files there. Initially, all tests will be in the Jasmine folder. As you start writing new tests and converting old ones from Jasmine to Jest, the Jest folder will gradually fill up, and the Jasmine folder will eventually become empty.

project/
β”œβ”€β”€ src/
β”‚   └── auth/
β”‚       β”œβ”€β”€ login.js
β”‚       β”œβ”€β”€ signup.js
β”‚       └── token.js
└── tests/
    β”œβ”€β”€ jasmine/
    β”‚   └── auth/
    β”‚       β”œβ”€β”€ login.spec.js              # Jasmine test
    β”‚       └── token.spec.js              # Jasmine test
    └── jest/
        └── auth/
            β”œβ”€β”€ signup.spec.js             # βœ… Jest test
            └── passwordReset.spec.js      # βœ… Jest test

Following the file structure above, the config files should look like this:

// jest.config.js

module.exports = {
  collectCoverage: true,
  coverageDirectory: 'coverage',
  coverageReporters: ['json', 'text', 'text-summary'],
  collectCoverageFrom: ['<rootDir>/src/**/*.js'],
  testPathIgnorePatterns: ['/node_modules/'],
  testMatch: ['tests/jest/**/*.spec.js'],
  roots: ['<rootDir>/src/']
};
// jasminerc.json
{
  "random": false,
  "spec_dir": "test",
  "spec_files": [
    "tests/jasmine/**/*.spec.js"
  ],
  "helpers": [
    "helpers/**/*.js",
  ]
}

We configure each library to find and run tests only within its respective subfolders.

Suffix strategy

Another strategy, if you keep test and production code in the same folder, is to leave the test files in their original location and add a suffix to the old test files to show which testing library was used to run them. This will help us indicate to the old testing library how to find them.

For new tests, the extra suffix is not needed because we will configure Jest to only look for tests without the .jasmine suffix. These will be identified as new tests to be run by Jest.

project/
└── src/
    └── auth/
        β”œβ”€β”€ login.js
        β”œβ”€β”€ signup.js
        β”œβ”€β”€ token.js
        β”œβ”€β”€ login.jasmine.spec.js           # Jasmine test
        β”œβ”€β”€ token.jasmine.spec.js           # Jasmine test
        β”œβ”€β”€ signup.spec.js                  # βœ… Jest test
        └── passwordReset.spec.js           # βœ… Jest test

To instruct Jest to run all tests except those with the .jasmine suffix, we need to add that expression to the testPathIgnorePatterns property.

// jest.config.js

module.exports = {
  collectCoverage: true,
  coverageDirectory: 'coverage',
  coverageReporters: ['json', 'text', 'text-summary'],
  collectCoverageFrom: ['<rootDir>/src/**/*.js'],
  testPathIgnorePatterns: ['/node_modules/',  '/src/**/.*\\\\.jasmine\\\\.spec\\\\.js'],
  roots: ['<rootDir>/src/']
};

For Jasmine, we need to set it up to run only the tests with the .jasmine suffix and skip the ones without it.

// jasminerc.json
{
  "random": false,
  "spec_dir": "test",
  "spec_files": [
    "src/**/*.jasmine.spec.js",
    "!src/**/*[!jasmine].spec.js"
  ],
  "helpers": [
    "helpers/**/*.js",
  ]
}

Suffix strategy + tests folder

The suffix strategy can also be used if your code organization mirrors the same file structure inside an external tests folder.

project/
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ auth/
β”‚   β”‚   β”œβ”€β”€ login.js
β”‚   β”‚   β”œβ”€β”€ signup.js
β”‚   β”‚   └── token.js
β”‚   └── payments/
β”‚       β”œβ”€β”€ charge.js
β”‚       └── refund.js
└── tests/
    β”œβ”€β”€ auth/
    β”‚   β”œβ”€β”€ login.jasmine.spec.js        # Jasmine test
    β”‚   β”œβ”€β”€ token.jasmine.spec.js        # Jasmine test
    β”‚   └── signup.spec.js               # βœ… Jest test
    └── payments/
        β”œβ”€β”€ refund.jasmine.spec.js       # Jasmine test
        └── charge.spec.js               # βœ… Jest test

In this case, you don't need to create separate subfolders for each tool inside the tests folder, nor do you need to move the test files. The only requirement is to add the .jasmine suffix to each test file meant to run with Jasmine, no matter which folder they are in. All tests without that suffix should be run with Jest.

The resulting configuration in both files is the same as when using the suffix strategy, except that in this case, the tests should be looked at inside the tests folder rather than the src folder.

How to choose the correct strategy to separate the test execution

It all depends on how suitable the strategy is for your tests files organization

  • Use folders when:

    • You want a clean, enforced separation between legacy and new tests.

    • Your tests are already in a root-level tests folder.

    • Your team prefers a directory-based organization, and you can afford some initial overhead.

    • You don’t mind moving files from one folder to another.

  • Use suffix when:

    • You want minimal disruption to the existing organization.

    • Your tests are tightly coupled with the source code in the same folders.

    • You need an easier and faster path with fewer moves, relying on naming conventions instead of reorganization.

Write the npm test script to execute both test runners sequentially

Once Jest is set up to run the new test files and Jasmine is set to run the old test files, you just need to trigger both test runners with a single npm test command.

I also suggest adding two extra scripts to run each test runner separately. This will be helpful when you start migrating tests.

Your package.json scripts should look like this:

"scripts" {
    "jest": "npx jest",
    "jasmine": "npx jasmine",
    "test": "npm run jest && npm run jasmine"
}

Now, when we run the tests, both testing libraries will execute one after the other. To prevent failures, it's important to configure them to pass even without tests. At the start, there will be no Jest tests, and after the migration, there won't be any tests written for Jasmine. Therefore, it's crucial to run the tests with a flag like --passWithNoTests.

Merging coverage results into a single report

Since we use the coverage report as a pipeline check, we need to keep using it even after making the two testing libraries work together. We must merge their coverage reports into a final report that includes all tests, regardless of whether they were written for the old or new testing tool.

In our project, we decided to use nyc to combine the coverage reports from Jest and Jasmine. We were already using it to collect coverage for Jasmine tests, and it offers two useful commands for merging reports: merge and report.

Since we noticed some inconsistencies with the merged report using the merge command, we opted to use the report command instead. We decided not to stress over the minor differences when comparing the coverage results to the original results from using just one testing tool.

To correctly use the report command of nyc, ensure reports don't overwrite each other by storing them in different folders when possible and renaming files if they need to be in the same folder.

To merge the coverage reports, the plan goes like this:

  1. Run Jest and collect the coverage of Jest test files into a temporary .coverage-jest folder.

  2. Run Jasmine and collect the coverage of Jasmine test files into a temporary .coverage-jasmine folder.

  3. Copy both coverage reports into a single temporary folder .coverage-merge, and rename the files by adding a suffix to identify the testing tool that generated them, preventing overwriting.

  4. Run the nyc report command, specifying the final thresholds and the temporary folder where the reports are located. Also, indicate the output folder where the merged coverage will be saved.

  5. Clean up all temporary files and folders.

πŸ’‘ It's important to set the coverage threshold as flags when calling nyc report and remove any threshold from the Jasmine and Jest configuration files. This is necessary because we need to validate the threshold across all files, not just a subset.

If you follow the steps described here, you'll end up with a package.json similar to the one belowβ€”I hope one much better β€”. We use the shx package to run commands across different operating systems to delete, copy, and move files and folders.

"scripts": {
    "copy-coverage-jasmine": "npx shx mv .coverage-jasmine/reports/coverage-final.json .coverage-merge/coverage-jasmine.json",
    "copy-coverage-jest": "npx shx mv .coverage-jest/coverage-final.json .coverage-merge/coverage-jest.json",
    "copy-coverage-files": "npx shx mkdir .coverage-merge && npm run copy-coverage-jasmine && npm run copy-coverage-jest",
    "clean-tmp-coverage": "npx shx rm -rf .coverage-jasmine && npx shx rm -rf .coverage-jest && npx shx rm -rf .coverage-merge",
    "multi-coverage": "npm run nyc-jasmine && npm run jest && npm run copy-coverage-files && nyc report --check-coverage --lines=80--branches=70 --functions=70 --statements=80 --reporter=lcov --reporter=text --reporter=text-summary --temp-directory=.coverage-merge --report-dir=./.coverage && npm run clean-tmp-coverage"
 }

With the scripts above, instead of running npm run test:coverage, you would use npm run multi-coverage. It's up to you if you want to make it explicit that the coverage is from multiple tools. And that's how you make two testing libraries work together, keep the coverage functional, and set the stage to gradually start migrating tests from one tool to another.

✍🏻A couple of notes to keep in mind

  • The final coverage script runs sequentially. It depends on which testing tool you run first in your npm test script. So, if the first tool fails, the next one won't run.

  • Merging reports isn't perfect. There are open issues about it, so pick the command that gets the resulting coverage closest to your current value and adjust the thresholds accordingly.


Up next:

  • We’ll learn to use tools to make the migration less tedious

  • Advice on what to pay attention to and how to migrate the tests without leaving the project unprotected

  • How to get rid of all of this config to leave a single testing tool after the migration

Incremental Testing Library Migration Guide