DEV Community

Abdoulaye Traoré
Abdoulaye Traoré

Posted on

Migrating AngularJS tests From Karma to Jest

Disclaimer

This is just a write up of how we migrated our AngularJS tests to Jest, it's not a guide and there certainly are elements that I won't cover but I hope it helps somebody out there.

In this post it is assumed that you have some good knowledge in javascript unit testing configuration using karma, that you have heard or know of Jest and that NPM, Babel, Typescript are familiar to you.

This article explains what Jest and some other tools are pretty well.

Context

At the end of 2017, we started migrating our whole front-end code base to Typescript and Webpack. In my opinion, Typescript is a godsend for large enterprise javascript code bases and we appreciate it. The old setup was getting complicated to maintain as it was custom made and didn't really follow javascript best practices.

Our front-end code is divided into two parts:

  • a library of reusable components that we use internally across all applications
  • the code that uses the lib and contains the application specific elements (views, custom components, services etc....)

The old setup required us to build the lib and then build the app that depended on it and a whole bunch of stuff that would give you nightmares; we even had "black magic" written in some places. If you ever come across such things in a code base, it doesn't smell good. I reckon even you agree we had to change stuff and change we did.

To get back on topic, after having setup our new build with Webpack and converted our js files to typescript; there was one last thing to do: make the tests work again !

Ah, didn't I tell you that ? Well in addition to what was previously mentioned, our tests weren't running anymore as a side effect of all the custom stuff.

The test setup was based on Karma/Mocha/Grunt/PhantomJS and just didn't work anymore. My first reflex was to update the tests to make them work with karma but that proved a more daunting task than I anticipated it to be. The plugin system
of karma can be cool but this time it was being more of a hassle than anything. I tried karma-typescript (really nice lib and awesome maintainer, shout out to @monounity); at first it went well, a majority of the library tests worked and all that but when I tried to run the application tests all hell broke loose. We used namespaces for the lib and karma-typescript didn't really like it so I opened an issue that monouty fixed but I then ran into other problems.

