Running AngularJS Tests with Jest

Running AngularJS Tests with Jest

Earlier this year, we began the process of using React in our AngularJS app. Luckily, rendering React components within Angular isn’t too hard. We use a component “bridge” based on the approach shared by the Small Improvements team, which is working well.

A side-effect of bringing React into the codebase was seeing how fast Jest could run our tests. We had a pretty complicated Angular setup using Karma/Webpack/PhantomJS, which took several minutes to build before any of the tests would even run. Plus, now we had two sets of coverage reports to deal with.

Jest uses a modified version of Jasmine as its test runner. Since we were using Jasmine for our Angular tests, it only required changing some configuration files, not rewriting all our tests.

Karma Config

To see where we started, here is what our Karma config file looked like. Nothing too crazy, but it did need its own Webpack config, which we had to keep in sync with our other Webpack config files.

require("phantomjs-polyfill");
require("karma-es6-shim");

process.env.TZ = "America/Los_Angeles";

var path = require("path");
var webpackConfig = require("./webpack.config.karma");
var entry = path.resolve(webpackConfig.context, webpackConfig.entry.app);
var testManifest = path.resolve(webpackConfig.context, "./test-manifest.js");

var preprocessors = {};
preprocessors[entry] = ["webpack"];
preprocessors[testManifest] = ["webpack"];

module.exports = function(config) {
  config.set({
    autoWatch: true,
    basePath: "../",
    frameworks: ["jasmine", "es6-shim"],
    ngHtml2JsPreprocessor: {
      stripPrefix: "src/",
      moduleName: "templates"
    },
    files: ["gettysburg/node_modules/phantomjs-polyfill/bind-polyfill.js", entry, testManifest],
    webpack: webpackConfig,
    exclude: [],
    port: 8080,
    browsers: ["PhantomJS"],
    plugins: [
      require("karma-webpack"),
      "karma-es6-shim",
      "karma-phantomjs-launcher",
      "karma-jasmine",
      "karma-ng-html2js-preprocessor"
    ],
    preprocessors: preprocessors,
    reporters: ["progress"],
    singleRun: true,
    colors: true,
    logLevel: config.LOG_INFO
  });
};

We also were managing a large test manifest that imported and ran all our test files. With Jest, this all went away!

Jest config

Because of how certain Angular libraries expect to interact with Angular, we use Webpack loaders to expose a few things to the global scope. But now that we aren’t using Webpack for our tests, we have to do this in a way that works for Jest.

Where before we imported the app and test-manifest, now we expose jQuery and Angular:

const jQuery = require("jquery");
Object.defineProperty(window, "jQuery", { value: jQuery });
Object.defineProperty(window, "$", { value: jQuery });

const angular = require("angular");
Object.defineProperty(window, "angular", { value: angular });

require("angular-mocks");
require("./");

We use the full jQuery library instead of Angular’s jqLite, so we need to expose jQuery first so that Angular registers it instead when it loads. Then expose Angular for any libraries that need it. Finally, import angular-mocks and the app entry file (index.js) so that the Angular sets up the app before the tests run.

Then update the jest config in package.json. Here’s our current setup (based on create-react-app with a few tweaks):

{ 
  "jest": {
    "collectCoverageFrom": [
      "**/*.{js,jsx}"
    ],
    "coveragePathIgnorePatterns": [
      "<rootDir>[/\\\\](coverage|dist|docs|flow-typed|node_modules|config)[/\\\\]"
    ],
    "setupFiles": [
      "<rootDir>/config/jest/polyfills.js"
    ],
    "setupTestFrameworkScriptFile": "<rootDir>/index-specrunner.js",
    "testPathIgnorePatterns": [
      "<rootDir>[/\\\\](coverage|dist|docs|node_modules|config)[/\\\\]"
    ],
    "testURL": "http://localhost",
    "transform": {
      "^.+\\.(js|jsx)$": "<rootDir>/node_modules/babel-jest",
      "^.+\\.css$": "<rootDir>/config/jest/cssTransform.js",
      "^(?!.*\\.(js|jsx|css|json)$)": "<rootDir>/config/jest/fileTransform.js"
    },
    "transformIgnorePatterns": [
      "[/\\\\]node_modules[/\\\\].+\\.(js|jsx)$"
    ]
  }
}

The key addition is this: "setupTestFrameworkScriptFile": "<rootDir>/index-specrunner.js" This runs the Angular/jQuery exposes after the test environment loads. For some reason it doesn’t work if you add it to setupFiles. My guess is it has to do with needing Jasmine and jsdom available for the exposing/mocks.

And that was it! We had to make some changes to our tests because of how we were importing them to the manifest, but a majority of the tests ran as is. The handful that did fail were due to spies or other Jasmine-specific things that are different with Jest, but again, slight tweaks.

Conclusion

It took a few tries to come up with the current configuration and get everything working, but it was worth the effort.

Our 800+ unit tests for both React and Angular run in under a minute on our CI, which is way better than the 2+ minutes required for the Webpack build alone.

Hope this helps you get started migrating your own AngularJS tests to a faster, more maintainable testing environment!

A big thanks to Matthieu Lux over at Zenica for showing this was possible. Our setup ended up being a little different, but it was definitely a good start.

(Originally posted on the Talentpair Engineering Blog)