Understanding Isolated Testing in Angular

When you're building applications with Angular, writing unit tests is a fundamental practice that gives you confidence in your code. But many developers overlook a crucial principle: tests should verify units in complete isolation.

This series dives into testing fundamentals, demonstrating practical approaches for common Angular testing scenarios.

The focus of this first installment is threefold:

  1. Recognizing why isolated testing matters
  2. Learning Angular's dependency resolution mechanics
  3. Implementing component mocks

The Importance of Test Isolation

Over the years, one of the most common gaps I've observed in developer testing practices is neglecting isolation. Despite sounding complicated, the concept is quite straightforward.

Isolated testing simply means the unit under examination operates independently from the rest of the application.

How does this translate to Angular testing?

Whatever you're examining, whether it's a component, a service, or a pipe, all its external dependencies need to be separated and mocked out.

Without proper isolation, you'll face frustrating debugging sessions, wading through confusing console output trying to pinpoint test failures.

Need more clarity? Let me demonstrate with a practical example.

Angular's Dependency Resolution

To effectively mock components, you first need to grasp how Angular resolves dependencies — this happens through modules.

Here's a concise definition that captures it well:

An Angular module groups related components, directives, pipes, and services, allowing them to combine with other modules to assemble an application. Think of it like a puzzle where each module is a unique piece needed for the full picture.

Let's inspect the generated app.module.ts file.

@NgModule({
  declarations: [
    AppComponent,
  ],
  imports: [
    BrowserModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

The NgModule decorator includes several properties, but let's review the key ones:

  1. The declarations array registers your components, directives and pipes. The Angular CLI automatically populates this when generating these items.
  2. The imports array includes external modules your application needs.
  3. The providers array handles services. However, you rarely modify this in the root app.module.ts.

Keep these arrays in mind — they're central to what follows.

Encountering the Issue

When starting a fresh Angular project via CLI, the AppComponent is scaffolded with default tests that run successfully.

Here are the initial test results:

Note: My setup has 'ChromeHeadless' configured in karma.config.js.

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

Having a baseline test suite is convenient, but your project quickly expands with new pieces. Let's generate a HeaderComponent for a navbar. For it to appear in the app, you'll consume it within the AppComponent template.

// app.component.html

<div>
   <app-header></app-header>
</div>
...

With this change, AppComponent now has a dependency on HeaderComponent to function properly.

From testing, this introduces a complication: running npm test now yields failures.

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

What's the cause?

The terminal output provides a hint. The app.component.spec.ts file was built assuming isolation, containing only the declarations needed for its own tests. With this new template dependency, the test environment lacks awareness of HeaderComponent, triggering an error.

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

....
{

While the CLI added HeaderComponent to app.module.ts in declarations, it didn't update test files, as illustrated above. Without HeaderComponent in the spec file's declarations array, the dependency cannot be resolved.

The Ineffective Fix

Your immediate instinct might be to import and declare the actual HeaderComponent in the test configuration:

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

This approach passes all tests:

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

But is this acceptable?

Not really. By utilizing the real component, the test environment incorporates the actual HeaderComponent, violating isolated testing principles. If that component has its own dependencies—other components or services—they're also active in this test file, which isn't ideal.

Let's look at the proper solution.

The Right Approach: Mocking

Instead of the real HeaderComponent, you can define a lightweight stand-in—a mock—that mimics its public interface. This satisfies the test environment while giving you control over its shape, excluding any unnecessary dependencies or logic. Testing gets much easier.

The process is straightforward: place a @Component decorator on a concise mock class in the test file.

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

Pay attention to these details:

  1. The selector property must exactly match the real HeaderComponent's selector. This is how Angular resolves the dependency correctly.
  2. The template property is mandatory, but its content can be an empty string.

Then, update the TestBed.configureTestingModule to declare your MockHeaderComponent.

TestBed.configureTestingModule({
      declarations: [
        AppComponent,
        MockHeaderComponent
      ],
    }).compileComponents();

Running the tests still shows successful results, but now the AppComponent interacts with the mock, not the real component.

That's the correct way to do it!

Wrapping Up

You've now solved a frequent testing pain point in Angular development.

In larger projects, it's wise to organize component mocks in separate directory structures, making them importable across various test files.

The next article in this series will walk you through mocking services. I'll be sharing it soon—stay tuned!