Blog Barista: Jim Rasche | Aug 27, 2018 | Developer Tools | Brew time: 7 min
Have you ever been plagued by StaticInjectorError while working on your Angular front-end unit tests? Problems like these kept cropping up for me, so I spent a day looking into how TestBed takes all that metadata we pass into configureTestingModule to build an application around our testable code. I’ll run through some behind-the-scenes operations performed during test case setup and then summarize some of the takeaways.
Firstly, when analyzing third-party libraries like Angular/core/testing, I always find it helpful to download the entire project. This allows us to use an IDE and makes finding files and navigating to dependencies faster. To look through Angular code, you can download the Angular project from https://github.com/angular/angular.
So what happens when we pass our module metadata into configureTestingModule? In short, this just sets the TestBed’s internal declarations, providers, imports, etc. to whatever you specified in the metadata.
After being configured, most interactions with the TestBed, including requesting compileComponents and getting an injected provider from TestBed, will create both the compiler and DynamicTestModule that encapsulates the component under test.
You can verify this by following the breadcrumbs from
platformCoreDynamic → JitCompilerFactory
JIT is the obvious choice here as we have access to the compiler in the browser and the uncompiled versions of files for debugging.
Let’s tweak the compiler to see what is happening under the hood. An easy way to modify node_modules files is by editing them in the application’s node_modules folder. After refreshing, the changes will be available in the browser. npm install will wipe out these changes, but it’s quicker than the alternative of downloading the module’s source, making your changes, compiling, altering your package.json to use your local module, then reinstalling your project’s node modules. Either of these methods can be used to add logging to third-party libraries for temporary testing, and that’s what we’ll do here to help illustrate how the compiler uses our module metadata to create our test classes.
ng new project will create a new Angular application from the default Angular project template, with a single test case app.component.spec.ts. The appComponent is very basic; its template uses no Angular directives, and the component doesn’t even have a constructor.
First, add logging to compiler.js so we can track what’s happening behind the scenes. Searching the source in the Chrome dev console will show us that the compiler file used is @angular/compiler/fesm5/compiler.js. Add the lines with arrows to this file in your project, under the node_modules folder:
Run ng test and we can see all the imports pulled in:
We can see a lot of modules are being loaded that our test component metadata didn’t specify. These are default modules needed to build a component, compile templates, etc.. The only part we supplied, AppComponent, is nicely packaged in the DynamicTestModule.
Let’s add a little complexity here to demonstrate a potential issue. An import that was giving me grief was angular2-hotkeys. Let’s install it and see why.
npm install angular2-hotkeys –save
Update the app.component.ts to use it:
Run ng test again and we get StaticInjectorError:
This makes sense. The compiler doesn’t know where to find the HotKeyService dependency and therefore can’t inject it. So let’s add the HotKeyModule to our test metadata and see what happens:
We get the same error. The console output from the compiler shows the HotKeyModule is available, so what’s the problem?
Seeing all the declarations, imports, and providers the compiler is building helps us understand that something is still wrong with our metadata setup. The HotKeyService is not among the providers we send to the compiler. A little investigation into how angular2-hotkeys is exporting its HotkeyModule reveals the issue: the providers are separated out from the HotKeyModule export via the static forRoot method.
Since our test case is a standalone application, we need to call HotKeyModule.forRoot() when we import this module, as there is no parent module like AppModule or CoreModule to do this for us.
Now we have the HotKeysService available for Angular to inject into the appComponent, and the test case passes.
- When you’re having problems using a third-party library, delve into the details of how it works by searching code and making changes to the source to log output and test how it handles data.
- forRoot is an Angular convention used to enforce singleton usage within an application. We will need to invoke it in test cases that use providers declared in forRoot.
- Compilers are usually a part of web development that one need not dig into. However, it is important to understand their inner workings for cases like this.
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…