This is the third post in the series about Angular unit testing. If you are unfamiliar with using Angular CLI or making changes to Karma configuration, please read the first two posts in the series.
In the last post, we learned how to edit karma.conf.js to add reporters and set coverage thresholds. In this post, we'll create shared testing files and create our own parameter to pass in while testing to limit the files that the test runs. As projects become larger, running all the tests can take a while, so this is an easy way to target tests. It's time to level up your testing game and get pumped up to unit test!
Angular CLI doesn't expose all the configuration options Karma has, so if targeting tests isn't your thing, maybe there's other ways you tailored Angular CLI tests. I'm showing this as an example of how to work around limitations in Angular CLI, not a suggestion on correct ways to configure Karma. I'd love to hear all about the ways you've tailored Angular CLI unit tests in the comments!
Follow Along
We're using Angular CLI to test the application and using code from a previous post I wrote.
Intercepting Http Requests-- Using And Testing Angular's HttpClient
Alisa ・ Aug 28 '17
You can clone the tutorial-angular-httpclient and run tests too. All the level ups discussed in the posts are found in "test-configuration" branch.
Create Shared Testing Files
As your app grows, you may find that you're creating the same fake more than once. We can DRY things up a bit by creating testing files.
Create your fake and place it in a testing folder. For ease, I named my folder "testing" and made it a child of "src" directory. If you're following the repo, I created a fake-user.service.ts.
In $/src/tsconfig.app.json, add the "testing" folder in the "exclude" array. It needs to match a glob pattern. So in my case I have
"exclude": [
"test.ts",
"**/testing/*",
"**/*.spec.ts"
]
Your spec files can now refer to your shared fake instead of having to define it in the file!
I also like to add a shortcut path to the "testing" folder so that I don't have the world's worst import statement. In your $/tsconfig.json, add a "paths" property after "libs" to add a shortcut to your testing folder like this
"lib": [
"es2018",
"dom"
],
"paths": {
"@testing/*": ["src/testing/*"]
}
In the spec files, I can import my fake users service by using
import { FakeUserService } from '@testing/fake-user.service';
Target Tests Using Jasmine Constructs
Before diving in too deep into custom code to target tests, let's start by using Jasmine's native constructs to force only specific tests to run. You can force tests by prepending 'f' to the test suite or test.
So change describe
to fdescribe
or it
to fit
. Then only the tests with the 'f' prefix will run. Your test output still includes all tests (it skips the tests not prefixed).
You'll want to remember to remove the 'f' prefixes. Luckily you can add a lint rule to remember to remove the forced tests.
In your $/tslint.json, add a new rule called "ban". We'll add "fit" and "fdescribe" like this:
"ban": [
true,
["fit"],
["fdescribe"]
]
But, using the native construct has its disadvantages. You have to edit the code and remember to remove the 'f' prefix. Also, when you have a lot of tests, it can still take time to complete the test run because it still evaluates whether to run each test. Then your output is full of test cases that were skipped. Boooo... Luckily, we can work around this.
Target Tests By Limiting Files
A way to run only the file you want is to edit the regex Angular CLI uses to find all *.spec files. We can hardcode the test files we want to run.
In $/src/test.ts
, find the line
const context = require.context('./', true, /\.spec\.ts$/);
Edit the regex to target the test files you want, such as
const context = require.context('./', true, /\.service\.spec\.ts$/);
to run all the service.spec.ts files.
This method certainly gets the job done. It speeds up the test run and only shows the service files in the console output, but now we have no lint safety to remember to revert changes we made. There's another option-- to create a custom parameter.
Create Custom Parameter
I'd like to pass in the files for the test run. We can add custom code to enable us to do this. Sometimes it's fun to take the covers off a library and dig in to the code ourselves. Taking things apart and putting it back together in way that works better for our needs helps us learn and makes us better engineers and craftsmen.
Warning: Angular v7 purposefully limits parameters passed in to Karma so this solution only works on Angular v6. Follow the instructions with at the risk of it breaking when you upgrade, but maybe it will give you some ideas of the sorts of things you can try!
Handle CommandLine Argument
In the karma.config.js, we need to parse the commandline arguments to grab the test files. I'm going to call this parameter specs
.
At the top of karma.config.js define a variable to hold the list of test files and parse through the command line arguments to grab the list of test files. My code looks like this (yep, it's a little ugly-- it can certainly be cleaned up):
let specs = [];
if (process.argv.indexOf('specs') !== -1) {
specs = process.argv.slice(process.argv.indexOf('specs') + 1);
specs = specs.slice(0, specs.findIndex(el => el.includes('--')) === -1 ? specs.length : specs.findIndex(el => el.includes('--')));
}
Then add specs
in the args
array:
client: {
args: [specs],
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
That's it for karma.conf.js.
Create Regex Of Files To Test
So now we can pass in a list of files to limit tests to. We do this in the test.ts file.
We need to get a hold of the karma
context, so declare it by adding
declare const __karma__: any;
as the first line after the import statements.
Then we need to build out the file list regex. I grabbed the list of files from the karma context and added files to a variable. We still want to run all tests if we don't limit the files.
const args = __karma__.config.args[0];
let fileRegex: RegExp = /.*/;
if ( args && args.length) {
const files = args.join('|');
fileRegex = new RegExp(`(${files})\.spec\.ts$`);
}
We want to use fileRegex
instead of the one Angular defined, so the line where we hard coded service tests previously changes to
const specFiles = context.keys().filter(path => fileRegex.test(path));
Lastly, we want to load the modules properly to take in to account specFiles
specFiles.map(context);
To see all the code in context, here's a gist of what we just did.
Now we can start using this new parameter.
Here's the kicker. Angular CLI won't recognize the specs
parameter, but there's a trick to get Angular CLI to accept it by including the the app name. Create a npm script called test:specs
"test:specs": "ng test tutorial-angular-httpclient specs"
Run the tests by passing the list of files through npm. You can add other commands as you like. For example
npm run test:specs -- user.service auth.interceptor --watch false --browsers ChromeHeadless
Hey hey! Finally only running the test files you want to target!
I'd love to hear how you level up your unit testing!
Top comments (10)
Hi,
Thanks for sharing the testing series. It works on below Angular 7 particularly for "Create Custom Parameter". I went through your process in my Angular 7 app but it didn't work. Would you suggest how to use "custom command line parameter" testing in Angular 7?
Thanks.
Thanks Shuvojit!
You are correct, the custom parameter doesn't work for Angular v7. I started working on this while still on Angular v6 and waited too long before posting. I decided to post the custom parameter section anyway since some people may not be able to update their Angular version.
If you are able to figure out a way to submit custom parameters in Angular v7 or v8, please share. I'd love to hear about it.
Great articles, really loved the series :). I need your suggestion with my Angular unit test problem. I have a 'abc.service' which initializes a 'Secondary App' for a remote signup for account creation. When I run the tests it throws error 'Secondary App already exists'. But when I comment that initialization method, everything works fine. How can I solve this problem? I tried the following:
Please help me out, thanks a lot!
Hi there! I just saw your comment so hopefully this response isn't too late. Are you testing your 'abc.service' and it has a reference to another service? or something else?
Thanks for writing this tutorial. Just got started looking into unit testing and this offered a really concise overview.
Thank you for your kind words!
Thanks @alisa , this series is really helpful. You have covered almost all the important points required to start the testing. Appreciate your time and efforts
Thank you for your kind words!
Really helpful series of articles. Will be adding 'html' to the coverage, and using the @shortcut/imports - some great testing tips here 👍
Thanks! I'm glad to hear it!