Mocking Components in Angular
October 27, 2020
|
––– views(Updated on February 20, 2022)
October 27, 2020
|
––– views(Updated on February 20, 2022)
Are you a front-end developer using Angular? If yes, you need to be writing unit tests to give you the confidence you need to know that your code is working as intended.
This series of articles will cover concepts of unit testing, and show you how to test common operations in an Angular project.
In this first article, I hope to accomplish three things:
One of the things I've noticed throughout the years is that many developers fail to understand a key concept of unit testing: testing in isolation.
Testing in isolation sounds complex, but it's actually a simple concept.
Testing in isolation means that the unit that is being tested should be separate from other parts of the application.
What does this mean when we talk about unit testing in Angular?
Whatever you're testing (whether that be a component, service, pipe, etc) should have all other dependencies (units) separated/mocked.
If you don't test in isolation, you'll end up with hours of headache as you sift through ambiguous console errors trying to figure out why your tests are failing.
Still a bit confusing? Keep reading; I'll clear up a few things in a minute.
Before we dive into mocking components, we need to understand how Angular resolves dependencies. Angular resolves dependencies through modules.
This is one of the best descriptive definitions I've found.
In Angular, a module is a mechanism to group components, directives, pipes and services that are related, in such a way that can be combined with other modules to create an application. An Angular application can be thought of as a puzzle where each piece (or each module) is needed to be able to see the full picture.
Open the app.module.ts file.
TYPESCRIPT@NgModule({declarations: [AppComponent],imports: [BrowserModule],providers: [],bootstrap: [AppComponent],})export class AppModule {}
There are several properties in the NgModule, but let's cover some of the more important properties:
Try to remember what each array is responsible for - we'll come back to this shortly.
When we generate a new project with the Angular CLI, the AppComponent is generated for us by default.
A new project also has some default tests as well. Running the tests produces the following results:
It's nice to have a starting point, but you'll quickly find yourself generating new components and services.
Let's generate a new component with the Angular CLI called HeaderComponent which displays a navbar for our web application. Generating the component isn't enough for it to show up in your web application; we need to consume it so that it is rendered on the screen. In order to do that, let's say that we consume the HeaderComponent in the AppComponent.
HTML// app.component.html<div><app-header></app-header></div>...
Now, in order for the AppComponent to work correctly, it needs to render the HeaderComponent. Thus, we can say that the AppComponent is dependent upon the HeaderComponent.
From a testing-perspective, we now have a problem.
If we run the tests in our project using npm test, we'll see that we have some failing tests.
Why?
Looking at the output from the terminal gives us a clue.
The test file associated with the AppComponent assumes that we are testing in isolation; it only contains what it needs to run the tests for the component. Because we've introduced a new dependency (the HeaderComponent) in the template file of the component under test, the test environment is now complaining because it doesn't know anything about the HeaderComponent. Open the app.component.spec.ts file. Notice the following code and the lack of a definition for the HeaderComponent:
TYPESCRIPTdescribe('AppComponent', () => {beforeEach(async(() => {TestBed.configureTestingModule({declarations: [AppComponent],}).compileComponents();}));....}
When we generated the HeaderComponent with the Angular CLI, it automatically imported the component in the app.module.ts file under the 'declarations' array, but does not include the component in test files, as shown above.
Because the app.component.spec.ts file doesn't have the HeaderComponent listed in the declarations array, it doesn't know how to satisfy that dependency.
Now that you understand why the test is failing, your first instinct may be to import the HeaderComponent and include it in the declarations array like this:
TYPESCRIPTbeforeEach(async(() => {TestBed.configureTestingModule({declarations: [AppComponent, HeaderComponent],}).compileComponents();}));
Doing this and running the test makes all the tests pass.
All good, right? Well, not really.
By bringing in the HeaderComponent, the test environment is now using the real HeaderComponent. This breaks the law of testing in isolation. If the HeaderComponent had other components inside of it, or had a service injected in it, all of those dependencies are now showing up and being used in our test file for the AppComponent. No bueno.
How do we solve this issue?
Let's take a look.
Instead of using the real HeaderComponent, we can create a fake class, also called a mock, that looks like the HeaderComponent and then provide it to our testing environment. This makes the test environment happy, and allows us to define the shape of the component without all of the other dependencies and/or encapsulated logic. This makes testing super simple.
So, how do we mock out a component? It's very simple.
At the top of our test file, use a @Component decorator and define a new mock component class.
TYPESCRIPT@Component({selector: "app-header",template: "",})class MockHeaderComponent {}
Notice a few things:
Now that we've defined the mock component, head back to the TestBed.configureTestingModule and include the MockHeaderComponent class in the declarations array.
TYPESCRIPTTestBed.configureTestingModule({declarations: [AppComponent, MockHeaderComponent],}).compileComponents();
Now if we run our tests, everything still passes. The difference is that the AppComponent is now using the mock class instead of the real HeaderComponent in the tests.
Great job!
If you’ve ever taken Calculus, you know that you learn derivative the long way, and once you understand the principle, you learn the short-hand solution.
Similarly, now that you understand how to manually mock a component in Angular, you can graduate to a much easier solution.
ng-mocks, a small testing library, takes care of the tedious work when mocking components in Angular (it can do more than mock components, by the way).
To mock a component with ng-mocks, use the MockComponent function from the library.
TYPESCRIPTTestBed.configureTestingModule({declarations: [AppComponent, MockComponent(HeaderComponent)],}).compileComponents();
ng-mocks creates a stub component instance that has:
Now that you know how to mock components in Angular, you've tackled one of the most common headaches that developers face when unit testing.
In a real enterprise application, I'd move all of my component mocks into their own directory in the project so that each test file can import the mock component it needs. Obviously, you can disregard the last sentence if you’re using ng-mocks.
I hope this article has been helpful and has helped demystify what it means to test in isolation, and how to actually mock out components in Angular.
In the next part of this series, I'll show you how to mock out services - make sure to subscribe to my newsletter so you don't miss it!
Thanks for reading! If you liked this article and want more content like this, read some of my other articles and make sure to follow me on Twitter!
Added a small introduction to ng-mocks. The purpose of this article is to help you understand why mocking is important and how to mock components yourself. Libraries like ng-mocks will handle all the hard stuff for you, but this article helps you understand what’s happening behind the scenes.
👍
❤️
👏
🎉
Share this article
A periodic update about my life, recent blog posts, how-tos, and discoveries.
No spam - unsubscribe at any time!