Don’t write ES6 Angular 1.x unit tests this way!
Working as a consultant, I’ve come across this pattern more-and-more frequently in source-code I’ve had to either maintain or work with. Here’s why I strongly believe that this coding/testing approach is an Angular unit-testing anti-pattern.
It makes re-using individual components impossible
Let’s say we have the following code:
As a consumer of your library, how can I reuse
FormComponenton it’s own?
Answer: I can’t. I have to write
angular.module('myProject', ['main.app.feature-b']), which means I’m getting all the other artefacts that are attached to this module, including any
run() blocks which might be completely unrelated to the behaviour of the component I’m trying to reuse. No amount of tree-shaking is going to help me at this point because all the dependencies are explicit.
Bad Solution: Just import the ES6 file and do your own wiring in your own project. That might work for simple components. For components with lots of dependencies, you’re going to need to put your detective hat on and discover the names of the modules that contain the dependent-components and make sure you include them in your module definition. This is time consuming and a pain in the a$$. Don’t do this.
Solution: Put all of your components/filters/services/providers/directives/… into their own module, and they can be both re-used AND tested in isolation. One component per Angular module per ES6 file (1–1–1).
Who imports their root-level Angular module when doing unit-testing?
If you import the root module to do a unit test, of course your unit tests will run slower. If you only have a single module in your application, that’s bad design (for any application other than a classroom demo). So yes, don’t import your whole application when trying to run a unit test.
If you used the above solution (1–1–1), you’ll end up with lots and lots of modules that only have one thing in them. Which makes testing them lightweight and fast.
You’re not testing what you think you’re testing
Let’s say you write unit tests for Angular components without using
angular.mock.module. You know that the code in the following file is working:
But can you prove that the
TodoService is working?
“Ah-ha!”, I hear you say. “I’ve already tested it!”. No you haven’t!
Unless you have a unit test for the above file, you have no evidence that the ‘TodoService’ that your application uses is the
TodoService that you’ve tested. In otherwords, the wiring of your application must be tested too (because it could be mis-wired).
Solution: 1–1–1… one component per Angular module per ES6 file will solve this problem. Why? With a 1–1–1 approach, the Angular module will contain the wiring that your application is using:
And if your test looks like this:
… then you can be certain that the ‘component’ you are testing is
myComponent, and that that is the same component that your application is actually using.
The fallacy of reuse
Developers who have argued for this anti-pattern often argue that because the code has no Angular references, it is easier to reuse. Tell me, how are you going to re-use this component when you switch to React?:
This component, while not referring to Angular directly, relies on Angular’s APIs and coding patterns. You’re not going to be able to re-use your ES6 Angular components in React. I’m sorry to break the news.
In fact, writing components that can be used in any framework is not easy. Web components is probably your best option if your goal is framework-agnostic component re-use.
Not everything is Angular code
I’m not arguing that every piece of code you write inside of an Angular project needs to be wrapped in an Angular module. There may be code — such as a date library or sorting functions — that can be used independently of Angular or any other framework. In those cases, write them as plain ES6 modules and test them without using
angular.mock.module, because that’s the right thing to do.
Testing code as described in the original article is an anti-pattern because:
- It reduces the ability to reuse individual components (because dependencies are not explicit and the wiring between the code and the name-of-the-code is externalised).
- The code that your application is actually using may not have been tested (because the wiring code is usually not tested when using this testing approach).
Am I taking crazy pills? Let me know :)