When building an Angular application, we usually stick to the suggested or auto-generated solution of unit testing; the Karma test runner and server, the Jasmine testing framework, and PhantomJS as the environment to run it all in. In this blog post I'll explain how this is rather silly, and will provide an alternative and lightweight approach to writing and running unit tests. It will depend on having a certain way of defining your Angular components, and may not be a full 1:1 drop-in replacement, but I can say with a certainty that it'll make your tests faster, the overhead of running them a lot smaller, and improve the quality of tests by having less to worry about.
Disclaimer, background and project structure
First, some disclaimers:
- The title says Angular, but it's not really Angular specific. I am targeting it because Angular by default comes with the 'heavy' test setup by default.
- It's been written with Angular 1.x in mind; at my assignment where we're building this we'll probably go to Angular 2 at some point. Small steps. I do think this approach can also be applied for Angular 2 and probably other frameworks, but I haven't gone there yet.
Second, some background; you can skip this part if you're familiar with how Angular applications should be structured: At my current assignment, we mostly follow the John Papa AngularJS style guide's approach of developing components, which dictates files and components to be very small and defined, having a single purpose, and have a filename descriptive of the content:
You may not need Karma
Complexity, cost, legal and performance reasons aside, honestly, you neither need nor should run your unit tests in multiple browsers nowadays. Two reasons:
- If you're writing browser-specific JS, you're doing it wrong. Or you're writing a library, in which case, disregard this blog post. Libraries and tools like Babel, TSC and related polyfills should take away most if not all browser-specific issues. Second, it's 2016, usage of older browser has dropped to trivial amounts, and most browsers will support most features that tools like Babel will compile down to without needing polyfills. It shouldn't be a problem for any modern webapp to run under IE 9 and newer.
- Unit tests should test logic, not browser quirks. What a unit test should do is go "if I call this function with these arguments, I expect this to happen". Nothing browser-specific in there. Don't waste time on things that are unlikely to happen. Unless you're writing a library or being clever, in which case, stop being clever.
TL;DR: in my opinion, you don't need the complexity of running in multiple browsers, and therefore, you don't need the overhead of serving files and running your tests in a headless browser.
A different approach to running tests
What I propose is simple: Run your tests in a simple NodeJS environment. Node starts fast, will run your JS and unit tests just as well as PhantomJS, and because if your applilcation is structured properly, you'll be able to test your application logic completely independent from AngularJS's dependency injection system, which is another source of both runtime overhead and mental overhead. If you have to write anything that isn't directly related to the component at hand, it's boilerplate and mental overhead (this includes a testing library's structural code and matchers, btw). As a second advantage, with this approach we don't need all possible dependencies and modules to be loaded, and we can test files in isolation instead of having to build, assemble and load the entire application.
Setting up the test runner
To enable this, we need to do only a few things:
- Install babel-cli - unfortunately, NodeJS still doesn't support import, so just for that we're going to need a transpilation step. There might be a more lightweight version of adding support to import that doesn't transpile the whole application; Node 6 and newer should be able to run most if not all of the ES6 code natively.
- Set up a Jasmine config and test runner. You can replace that with your favorite test framework and utilities if you want.
Testing asynchronous code
Testing DOM-manipulating code
Here's where it becomes tricky and vague because I haven't actually gotten to this part yet, but, my colleague experimented a bit with this and managed to use jsdom to run tests, which also included having Angular run. This partially defeats the purpose, but it will allow you to create and run specs that test DOM manipulation, mostly directives, components and/or views outside of the context of a browser. Of course, it's always possible to have multiple test runners in your application, separate between DOM manipulating tests and logical tests.
This test runner should allow you to rewrite your tests - or mostly your test runner - in such a way that it should run a lot faster with a lot less memory and CPU overhead. In addition, it makes your tests simpler, reduces boilerplate, and reduces mental overhead because you no longer have to deal with loading the correct Angular modules, registering mocks with Angular's DI system, and injecting the service under test into the application. If you're suffering from long test runs, consider using this approach. In our current project, we have (only) about a hundred or so test cases running (vs a thousand for the traditional test runner), but they run in approx 0.06 seconds on my machine, versus several seconds for the other tests. The main overhead in running tests is booting up node-babel, which relies on in-memory buffers and the like before it reaches maximal speed. Shouldn't be a problem for running tests in 'watch' mode. It might be possible to run Babel in daemon mode, to reduce this (short) overhead. Finally, this approach might work for Angular 2; it depends on whether Typescript and Angular 2 allow you to write your logic in one file, and add Angular or framework-specific boilerplate in another file. This is true for Angular 1, but Angular 2 relies on annotations, which might cause problems. Of course, it should be possible to just ignore the annotations while running tests. this post was cross-posted to / from my personal website.