Mocking Components in Angular

Mocking Components in Angular

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:

  1. Understand why testing in isolation is extremely important
  2. Understand how Angular resolves dependencies
  3. Understand how to mock components

Testing In Isolation

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.

So, how does Angular handle dependencies?

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.

@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:

  1. The declarations array is used to list out components, directives and pipes in your application. Whenever you generate a new component, directive or pipe using the Angular CLI, it will automatically be placed here.
  2. The imports array is used to list out other modules in your application.
  3. The providers array is used to list out services, but generally you won't edit the providers array in the app.module.ts file.

Try to remember what each array is responsible for - we'll come back to this shortly.

The Problem

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:

NOTE: I have 'ChromeHeadless' enabled in my karma.config.js file.

Screen Shot 2020-10-27 at 2.26.07 PM.png

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.

// 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.

Screen Shot 2020-10-27 at 2.27.35 PM.png

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:

describe('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.

The Wrong 'Solution'

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:

beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [
        AppComponent,
        HeaderComponent
      ],
    }).compileComponents();
  }));

Doing this and running the test makes all the tests pass.

Screen Shot 2020-10-27 at 4.11.32 PM.png

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.

The Real Solution - Mocking

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.

@Component({
  selector: 'app-header',
  template: ''
})
class MockHeaderComponent {}

Notice a few things:

  1. The selector property has a value that matches the selector of the real HeaderComponent. This must match the real HeaderComponent selector and is how the test environment satisfies the dependency.
  2. The template property is required, but can be left as an empty string.

Now that we've defined the mock component, head back to the TestBed.configureTestingModule and include the MockHeaderComponent class in the declarations array.

TestBed.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!

Final Thoughts

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.

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 , subscribe to my newsletter and make sure to follow me on Twitter!

Did you find this article valuable?

Support Braydon's Blog by becoming a sponsor. Any amount is appreciated!