In light of all these issues, I couldn't make it work on time and had to leave it alone for a while, there were other things that needed my attention unfortunately. Fast forward to April 12th 2018, I was attending a meetup with a friend that was about TDD and BDD (an article about the meetup in french but with slides in english link) and they used Jest (woohoo, he's finally talking about it). I had heard of the framework and read this good article on using it for angular apps. This reminded me of my unfinished business with karma. I pitched Jest to my team and given my previous run-ins with karma, we decided to go ahead and migrate (don't know till you try) all our tests to it.

Migration

karma config

Here are the karma config files we used. The first one is for the app and there wasn't any attempt to make it work. The second one is for our internal library and is the one that I tried to make work.

Jest configuration

I started reading the official documentation (who said devs don't read the manual?) and there was a section about testing web frameworks that led to my previously mentioned article and this lifesaving piece by @benbrandt. There aren't many articles about Jest + Angular out there and trust me you need it when doing this kind of migration.

Typescript

We are using Typescript and Jest doesn't natively support it so we need a preprocessor to do the job. Enter TS-Jest, it does it all for you.

So we end up with a transform that looks like this.

    "transform": {
      "^.+\\.ts?$": "ts-jest",
    },
Enter fullscreen mode Exit fullscreen mode

I also had to create a separate tsconfig file for ts-jest because it doesn't support all the options that we use in our typescript config file. I also disabled the TsDiagnostics but you shouldn't

    "globals": {
      "ts-jest": {
        "tsConfigFile": "test-tsconfig.json",
        "enableTsDiagnostics": false
      }
    }
Enter fullscreen mode Exit fullscreen mode

Namespace

I read the articles for a bit, and started creating the configuration file for the library tests. The first problem I ran into was managing our namespace. After reading the docs for a while, I saw the moduleNameMapper option and that was it, problem solved.

    "moduleNameMapper": {
      "customNamespace/(.*)$": "<rootDir>/src/$1",
    },
Enter fullscreen mode Exit fullscreen mode

Loading html files

We use webpack for our build and load html files using webpack's html-loader. I needed the same functionality for the tests. A couple of google searches later I found this stackoverflow issue. After reading all the comments and answers, I decided to follow their advice and create a custom preprocessor for Jest (yes Jest allows you to do that).

All that's left is to include it in the config.

    "transform": {
      "^.+\\.ts?$": "ts-jest",
      "^.+\\.html$": "<rootDir>/src-test/utils/htmlLoader.ts"
    },
Enter fullscreen mode Exit fullscreen mode

Making sure img tag doesn't break my tests

In some of our html templates, we directly import images and that doesn't work with Jest; you need to stub it. Searching on the internet brought me to this package jest-static-stubs that is just perfect for the job. In the moduleNameMapper section of the config we just add the right line:

    "moduleNameMapper": {
      "customNameSpace/(.*)$": "<rootDir>/src/$1",
      "^.+\\.(jpg|jpeg|gif|png|mp4|mkv|avi|webm|swf|wav|mid)$": "jest-static-stubs/$1"
    }
Enter fullscreen mode Exit fullscreen mode

Angular-mocks and global jquery

Due to how certain things work with angular ( this is better explained in mr brandt's article) we have to expose certain values (Jquery, Angular) on the global scope. In addition to that we need to import angular-mocks so that Angular sets up the app prior to running the tests. This is all in the form of an init file that is later referenced in the Jest configuration.

Init file content:

Referencing config in Jest config:

"setupTestFrameworkScriptFile": "<rootDir>/src-test/utils/init.ts",
Enter fullscreen mode Exit fullscreen mode

Library config

We ended up with a configuration for Jest like this in our package.json:

That was it for our library tests and we even had code coverage without adding anything else. The cherry on top for me as a vs-code fanboy is the existence of this extension. The extension is pretty cool and I would recommend checking it out if you use vs-code and Jest.

Moving on to the application tests, I thought it would be a straightforward copy-paste and adapt thing.... Little did I know other issues were awaiting.

ES6 module support

As previously stated, our code is split into two parts: a library that is an npm module and the applications that depend on it. The library is written in typescript and we compile to es6. I needed to configure Jest to correctly load es6 modules and this issue had the answer somewhere in the thread. The solution was to use babel-jest for js files (my node_modules in this case) and to add a .babelrc file to my project containing:

The transform part of Jest config became :

    "transform": {
      "^.+\\.js?$": "babel-jest",
      "^.+\\.ts?$": "ts-jest",
      "^.+\\.html$": "<rootDir>/src-test/utils/htmlLoader.ts"
    },
Enter fullscreen mode Exit fullscreen mode

The final Jest config is not that different from the lib one :

Comparison with Karma

The performance between Jest and Karma cannot be compared as there is no reference from the time the karma tests worked. I can tell you that Jest takes 38.425 seconds to run 92 tests organised in 9 test suites and run coverage. We went from 13 to 4 dependencies (jest, ts-jest, babel-jest, jest-static-stubs) needed to run our tests. PhantomJS is not needed anymore since Jest uses JSdom; that can be seen as an advantage or a disadvantage since we are no longer testing against real browsers. I hope testing against real browsers can be an option for Jest in the futur.

Conclusion

It wasn't easy but in my opinion it was worth it; we now have a more maintainable and modern test configuration. Testing can be fun with the right tools and I hope that we can add to our test base on a more regular basis with this setup.

A big thanks to the open source community without which this wouldn't have been half as easy. Hope this helps you.

A big thanks to Steven, Sam, Jean-Baptiste for advice and editing.

Photo credit goes to @weilstyle.

Discussion (5)

Collapse
algil profile image
Antonio Gil

Hi!

Great article.

Could you add some examples, like how to inject a service?

Thanks!

Collapse
abdoulayektr profile image
Abdoulaye Traoré Author

Hey, thanks. I didn't include the test files because the tests worked out of the box without any change once the test runner was correctly configured plus the focuse for me was more on Jest than the tests themselves. I'll try to paste some examples below from our test base if that can help you.

Collapse
algil profile image
Antonio Gil

Thanks!!

Thread Thread
abdoulayektr profile image
Abdoulaye Traoré Author

yo @Antonio, I hope this helps you, it's a simple example

import chai, { expect } from 'chai';
import sinonChai from 'sinon-chai';
import * as angular from 'angular';
import otListService from 'ot/components/widget/listService';
import otResourceService from 'ot/components/resourceService';
chai.use(sinonChai);

describe('otListService', function () {

  let Test;
  let _otListService;
  let httpBackend;
  let _OT_LIST_ITEMS_PER_PAGE;
  let responseArray = [{
    id: 1,
    name: 'carapuce'
  }, {
    id: 2,
    name: 'salameche'
  }, {
    id: 3,
    name: 'bulbizare'
  }];

  beforeEach(function () {
    angular.mock.module(otListService, otResourceService);
    inject(function (otResource, otListService, $httpBackend, OT_LIST_ITEMS_PER_PAGE) {
      _otListService = otListService;
      httpBackend = $httpBackend;
      Test = otResource('test');
      _OT_LIST_ITEMS_PER_PAGE = OT_LIST_ITEMS_PER_PAGE;
    });

  });

  afterEach(function () {
    httpBackend.verifyNoOutstandingExpectation();
    httpBackend.verifyNoOutstandingRequest();
  });

  describe('query()', function () {

    it('should be able to request the resource without pagination', function (done) {
      httpBackend.expectGET('test?orderBy=name&reverse=false').respond(JSON.stringify({
        models: responseArray
      }), {
        'X-Count': responseArray.length
      });

      _otListService.query(Test, null, null, null, null, null, function (data, totalItemCount) {

        data.should.be.an.instanceof(Array);
        data.should.have.length(responseArray.length);
        expect(totalItemCount).to.be.equal(responseArray.length);
        done();
      });

      httpBackend.flush();
    });
Thread Thread
algil profile image
Antonio Gil

Hi!

Thanks!! very useful!

Regards!