Blog Barista: Jim Rasche | Aug 28, 2018 | Developer Tools | Brew time: 10 min
I recently became obsessed with speeding up my Angular test cases. Of course speed is already part of the definition of a unit test, but the faster they are, the more we can effectively use testing throughout the development process. With sufficiently fast test suites, developers can run tests continuously in the background, giving quicker feedback when a breaking change is introduced. Speedy unit tests also make it easier to integrate testing with CI and not irresponsibly bog down the whole process.
This post will share a couple strategies to speed up front-end Angular unit tests. The first technique prevents consecutive tests from running unnecessary setup code. The second technique aims to reduce the amount of setup code used overall.
1. Overwrite the resetTestingModule method of TestBed
After every test case completes, Angular’s core/testing package destroys both the TestBed’s module, created via TestBed.configureTestingModule(…), and the Zone the test ran in. It’s fine to discard the Zone, as the context the test ran in is no longer useful, but all that work the compiler did to build our component can definitely be reused.
Let’s think about what really changes from test case to test case. The compiler won’t change unless we choose to configure it within the test case, which seems like an edge case… we can re-use it. We’re probably not changing the module metadata we pass to configure the testing module, therefore the creation of our DynamicTestModule and the collection of dependent declarations, imports, providers, etc. will not change… we can re-use it. So, what must be discarded? Based off of a discussion I read on GitHub, the only part of TestBed that cannot be reused between test cases is the component fixture. This makes sense as we will likely modify the component during testing to verify its behavior and those modifications shouldn’t affect the next test.
Setting these two Jasmine methods will override the original resetTestingModule method and only reset the properties necessary to run the next test case cleanly. Overriding the TestBed’s resetTestingModule can accomplish everything we just discussed: preserve the compiler and DynamicTestModule, and destroy the fixtures after each test runs.
The difference I’ve found by preventing full module reset is substantial: the 28 test cases ran ~4 times faster. However, note that the speed advantage gained from this technique scales with the number of test cases in a feature test. No gain will be recognized for a feature with only 1 test case, as the compiler and module must be created once regardless.
2. Removing unnecessary shared modules
While going over some old test cases, I noticed that some imported a shared module called SharedModule. I had to wonder what effect all these unused files brought in by importing SharedModule were having on the speed of the test case. The short answer is, a significant effect. From this single experiment, the speed increased by ~35%.
This component had many more dependencies than the one from technique 1, so the process of isolating exactly which components, pipes, and services were needed took a while, maybe 30 iterations of running a single test case and getting a build error like:
A sample of isolating as small a portion of code to use in a test case as possible:
1.) Run a test
2.) Get the following error:
3.) Identify what file the [popover] directive is in (in VS code, you can do this through left click -> Go to definition)
4.) Add this file as a declaration in:
I can see why SharedModule was used originally. This is the easy solution to know you have everything you need, no tracing down dependent grandchild components or services injected to your components service’s service. You could follow this technique to its logical conclusion where no modules are imported in any component, and you simply pull in all the components, pipes, and directives needed by the component under test and every component within the test components tree.
There are two reasons not to push it to this extreme:
1.) When providers are intended to be used as singletons, they should be exported in a modules forRoot() method. In this case, they cannot be declared and must be imported, which imports all unnecessary code in that module.
2.) Specifying every import makes a test case fragile to changes in code outside the class under test. The test case declarations will need to be updated anytime a component or service adds a dependency not already declared in the testing module.
In the struggle between the speed of specificity and the durability of generality, I lean towards specificity. Having to know precisely what your component needs makes us think more closely about our component interactions. This leads to a better understanding of our applications as a whole.
I used the process discussed in one of my previous blog posts to determine that removing the SharedModule alone took 91 out of 187 (~48%) components, directives, pipes, services, and templates out of the compilation process.
In summary, a focus on speed is important for testing. There are some tradeoffs that need to be weighed when we make changes to improve speed, but with a little cleverness we can improve performance measurably.
Other recent posts:
Blog Barista: Anthony Wolf | May 20, 2020 | Development Practices | Brew time: 6 min
Thinking About Your Data Model
I often read code in forums or Stack Overflow from people who are beginners at C#, and see them using FirstOrDefault in every situation where they need a single item from an IEnumerable. If I ask them why they made this choice, the reply is typically something like “it always works” or…
Blog Barista: Jonathan Nicholson | May 6, 2020 | Privacy & Security | Brew time: 7 min
There are many things that you can do to slightly increase your privacy in this digital age. A lot can be accomplished without being too extreme like swearing off all social media, self-encrypting all of your emails, and using Tor—a software tool for…