This is adapted from a talk I gave at DenverScript last year. It covers basic testing principles, introduces Angular and a little bit about how to put the two together.
We’re going to be starting from the conceptual basics of testing. I hope that starting at the beginning will be helpful to newer developers and put this all into context.
At the end of this post, you will be able to…
- Talk about foundational testing concepts using the right terminology
- Talk about an Angular application at a high level
- Discuss the differences between isolated unit tests and integrated unit tests, and when to use each in the context of Angular
- Know the difference between shallow and deep integrated tests
- Go back to your own Angular code and get started with writing unit tests
What is a unit test?
An isolated test of the smallest unit of code. It should be fast, cheap to write, cover a single state change, assert one thing, avoid crossing process boundaries, and be reliable. When writing a unit test, any kind of dependency that is slow, hard to initialize or manipulate, should be mocked so you can focus on what the unit of code is doing, not what its dependencies do.
The basic toolset is built right into Angular with the CLI, but it’s good to know what each part does.
- Angular – A robust front-end framework with many built-in utilities for testing
- Jasmine – A behavior-driven testing framework
- Karma – A lightweight server that loads and runs tests in browsers
Anatomy of a Test
- A suite is a collection of closely related tests.
- A spec is an individual test.
- An assertion is the part where you actually run your code against the test.
- A matcher is part of the assertion. It’s a function that you chain onto the end of an `expect()` and it’s a way of comparing the actual results of your code to the expected result. Jasmine has a bunch of built-in matchers like `.toBe()` and `toContain()` and their arguments are the expected result. You can also write custom matchers.
When learning something new, it’s always good to start with a technique or a pattern. Triple A is a standard one. It means Arrange, Act, and Assert.
When you write a spec, first arrange all necessary preconditions and inputs. Then act on the object or method under test. Finally, assert that the expected results have occurred.
In the example below, I am creating a
speaker object to be the argument for the
addSpeaker method in the
speakerService. That is the arrangement. When I actually call `addSpeaker`, I am acting. When this is complete, and I check that that first speaker in the speaker array has the first name of Elana, that is where the assertion is happening.
Mocking and Spies
Mocking is the mechanism of replacing a dependency with a fake piece of code that does less than the original.
Why would you want to do that?
For speed, for ease of testing, or to just avoid code that you don’t want to test. The idea of writing unit tests is to be targeted, to test one thing, and mocking allows us to focus on what is important and fake what isn’t.
There is also the concept of a spy. This is a test double function. A spy can stub any function and tracks calls to it and all arguments. A spy only exists in the describe or it block in which it is defined, and will be removed after each spec.
A mock is a dummy class replacing a real one. A spy is kind of a hybrid between real object and stub. It is basically the real object with some methods shadowed by stub methods.
Tour of an Angular Application
Let’s get a quick overview of an Angular app.
Angular has its own module system that makes writing modular, reusable code very easy.
At minimum, you will create one module that contains your app.
Components and Services
Each module is basically made up of encapsulated components and the services that span across them. These are the things that we are going to be focusing on with our unit tests today.
A Component is made up of a Template, a Class and Metadata
A component has three basic parts: a template, a class (that contains methods and properties), and metadata.
A service is also a class, but of course, without the template… because that would just make it a component.
Let’s take a look at some code.
This is really the main file in our module and this is what a simple app might look like. At the top, we’re importing some core Angular modules that we need to get us started. We’re also importing our main component and a service. If we had more components and services, we would have to import and register them here as well.
Now down here, you can see the
@ngModule() decorator. That’s where we’re actually bootstrapping the main component of our app. After we tell Angular to bootstrap our base component, we register all of our imported modules in our imports array – and if we had additional components, or pipes, or resolvers, they would go into imports as well. And then we do the same with services in the providers array.
Again, this is where the Angular module gets initialized. This is important to know for writing an Angular application, but also keep this in mind for when we get to integrated unit tests.
Let’s take a quick look at the code for a Component….
Note that I’m using TypeScript. That’s not mandatory with Angular, but it’s pretty common, and in fact, pretty pleasant to use. For now, even if you’re not familiar with TypeScript, it should still be pretty readable.
So, starting from the top, we import the component decorator from ‘@angular/core’. That’s necessary for configuring up the metadata that we need for a component.
Then, we use our decorator to define the selector that we’ll pop into our index file, or if this wasn’t the base component, we might nest it inside of another template. We are also referencing an HTML template here.
Down at the bottom, we are exporting a class with properties and methods. Altogether, that makes an Angular component.
To complete our incomplete tour of Angular, I want to show you a service, just so that you can see that it is also a class.
Isolated vs Integrated Tests
An isolated unit test means…
- We are testing the class only. We are testing methods in the class. If we’re testing a service, it’s just a class. If we’re testing a component, we know that it has template, but we don’t care.
- In an isolated unit test, we construct the class in the test. That will be different when we look at the integrated test.
- An isolated unit test is the simplest. You can write lots of these.
- They are best for Services and Pipes – which we haven’t talked about, but are essentially Angular’s answer for filters.
- They can be appropriate for components and directives as well, but again, when we write them for things that have templates, we just ignore the templates.
In contrast, integrated unit tests…
- Allow us to test classes with their templates.
- They’re a bit more complex to set up and we have to use some tools that the framework provides to construct the classes.
- They are mainly used for components and directives.
- And there are two types: deep and shallow.
Let’s start with an isolated test. Let’s test a service.
* I see the typo –
voterService should be
speakerService, but I’ve already embarrassed myself in front of a live audience. I’m not changing it right now. For the internet…
- Integrated tests include the template.
- Touch more features and therefore less reliable.
- The value of integration tests comes into play when you have more complex templates.
- In integrated tests, you actually have to import dependencies. You won’t instantiate them, but you’ll need to have them to declare types.
There are two types of Integrated Tests
- Deep Tests
- Shallow Tests
Shallow vs Deep Integration Tests
In a shallow test, you are testing just immediate dependencies, while deep tests test dependencies of dependencies.
Before we go further into these two types, there are a few more things we need to discuss to set up integrated unit tests.
Setting up an Integrated Test
TestBed is an Angular testing utility that creates an Angular testing module—an
@NgModule class—that you configure with the
configureTestingModule() method to produce the module environment for the class you want to test.
In effect, you detach the component to be tested from its own application module and re-attach it to a dynamically-constructed Angular test module tailored specifically for this battery of tests.
TestBed is a utility that configures and initializes an environment for unit testing and provides methods for creating components and services in unit tests. It is a very close approximation of running live in the browser. It integrates with components and constructs the module.
A fixture is a wrapper around the component – not the component itself. In an isolated unit test, you just create the component. In an integrated test, you need to create the wrapper/fixture in order to have access to more info about the component (change detection, dependency injector, etc.)
Five Steps to Writing a Deep Test
- Import your dependencies
2. Create a Fixture and an Element
3. Mock Services
4. Configure the TestBed
In a shallow integrated test, you test a component (with its template) in isolation. It is like a deep test, but without all the dependencies. Sometimes it makes sense to leave pipes in – they’re pretty small and generally don’t add complexity
There are two strategies for writing shallow tests
- Replace the child components with fake components
- Tell Angular to ignore child components and treat them like dead HTML elements
The first option asks you to use mocks and stubs.
In the second option, you would import the NO_ERRORS_SCHEMA from
@angular/core and register in the schemas array. A problem with this option is the potential for actual missing errors.
A Final Word about Integration Tests
You may have seen this testing pyramid before.
The idea is that you should write more isolated unit tests than any other kind of test. They’re the easiest to write and the least brittle. In the opposite end of the spectrum are end-to-end (e2e) tests. They’re time consuming and generally more brittle. Integrated tests are somewhere in between, so use your judgment for how much and how deep test coverage should be.
- Use isolated tests primarily for services and pipes
- Use integrated tests for complete component testing
- Deep integrated tests test all connected components
- Shallow integrated tests test just the component in isolation and are better for when related tests add too much complexity
- Write the fewest e2e tests, more integrated tests and the most unit tests