Testing Angular Applications A Developer’s Guide

The Angular framework is a mature and comprehensive solution for building enterprise-ready applications based on web technologies. At Angular’s core lies the ability to test all application parts in an automated way. How do we take advantage of Angular’s testability?

Introduction

Most web developers come across automated tests in their career. They fancy the idea of writing code to scrutinize a web application and put it to an acid test. As web developers, as business owners, we want to know whether the site works for the user, our customers.

Does the site allow the user to complete their tasks? Is the site still functional after new features have been introduced or internals have been refactored? How does the site react to user errors or system failure? Testing gives you answers to these questions.

I believe the benefits of automated testing are easy to grasp. Developers want to sleep well and be confident that their application works correctly. Moreover, testing helps developers to write better software. Software that is more robust, better to understand and easier to maintain.

In stark contrast, I have met only few web developers with a steady testing practice. Only few find it easy, let alone enjoy writing tests. This task is seen as a chore or nuisance.

Often individual developers are blamed for the lack of tests. The claim that developers are just too ignorant or lazy to write tests is simplistic and downright toxic. If testing has an indisputable value, we need to examine why developers avoid it while being convinced of the benefits. Testing should be easy, straight-forward and commonplace.

If you are struggling with writing tests, it is not your fault or deficit. We are all struggling because testing software automatically is inherently complicated and difficult.

Why is testing so difficult? First, writing automated tests requires a different mindset than writing the implementation code. Implementing a feature means building a structure – testing means trying to knock it over. You try to find weaknesses and loopholes in your own work. You think through all possible cases and pester your code with “What if?” questions. What seems frustrating at first sight is an invaluable strategy to improve your code.

Second, testing has a steep learning curve. If testing can be seen as a tool, it is not a like a screwdriver or power drill. Rather, it compares to a tractor or excavator. It takes training to operate these machines. And it takes experience to apply them accurately and safely.

This is meant to encourage you. Getting started with testing is hard, but it gets easier and easier with more practice. The goal of this guide is to empower you to write tests on a daily basis that cover the important features of your Angular application.

Terminology

Before we dive in, a quick note regarding the technical terms. Some words have a special meaning in the context of Angular. In the broader JavaScript context, they have plenty other meanings. This guide tries to distinguish between these meanings by using a different case.

When referring to core Angular concepts, this guide uses upper case: Module, Component, Service, Input, Output, Directive, Pipe etc.

When using these terms in the common sense, this guide uses lower case: module, component, service, input, output etc.

Testing principles

There is a gap between practical introductions – how to test a feature at all – and essential discussions on the core concepts – what does testing achieve, which type of tests are beneficial etc. Before we dive into the practical tutorial, we need to reflect on a few basics about testing.

Why we test – what makes a good test

When you are writing tests, you need to keep in mind what the goals of testing are. You need to judge whether a test is valuable with regard to these goals.

Automated testing has several technical, economical and organizational benefits. Let us look pick a few that are useful to judge a test:

  • Testing saves time and money. Testing tries to nip software problems in a bud. Tests prevent bugs before they cause real damage, when they are still manageable and under control.

    Of course, quality assurance takes time and costs money itself. But it takes less time and is cheaper then letting the bugs slip through into the software release. When a faulty application ships to the customer, when users run into a bug, when data is lost or corrupted, your whole business might be at stake. After an incident, it is expensive to analyse and fix the bug in order to regain the user’s trust.

    A valuable test is cost-effective. The test prevents bugs that could ultimately render the application unusable. The test is cheap to write compared to the potential damage is prevents.

  • Testing formalizes and documents the requirements. A test suite is a formal, human- and machine-readable description of how the code should behave. It helps fellow developers to understand the requirements the original developers had to implement and the challenges they had to deal with.

    A valuable test clearly describes how the implementation code should behave. The test uses a proper language to talk to developers and convey the requirements. The test lists known cases the implementation has to deal with.

  • Testing ensures that the code implements the requirements and does not exhibit bugs. Testing taps every part of the code in order to find flaws.

    A valuable test covers the important scenarios – both correct and incorrect input, expected cases as well as exceptional cases.

  • Testing makes change safe by preventing regressions. Tests not only verify that the current implementation meets the requirements. They also verify that the code still works as expected after changes. With proper automated tests in place, accidentally breakage is less likely. Implementing new features and code refactoring is safer.

    A valuable test fails when essential code is changed or deleted. Design the test to fail if dependent behavior is changed. It should still pass if unrelated code is changed.

What testing can achieve

Automated testing is a tool with a specific purpose and scope of application. A basic concept is that testing helps you to ship an application that functions according to its requirements. That is true, but there are certain subtleties.

The International Software Testing Qualifications Board (ISTQB) came up with Seven Testing Principles that shed light on what testing can achieve and what not. Without discussing every principle, let us consider the main ideas.

https://www.istqb.org/downloads/category/2-foundation-level-documents.html https://www.istqb.org/downloads/send/2-foundation-level-documents/281-istqb-ctfl-syllabus-2018-v3-1.html

The purpose of a test is to discover bugs. If the test fails, it proves the presence of a bug (or the test is set up incorrectly). If the test passes, it proves that this particular test setup did not trigger a bug. It does not prove that the code is correct and free of bugs.

So should you write automated tests for all possible cases to ensure correctness? No, say the ISTQB principles: “Exhaustive testing is impossible”. It is neither technically feasible nor worthwhile to write tests for all possible inputs and conditions. Instead, you should assess the risks of a certain case and write tests for high-risk cases first.

Even if it was viable to cover all cases, it would give you a false sense of security. No software is without errors, and a fully tested software may still be a usability nightmare that does not satisfy its users.

Another core idea is that testing depends on its context and that it needs to be adapted again and again to provide meaning. The specific context in this guide are single-page web applications written in JavaScript, made with Angular. Such applications need specific testing methods and tools we will get to know.

Once you have learned and applied these tools, you should not stop. A fixed toolchain will only discover certain types of bugs. You need to try out different approaches to find new classes of bugs. Likewise, an existing test suite needs to be updated regularly so that it still finds regressions.

Tailoring your testing approach

While there are several competing schools of thoughts and methodologies, there is not one correct approach to testing. Learn from other’s experience, but develop a testing approach that suits your application, your team, your project or business.

Before you start setting up tests, you should examine the current situation of your application:

  • What are the critical features? For example, logging in, searching for a record and editing a form.
  • Which technical obstacles frustrate your users? For example, bad cross-browser compatibility.
  • What are the frequently reported technical problems? For example, your application may lack error handling.
  • What are the technical requirements? For example, your application needs to consume structured data from a given back-end API. In turn, it needs to expose certain URL routes.

This technical assessment is as important as an inquiry of your development team:

  • What is the overall attitude on testing? For example, some developers value testing while others find it ineffective to avoid bugs.
  • What is the current testing practice? For example, developers sometimes write tests, but not as a daily routine.
  • What is the experience on writing tests? For example, some developers have written tests for several environments, while others understand the basic concepts but have not yet gotten into practice.
  • What are the obstacles that impede a good testing routing? For example, developers have not been trained on the existing testing tools.
  • Are tests well-integrated into your development workflow? For example, a continous integration server automatically runs the test suite on every change set.

Once you have answered these questions, you should set up a testing goal and implements steps to achieve it. A good start is to think economically. What is the return on investment of writing a test? Pick the low-hanging fruits. Find business-critical features and make sure they are covered by tests. Write tests that are easy to write but cover large parts of the code.

Simultaneously, integrate testing into your team’s workflow. Make sure everyone shares the same basic expertise. Offer formal training workshops and pair experienced programmers with team members less familiar with testing. Appoint maintainers and contact persons for test quality and testing infrastructure. If applicable, hire dedicated software testers.

Writing automated tests should be easy and fun for your team members. Remove any obstacles that make testing difficult or inefficient.

The right amount of testing

A fierce debate revolves around the right amount of testing. Too little testing is a problem: Features are not properly specified, bugs go unnoticed, regressions happen. But too much testing consumes development time, yields no additional profit and slows down development in the long run.

So we need to reach a sweet spot. If your testing practice deteriorates from this spot, you run into problems. If you add more tests, you observe little benefit.

Tests differ in their value and quality. Some tests are more meaningful than others. If they fail, your application is actually unusable. This means the quality of tests is more important than their quantity.

A common metric of testing is code coverage. It counts the lines in your code that are called by your tests. It tells you which parts of your code (file, method/function, block, expression etc.) are executed at all. Code coverage is typically expressed as percent values, e.g. 79% statements, 53% branches, 74% functions, 78% lines.

This metric on testing is useful but also deeply flawed because the value of a test cannot be quantified automatically. Code coverage tells you whether a piece of code was called, regardless of its importance. The coverage report may point to important behavior that is not yet covered by tests, but should be. It does not tell whether the existing tests are meaningful and make the right expectations. You can merely infer that the code does not throw exceptions under test conditions.

It is controversial whether one should strive for 100% code coverage. While it is feasible to cover 100% of certain business-critical code, it requires immense efforts to cover all parts of an application written in Angular and TypeScript. If you write tests for the main features of your app from a user’s perspective, you can easily achieve a code coverage of 60-70%. Every extra percent gain takes more and more time and leads to weird and twisted tests that do not reflect the actual usage of your application.

We are going to discuss the practical use of code coverage tools later.

Levels of testing

We can distinguish automated tests by their perspective and proximity to the code.

End-to-end tests

Some tests have a high-level, bird’s-eye view on the application. They try to replicate a user interacting with the application: Navigating to an address, reading text, clicking on a link or button, filling out a form, moving the mouse or typing on the keyboard. They make expectations about what the user sees and reads in the browser.

From the user’s perspective, it does not matter that your application is implemented in Angular. Technical details like the inner structure of your code are not relevant. There is no distinction between front-end and back-end, between parts of your code. The full experience is tested.

These tests are called end-to-end (E2E) tests since they integrate all parts of the application from one end (the user) to the other end (the darkest corners of the back-end). End-to-end tests also form the automated part of acceptance tests since they tell whether the application works for the user.

Unit tests

Other tests have a low-level, worm’s-eye view on the application. They pick a small piece of code and put it through its paces. From this perspective, implementation details matter. The developer needs to set up an appropriate testing environment to trigger all relevant cases.

The shortsighted worm only sees what is directly in front. This perspective tries to cut off the ties of the code under test with its dependencies. It tries to isolate the code in order to examine it.

These tests are called unit tests. A unit is a small piece of code that is reasonable to test.

Integration tests

Between these two extreme perspectives, there are tests that operate on specific parts of the code, but test cohesive groups. They prescind from implementation details and try to take the user’s perspective.

These tests are called integration tests since they test how well the parts integrate into the group. For example, all parts of one feature may be tested together. An integration test proves that all parts work together properly.

Distribution of testing efforts

All levels of testing are necessary and valuable. Different types of tests need to be combined to create a thorough test suite. But how should we divide our attention? On which level should we spend most of the time? Should we focus on end-to-end tests since they mimic how the user interacts with the application? Again, this is a controversial issue among testing experts.

What is indisputable is that high-level tests like end-to-end tests are expensive and slow, while lower-level tests like integration and unit tests are cheaper and faster.

End-to-end tests are brittle, meaning they often produce false negatives. Sometimes they fail for now apparent reason – when you run the same tests again, they suddenly pass. Even if the test failure is a true negative, it is hard to find the root cause of the problem. You need to wander through the full stack to locate the bug.

End-to-end test are computationally expensive and run slow. Since they use a real, sometimes “headless” browser and run against the full software stack, the testing setup is immense. You need to deploy front-end, back-end, databases, caches etc. to testing machines and then have machines to run the end-to-end tests.

Several commercial tools try to make end-to-end tests easier, faster and more robust. Since end-to-end tests are unreliable because of their complexity, these tools try to reduce complexity at the expense of flexibility. For example, they remote-control one possible browser instead of all browsers that support a generic automation protocol like WebDriver.

In comparison, integration tests are simpler and unit tests even more so. Since they have less moving parts and fewer dependencies, they run faster and the results are reproducible. The setup is relatively simple. Integration and unit tests typically run on one machine with a build of the code under test.

The crucial question for dividing your testing efforts is: Which tests yield the most return on investment? How much work is it to maintain a test in relation to its benefit?

In theory, the benefit of end-to-end tests is the highest, since they indicate whether the application works for the user. In practice, they are flaky, imprecise, painful to write and debug. The business value of integration and unit tests is estimated higher.

For this reason, some experts argue you should write few end-to-end test, a fair amount of integration tests and many unit tests. If this distribution is visualized, it looks like a pyramid:

End to end Integration Unit

These proportions are known as the Testing Pyramid. They are widely recognized in software testing across domains, platforms and programming languages.

However, this common distribution also drew criticism. In particular, experts disagree on the value of unit tests.

On the one hand, unit tests are precise and cheap. They are ideal to specify all tiny details of a shared module. They help you to design small, composable modules that “do one thing and do it well”. This level of testing forces you to reconsider how the module interacts with other modules.

On the other hand, unit test are too low-level to check whether a certain feature works for the user. They give you little confidence that your application works. In addition, unit tests might increase the cost of every code change.

Unit tests run the risk of mirroring or even duplicating implementation details. These details change frequently because of new requirements elsewhere or during internal refactorings. If you change a line of code somewhere, some distant unit test suddenly fails. This makes sense if you have touched shared types or shared logic, but it may just be a false alarm. You have to fix this failing test for technical reasons, not because something broke.

Integration tests provide a better tradeoff. These mid-level tests prescind from implementation details, cover a group of code units and provide more confidence. They are less likely to fail if you refactor code inside of the group.

That is why some experts deem integration tests more valuable and recommend that you spend most of your testing efforts on this level.

In Angular, the difference between unit and integration tests is sometimes subtle. A unit test typically focusses on a single Angular Component, Directive, Service, Pipe etc. Dependencies are replaced with fakes. An integration test spans one component together with its children and possibly connected Services as well. It is also possible to write a test that integrates all parts of an Angular Module.

Comparison of software testing levels
Level Coverage Performance Reliability Isolate Failures Simulate the Real User
End-to-End full bad unreliable hard yes
Integration large fair reliable fair no
Unit small best most reliable easy no

(Table adapted from Just Say No to More End-to-End Tests by Mike Wacker.)

Black box vs. white box testing

Once you have identified a piece of code you would like to test, you have to decide how to test it properly. One important distinction is whether a test treats the implementation as a closed box (black box) or an opened box (white box). In this metaphor, the code under test is seen as a machine in a box with holes for inputs and outputs.

Black box testing does not assume anything about the internal structure. It puts certain values into the box and expects certain output values. The test talks to the publicly exposed, documented API consisting of classes, methods and functions. The inner state and workings are not examined.

Input Black box Output

White box testing opens the box, sheds light on the internals and takes measurements by reaching into the box. For example, a white box test calls methods that are not necessarily part of the public API, but still technically tangible. Then it checks the internal state and expects that it has changed accordingly.

While both approaches have their value, this guide recommends to write black box tests whenever possible. You should check what the code does for the user and for other parts of the code. For this purpose, it is not relevant how the code looks internally. Tests that make assumptions about internals are likely to break in the future when the implementation slightly changes.

More importantly, white box tests run the risk of forgetting to check the real output. They reach into the box, spin some wheel, flip some switch and check a particular state. They just assume the output without actually checking it. So they fail to cover important code behavior.

For an Angular Component, Directive, Service, Pipe etc., a black box test passes a certain input and expects a proper output or measures side effects. The test only calls methods that are marked with public in the TypeScript code. Internal methods should be marked with private.

Example applications

In this guide, we will explore the different aspects of testing Angular applications by looking at two examples.

The Counter Component

The counter is a reusable Component that allows to increment, decrement and reset a number using buttons and input fields.

For advanced Angular developers, this might look trivial. That is intentional. This guide assumes that you know Angular basics and that you are able to build a counter Component, but still struggle testing the ins and outs.

The goals of this example are:

  • Simplicity: Quickly grasp what the Component is supposed to do.
  • Covering core Angular features: Reusable Components with state, Inputs, Outputs, templates, event handling.
  • Scalability: Starting point for more complex application architectures.

The counter comes in three flavors with different state management solutions:

  1. An independent, self-sufficient counter Component that holds its own state.
  2. A counter that is connected to a Service using dependency injection. It shares its state with other counters and changes it by calling Service methods.
  3. A counter that is connected to a central NgRx Store. (NgRx is a state management library we will introduce later.) The counter changes the state indirectly by dispatching NgRx Actions.

While this example seems trivial to implement, it already offers valuable challenges from a testing perspective.

This app that allows to search public photos on Flickr, the popular photo hosting site. First, the user enters a search term and starts the search. The Flickr search API is queried. Second, a list of search results with thumbnails is rendered. Third, the user might select a search result to see the photo details.

This application is straightforward and relatively simple to implement. Still it raises important questions:

  • App structure: How to split responsibilities into Components and how to model dependencies.
  • API communication: How to fetch data by making HTTP requests and update the user interface.
  • State management: Where to hold the state, how to pass it down in the Component tree, how to alter it.

The Flickr Search comes in two flavors using different state management solutions:

  1. The state is managed in the top-level Component, passed down in the Component tree and changed using Outputs.
  2. The state is managed by an NgRx Store. Components are connected to the store to pull state and dispatch Actions. The state is changed in a Reducer. The side effects of an Action are handled by NgRx Effects.

Once you are able to write automatic tests for this example application, you will be able to test most features of a typical Angular application.

Angular testing principles

Testable application parts

In contrast to other popular front-end JavaScript libraries, Angular is a an opinionated, comprehensive framework that covers all important aspects of developing a JavaScript web application. Angular provides high-level structure, low-level building blocks and means to bundle everything together into a usable application.

The complexity of Angular cannot be understood without considering automated testing. Why is an Angular application structured into Components, Services, Modules etc.? Why are the parts intertwined the way they are? Why do all parts of an Angular application apply the same patterns?

An important reason is testability. Angular’s architecture guarantees that all application parts can be tested easily in a similar way.

We know from experience that code that is easy to test is also simpler, better structured, easier to read and easier to understand. The main technique of writing testable code is to break code into smaller chunks that “do one thing and do it well”. Then couple the chunk loosely.

Dependency injection and fake objects

A major design pattern for loose coupling is dependency injection and the underlying inversion of control. Instead of creating a dependency itself, an application part merely declares the dependency. The tedious task of creating and providing the dependency is delegated to an injector that sits on top.

This division of work decouples an application part from its dependencies: One part does not need to know how to set up a dependency, let alone the dependency’s dependencies and so forth.

Dependency injection turns tight coupling into loose coupling. A certain application part no longer depends on a specific class, function, object or other value. It rather depends on an abstract token that can be traded in for a concrete implementation. The injector takes the token and exchanges it for a real value.

This is of immense importance for automated testing. In our test, we can decide how to deal with a dependency:

  • We can either provide an original, fully-functional implementation. In this case, we are writing an integration test that includes direct and indirect dependencies.
  • Or we provide a fake implementation, also called mock, that does not have side effects. In this case, we are writing a unit test that tries to test the application part in isolation.

A large portion of the time spent while writing tests is spent on decoupling an application part from its dependencies. This guide will teach you how to set up the test environment, isolate an application part and reconnect it with equivalent fake objects.

Angular’s testing tools

Angular provides solid testing tools out of the box. When you create an Angular project using the Angular command line interface, it comes with a fully-working testing setup for unit, integration and end-to-end tests.

So the Angular team already made important decisions for you: Jasmine as testing framework, Karma as test runner as well as Protractor for running end-to-end tests. Implementation and test code are bundled with Webpack. Application parts are typically tested inside Angular’s TestBed.

This setup works well and covers most cases. It is a trade-off with strengths and weaknesses. Since it is merely one possible way to test Angular applications, you could compile your own testing toolchain. For example, some people use Jest instead of Jasmine and Karma. Some people swap Protractor with Cypress. Some people use Spectator or the Angular Testing Library as an abstraction instead of using TestBed directly.

Other testing tools are not simply better or worse, but make different trade-offs. This guide assumes you begin with the recommended setup. Later, once you have reached its limits, you should investigate whether alternatives make testing your specific application easier, faster and more reliable.

Testing conventions

Angular offers some tools and conventions on testing, but you need to decide how to apply them. Because there are opposing testing methodologies, the tools are flexible enough to support different ways of testing.

This freedom of choice benefits experts, but confuses and paralyses beginners. In your project, there should be one preferrable way how to test a specific application part. You need to make choices and set up project-wide conventions and patterns.

The testing tools that ship with Angular are low-level. They merely provide the basic operations. If you use these tools directly, your tests become messy, repetitive and hard to maintain. You need to create high-level testing tools that cast your conventions into code in order to write short, readable and understandable tests.

This guide values strong conventions and introduces simple helper functions that follow essential testing conventions. Again, your mileage may vary. You should adapt these tools to your needs or build higher-level testing helpers.

Test suites with Jasmine

Angular ships with two tools that enable you to write and execute unit and integration tests: Karma and Jasmine.

Jasmine is a testing framework consisting basically of three parts:

  1. A library with classes and functions for constructing tests.
  2. A test execution engine.
  3. A reporting engine that outputs test results in different formats.

If you are new to Jasmine, I recommend reading the official Jasmine tutorial.

This guide does not provide a full introduction to Jasmine, but let us recap Jasmine’s basic structure and terminology that will be used throughout this guide.

Creating a Jasmine suite

In terms of Jasmine, a test consists of one or more suites. A suite is declared with a describe block:

describe('Suite description', () => {
  /* … */
});

Each suite describes a piece of code, the code under test.

describe is a function that takes two parameters. The first parameter is a string with a human-readable name. Typically, contains the name of the class or function under test. For example, describe('IndependentCounterComponent', /* … */) if the suite is testing the IndependentCounterComponent class. The second parameter is a function containing the suite definition.

Specifications

Each suit consists of one of more specifications, or shorter, specs. A spec is declared with an it block:

describe('Suite description', () => {
  it('Spec description', () => {
    /* … */
  });
  /* … more specs …  */
});

Again, it is a function that takes two parameters. The first parameter is a string with a human-readable spec description. The second parameter is a function containing the spec code.

The pronoun it refers to the code under test. it should be the subject of a human-readable sentence that asserts the behavior of the code under test. The actual spec code then prove this assertion. This style of writing specs originates from the concept of Behavior-Driven Development (BDD).

One goal of BDD is to describe software behavior in a natural language – in this case, English. Every stakeholder should be able to read the it sentences and understand how the code is supposed to behave. Team members without JavaScript knowledge should be able to add more requirements by forming it does something sentences.

Ask yourself, what does the code under test do? For example, in case of a IndependentCounterComponent, it increments the counter value. And it resets the counter to a specific value. So you could write:

it('increments the count', () => {
  /* … */
});
it('resets the count', () => {
  /* … */
});

After it, typically a verb follows, like increments and resets in the example.

Some people prefer to write it('should increment the count', /* … */), but I see no value in the additional should. The nature of a spec is to state what the code under test should do. So the word “should” is redundant and just makes the sentence longer. My recommendation is to simply state what the code does.

Structure of a test

Inside the it block lies the actual testing code. Irrespective of the testing framework, the testing code typically consists of three phases: Arrange, Act and Assert.

  1. Arrange is the preparation and setup phase. For example, the class under test is instantiated. Dependencies are set up. Spies and fakes are created.
  2. Act is the phase where interaction with the code under test happens. For example, a method is called or an HTML element in the DOM is clicked.
  3. Assert is the phase where the code behavior is checked and verified. For example, the actual output is compared to the expected output.

How could the structure of the spec it('resets the count', /* … */) for the IndependentCounterComponent look like?

  1. Arrange:

    • Create an instance of IndependentCounterComponent.
    • Render the Component into the document.
  2. Act:

    • Find and focus the reset input field.
    • Enter the text “5”.
    • Find and click the “Reset” button.
  3. Assert:

    • Expect that the displayed count now reads “5”.

This structure makes it easier to come up with a test and also to implement it. Ask yourself:

  • What is the necessary setup? Which dependencies do I need to provide? How do they behave? (Arrange)
  • What is the user input or API call that triggers the behavior I would like to test? (Act)
  • What is the expected behavior? How do I prove that the behavior is correct? (Assert)

In Behavior-Driven Development (BDD), the three phases of a test are fundamentally the same. But they are called Given, When and Then. These plain English words try to avoid technical jargon and allow a natural way to think of a test’s structure: “Given these specific conditions, when the user interacts with the application, then it behaves in a certain way.”

Expectations

In the Assert phase, the test compares the actual output or return value to the expected output or return value. If they are the same, the test passes. If they differ, the test fails.

Let us examine a simple contrived example, an add function:

const add = (a, b) => a + b;

A primitive test without any testing tools could look like this:

const expectedValue = 5;
const actualValue = add(2, 3);
if (expectedValue !== actualValue) {
  throw new Error(
    `Wrong return value: ${actualValue}. Expected: ${expectedValue}`
  );
}

We could write that code in a Jasmine spec, but Jasmine allows to create expectations in an easier and more concise manner: The expect function together with a Matcher.

const expectedValue = 5;
const actualValue = add(2, 3);
expect(actualValue).toBe(expectedValue);

First, we pass the actual value to the expect() function. It returns an expectation object with methods for checking the actual value. We would like to compare the actual value to the expected value, so we use the toBe matcher.

toBe is the simplest matcher that applies to all possible JavaScript values. Internally, it uses JavaScript’s strict equality operator ===, also called identity operator. expect(actualValue)​.toBe(expectedValue) essentially runs actualValue === expectedValue.

toBe is useful to compare primitive values like strings, numbers and booleans. For objects, toBe matches only if both objects are the same. It fails if two objects are not identical, even if they happen to have the same properties and values.

For checking the deep equality of two objects, Jasmine offers the toEqual matcher. This example illustrates the difference:

// Fails, objects are not identical
expect({ name: 'Linda' }).toBe({ name: 'Linda' });
// Passes, objects are not identical but deeply equal
expect({ name: 'Linda' }).toEqual({ name: 'Linda' });

Jasmine has numerous useful matchers built-in, toBe and toEqual being the most common. You can add custom matchers to hide a complex check behind a short name.

The pattern expect(actual).toEqual(expectedValue) originates from Behavior-Driven Development (BDD) again. The code forms a human-readable sentence: “Expect the actual value to equal the expected value.” The expect function call and the matcher methods starting with to allow to form a readable sentence. The goal is to write a specification that is as readable as a plain text but can be verified automatically.

Efficient test suites

When writing multiple specs in one suite, you quickly realize that the Arrange phase is similar or even identical across these specs. For example, when testing the IndependentCounterComponent, the Arrange phase always consists of creating an instance of the class and rendering the Component into the document.

This setup is repeated over and over, so it should be defined once at a central place. You could simply write a setup function and call it at the beginning of each spec. But Jasmine allows to declare code that is called before and after each spec, or before and after all specs. Fur this purpose, there are four functions: beforeEach, afterEach, beforeAll and afterAll. They are called inside of a describe block, just like it. They expect one parameter, a function that is called at the given stages.

describe('Suite description', () => {
  beforeAll(() => {
    console.log('Called before all specs are run');
  });
  afterAll(() => {
    console.log('Called after all specs are run');
  });

  beforeEach(() => {
    console.log('Called before each spec is run');
  });
  afterEach(() => {
    console.log('Called after each spec is run');
  });

  it('Spec 1', () => {
    console.log('Spec 1');
  });
  it('Spec 2', () => {
    console.log('Spec 2');
  });
});

This suite has two specs and defines shared setup and teardown code. The output is:

Called before all specs are run
Called before each spec is run
Spec 1
Called after each spec is run
Called before each spec is run
Spec 2
Called after each spec is run
Called after all specs are run

Faking dependencies

When testing a piece of code, you need to decide between an integration test and a unit test. To recap, the integration test includes (“integrates”) the dependencies. In contrast, the unit test replaces the dependencies with fakes in order to isolate the code under test.

These replacements are also called doubles, stubs or mocks. Replacing of a dependency is called stubbing or mocking. Since these terms are used inconsistently and their difference is subtle, this guide uses the umbrella term “fake” and “faking” for any dependency substitution.

Rules for faking dependencies

Unit tests isolates a piece of code to scrutinize all its details. Creating and injecting fake dependencies is essential for unit tests. This technique is double-edged – it is powerful and dangerous at the same time. We need to set up rules to apply the technique safely.

A fake implementation must have the same shape the original. If the dependency is a function, the fake must have the same signature, meaning the same parameters and the same return value. If the dependency is an object, the fake must have the same public API, meaning the same methods and properties.

The fake does not need to be complete, but sufficient enough to act as a replacement. Like a fake building on a movie set, the outer shape needs to be indistinguishable from an original. But behind the authentic facade, there is only a wooden scaffold.

The biggest danger of creating a fake is that it does not properly mimic the original. Even if the fake resembles the original at the time of writing the code, it might easily get of sync later when the original is changed.

When the original dependency changes its public API, dependent code needs to be adapted. Also, the fake needs to be aligned to the changed API. Otherwise the corresponding unit test produces a false positive. When a fake is outdated, the unit test is a dreamworld where everything works and all tests are green. In reality, the code under test is broken.

How can we ensure that the fake is up to date with the original? We can use TypeScript to enforce that the fake has a matching type. If the code involved is properly typed, TypeScript reminds us to update the implementation and the fake – the code simply does not compile if we forget to. We will see how to declare matching types in the upcoming examples.

Faking function dependencies with Jasmine spies

Jasmine provides simple yet powerful patterns to create fake implementations. The most basic pattern is the Jasmine spy for replacing a function dependency.

In its simplest form, a spy is a function that records its calls. For each call, it records the function parameters. Using this record, we later assert that the spy has been called with particular input values. For example, we declare in a spec: “Expect that the spy has been called two times with the values mickey and minnie, respectively.”

Like every other function, a spy can have a meaningful return value. In the simple case, this may be a fixed value. The spy will always return the same value, regardless of the input parameters. In a more complex case, the return value may originate from an underlying fake function.

A standalone spy is created by calling jasmine.createSpy:

const spy = jasmine.createSpy('name');

createSpy expects one parameter, an optional name. It is recommended to pass a name that describes the original. The name will be used in error messages when you make expectations against the spy.

Assuming we have class TodoService responsible for fetching a to-do list from the server. The class uses the Fetch API to make an HTTP request. (This is a plain JavaScript example. It is uncommon to use fetch directly in an Angular app.)

class TodoService {
  constructor(
    // Bind `fetch ` to `window` to ensure that `window` is the `this` context
    private fetch = window.fetch.bind(window)
  ) {}

  public async getTodos(): Promise<string[]> {
    const response = await this.fetch('/todos');
    if (!response.ok) {
      throw new Error(
        `HTTP error: ${response.status} ${response.statusText}`
      );
    }
    return await response.json();
  }
}

The TodoService uses a pattern called constructor injection, meaning the fetch dependency can be injected via an optional constructor parameter. In production code, this parameter is empty and defaults to the original window.fetch. In the test, a fake dependency is passed to the constructor. The fetch parameter, whether original or fake, is saved as an instance property this.fetch. Eventually, the public method getTodos uses it to make an HTTP request.

In our unit test, we do not want the service to make any HTTP requests. We pass in a Jasmine spy as replacement for window.fetch.

// Fake todos and response object
const todos = [
  'shop groceries',
  'mow the lawn',
  'take the cat to the vet'
];
const okResponse = new Response(JSON.stringify(todos), {
  status: 200,
  statusText: 'OK',
});

describe('TodoService', () => {
  it('gets the to-dos', async () => {
    // Arrange
    const fetchSpy = jasmine.createSpy('fetch')
      .and.returnValue(okResponse);
    const todoService = new TodoService(fetchSpy);

    // Act
    const actualTodos = await todoService.getTodos();

    // Assert
    expect(actualTodos).toEqual(todos);
    expect(fetchSpy).toHaveBeenCalledWith('/todos');
  });
});

There is a lot to unpack in this example. First, we set up the fake data before the describe block:

const todos = [
  'shop groceries',
  'mow the lawn',
  'take the cat to the vet'
];
const okResponse = new Response(JSON.stringify(todos), {
  status: 200,
  statusText: 'OK',
});

First, we define the fake data we want the fetch spy to return. Essentially, this is an array of strings.

The original fetch function returns a Response object. We create one using the built-in Response constructor. The original server response is a string before it is parsed as JSON. So we need to serialize the array into a string before passing it to the Response constructor. (You do not have to understand these fetch details to grasp the overall example.)

Then, we declare a test suite using describe:

describe('TodoService', () => {
  /* … */
});

The suite contains one spec that tests the getTodos method:

it('gets the to-dos', async () => {
    /* … */
});

The spec starts with Arrange code:

// Arrange
const fetchSpy = jasmine.createSpy('fetch')
  .and.returnValue(okResponse);
const todoService = new TodoService(fetchSpy);

Here, we create a spy. With .and.returnValue(…), we set a fixed return value: the successful response.

We also create an instance of TodoService, the class under test. We pass the spy into the constructor.

In the Act phase, we call the method under test:

// Act
const actualTodos = await todoService.getTodos();

getTodos returns a Promise We use an async function together with await to access the return value easily. Jasmine deals with async functions just fine and waits for them to complete.

In the Assert phase, we make two expectations:

// Assert
expect(actualTodos).toEqual(todos);
expect(fetchSpy).toHaveBeenCalledWith('/todos');

First, we verify the return value. We compare the actual data (actualTodos) with the fake data the spy has returned (todos). If they are equal, we have proven that getTodos parsed the response as JSON and returned the result. (Since there is no other way getTodos could access the fake data, we can also deduce that the spy has been called at all.)

Second, we verify that the fetch spy has been called with the correct parameter, the API endpoint URL. Jasmine offers several matchers for making expectations on spies. The example uses toHaveBeenCalledWith to assert that the spy has been called with the parameter '/todos'.

Both expectations are necessary to guarantee that getTodos works correctly.

After having written the first spec for getTodos, we need to ask ourselves: Did the test fully cover its behavior? We did test the success case, also called happy path, but the error case, also called unhappy path, is yet to be tested. In particular, this error handling code:

if (!response.ok) {
  throw new Error(
    `HTTP error: ${response.status} ${response.statusText}`
  );
}

When the server response is not “ok”, we throw an error. “Ok” means the HTTP response status code is 200-299. Examples of “not ok” are 403 Forbidden, 404 Not Found and 500 Internal Server Error. Throwing an error rejects the Promise so the caller of getTodos knows that fetching the to-dos failed.

The fake okResponse mimics the success case. For the error case, we need to define another fake Response. Let us call it errorResponse with the notorious HTTP status 404 Not Found:

const errorResponse = new Response('Not Found', {
  status: 404,
  statusText: 'Not Found',
});

Assuming the server does not return JSON in the error case, the response body is simply the string 'Not Found'.

Now we add a second spec for the error case:

describe('TodoService', () => {
  /* … */
  it('handles an HTTP error when getting the to-dos', async () => {
      // Arrange
      const fetchSpy = jasmine.createSpy('fetch')
        .and.returnValue(errorResponse);
      const todoService = new TodoService(fetchSpy);

      // Act
      let error;
      try {
        await todoService.getTodos();
      } catch (e) {
        error = e;
      }

      // Assert
      expect(error).toEqual(new Error('HTTP error: 404 Not Found'));
      expect(fetchSpy).toHaveBeenCalledWith('/todos');
    });
});

In the Arrange phase, we inject a spy that returns the new error response.

In the Act phase, we call the method under test but anticipate that it throws an error. In Jasmine, there are several ways to test whether a Promise has been rejected with an error. The example above wraps the getTodos call in a try/catch statement and saves the error. Most likely, this is how implementation code would handle the error.

In the Assert phase, we make two expectations again. Instead of verifying the return value, we make sure the caught error is an Error instance with a useful error message. Finally, we verify that the spy has been called with the right value, just like in the spec for the success case.

Again, this is a plain JavaScript example to illustrate the usage of spies. Usually, an Angular Service does not use fetch directly but uses HttpClient instead. The test typically uses HttpTestingController. We will get to know this way of testing later.

Spying on existing object methods

We have used jasmine.createSpy('name') to create a standalone spy and have injected it into the constructor. Explicit constructor injection is a straightforward and recommended way to provide a dependency. It is also the predominant dependency injection strategy in Angular.

Sometimes, there is already an object whose method we need to spy on. This is especially helpful if the code uses global methods from the browser environment, like window.fetch in the example above.

For this purpose, we can use the spyOn() method:

spyOn(window, 'fetch');

This installs a spy on the global fetch method. Under the hood it saves the original window.fetch function for later and overwrites window.fetch with a spy. Once the spec is completed, Jasmine automatically restores the original function.

spyOn returns the created spy, allowing to set a return value, like we have learned above.

spyOn(window, 'fetch');
  .and.returnValue(okResponse);

We can create a version of TodoService that does not rely on construction injection, but uses fetch directly:

class TodoService {
  public async getTodos(): Promise<string[]> {
    const response = await fetch('/todos');
    if (!response.ok) {
      throw new Error(
        `HTTP error: ${response.status} ${response.statusText}`
      );
    }
    return await response.json();
  }
}

The test suite then uses spyOn to catch all calls to window.fetch:

// Fake todos and response object
const todos = [
  'shop groceries',
  'mow the lawn',
  'take the cat to the vet'
];
const okResponse = new Response(JSON.stringify(todos), {
  status: 200,
  statusText: 'OK',
});

describe('TodoService', () => {
  it('gets the to-dos', async () => {
    // Arrange
    spyOn(window, 'fetch')
      .and.returnValue(okResponse);
    const todoService = new TodoService();

    // Act
    const actualTodos = await todoService.getTodos();

    // Assert
    expect(actualTodos).toEqual(todos);
    expect(window.fetch).toHaveBeenCalledWith('/todos');
  });
});

Not much has changed here. We spy on fetch and make it return okResponse. Since window.fetch is overwritten with a spy, we make the expectation against it to verify that it has been called.

Creating standalone spies and spying on existing methods are not mutually exclusive. Both will be used frequently when testing Angular applications, and both work well with dependencies injected into the constructor.

Faking object dependencies

We have learned to use spies to fake dependencies on individual functions or methods. But most of the time, dependencies are objects with methods. This includes instances of classes.

In the counter example, the ServiceCounterComponent depends on the CounterService. In ServiceCounterComponent’s unit test, we need to create and provide an appropriate fake CounterService. This is the outer shape of CounterService:

class CounterService {
  public getCount(): Observable<number> { /* … */ }
  public increment(): void { /* … */ }
  public decrement(): void { /* … */ }
  public reset(newCount: number): void { /* … */ }
  private notify(): void { /* … */ }
}

How do we create a fake instance of CounterService? The simplest way is to use an object literal {…} with methods:

const fakeCounterService = {
  getCount() {
    return of(count);
  },
  increment() {},
  decrement() {},
  reset() {},
};

This is far from perfect, but already a viable replacement for a CounterService instance. It walks like the original and talks like the original. The methods are empty or return fixed data.

The fake implementation happens to have the same shape as the original. As we have discussed, it is of utter importance that the fake remains up to date with the original. This is not yet enforced by TypeScript. We want TypeScript to check whether the fake properly replicates the original. The first attempt would be add a type declaration:

const fakeCounterService: CounterService = {
  /* … */
};

Unfortunately, this does not work. TypeScript complains that the private method notify is missing. That is correct, but we cannot add a private method to the object literal, nor should we. To fix this problem, we use a TypeScript trick. Using Pick and keyof, we create a derived type that only contains the public methods:

const fakeCounterService: Pick<CounterService, keyof CounterService> = {
  /* … */
};

This type ensures that the fake looks exactly the same as the original. This prevents the fake and therefore the whole test to get out of sync with the original. When the original CounterService changes its public API, the dependents ServiceCounterComponent needs to be adapted. Likewise, fakeCounterService, the fake implementation of CounterService, needs to reflect the change. The type declaration reminds you to update the fake.

If the code under test does not use the full API, the fake does not need to provide the full API either. We can declare only those methods and properties that the code under test actually accesses.

For example, if the code under test only calls getCount, just provide this method. Make sure to add a type declaration that picks the method from the original type:

const fakeCounterService: Pick<CounterService, 'getCount'> = {
  getCount() {
    return of(count);
  },
};

TypeScript’s mapped types allow to bind the fake object to the original type in a way that TypeScript can check whether they are equivalent.

A plain object literal with methods is an easy way to provide a fake instance. We should not forgot that the spec needs to verify that these methods have been called with the right parameters. Jasmine spies are the right tool for this job. We can combine the object literal with Jasmine spies. A first approach could look like this:

const fakeCounterService: Pick<CounterService, keyof CounterService> = {
  getCount: jasmine.createSpy('getCount').and.returnValue(of(count)),
  increment: jasmine.createSpy('increment'),
  decrement: jasmine.createSpy('decrement'),
  reset: jasmine.createSpy('reset'),
};

This is fine, but overly verbose. Jasmine provides a helper function to create an object with multiple spy methods, jasmine.createSpyObj(). It expects a descriptive name and a list of methods:

const fakeCounterService =
  jasmine.createSpyObj<CounterService>('CounterService', {
    getCount: of(count),
    increment: undefined,
    decrement: undefined,
    reset: undefined,
  });

The code above creates an object with four methods, all of them being spies. They return the values that are given. getCount returns an Observable<number>. The other methods return undefined.

createSpyObj accepts a type parameter to declare the type the object adheres to. We pass CounterService between angle brackets so TypeScript checks that the fake matches the original.

In the Assert phase, we can now make an expectation against the spies, for example:

expect(fakeCounterService.getCount).toHaveBeenCalled();

Karma

TODO

Testing Components

Components are the power houses of an Angular application. Together, they compose the user interface.

A component deals with several concerns, among others:

  • A component renders a certain HTML DOM using the template.
  • It accepts data from parent components using Input properties.
  • It emits data to parent components using Outputs.
  • It reacts to user input using event handlers.
  • It renders the content (ng-content) and templates (ng-template) that are passed.
  • It binds data to form controls and allows to edit the data.
  • It talks to services or other state managers.
  • It uses routing information like the current URL and its parameters.

All these tasks need to be tested properly.

Unit test for the Counter Component

As a first example, we are going to test the IndependentCounterComponent. This is how it looks in action:

When designing a Component test, the guiding questions are: What does the Component do, what needs to be tested? How do I test this behavior?

We will test the following features of the IndependentCounterComponent:

  • It displays the current count. The initial value is 0 and can be set by an Input.
  • When the user activates the “+” button, the count increments.
  • When the user activates the “-” button, the count decrements.
  • When the user enters a number into the reset input field and activates the reset button, the count is set to the given value.
  • When the user changes the count, an Output emits the new count.

Writing down what the Component does already helps to structure the unit test. The features above roughly translate into specs in a test suite.

TestBed

Several chores are necessary to render a Component in Angular, even the simple Counter Component. If you look into the main.ts and the AppModule of a typical Angular application, you find that a “platform” is created, a Module is declared and this Module is bootstrapped.

The Angular compiler translates the templates into JavaScript code. To prepare the rendering, an instance of the Component is created, dependencies are resolved and injected, inputs are set. Finally, the template is rendered into the DOM. For testing, you could do all that manually, but you would need to dive deeply into Angular internals.

Instead, the Angular team provides the TestBed to ease unit testing. The TestBed creates and configures an Angular environment so you can test particular application parts like Components and Services safely and easily.

Configuring the testing Module

The TestBed comes with a testing Module that is configured like normal Modules in your application: You can declare Components, Directives and Pipes, provide Services and other Injectables as well as import other Modules. TestBed has a static method configureTestingModule which accepts a Module definition:

TestBed.configureTestingModule({
  imports: [ /*… */ ],
  declarations: [ /*… */ ],
  providers: [ /*… */ ],
});

In a unit test, add those parts to the Module that are strictly necessary: the code under test, mandatory dependencies and fakes. For example, when writing a unit test for IndependentCounterComponent, we need to declare that Component class. Since the Component does not have dependencies, does not render other Components, Directives or Pipes, we are done.

TestBed.configureTestingModule({
  declarations: [ IndependentCounterComponent ],
});

Our Component under test is now part of a Module. We are ready to render it, right? Not yet. First we need to compile all declared Components, Directives and Pipes:

TestBed.compileComponents();

This instructs the Angular compiler to translate the template files into JavaScript code.

Since configureTestingModule() returns the TestBed class again, we can chain those two calls:

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

You will see this pattern in most Angular tests that rely on the TestBed.

Rendering the Component

Now we have a fully-configured testing Module with compiled components. Finally, we can render the Component under test using createComponent():

const fixture = TestBed.createComponent(IndependentCounterComponent);

createComponent() returns a ComponentFixture, essentially a wrapper around the Component with useful testing tools. We will learn more about the ComponentFixture later.

createComponent() renders the Component into a root element in the HTML DOM. Alas, something is missing. The Component is not fully rendered. All the static HTML is present, but the dynamic HTML is missing. The template bindings, like 8 in the example, are not evaluated.

In our testing environment, there is no automatic change detection. Even with the default change detection strategy, a Component is not automatically rendered and re-rendered on updates. In testing code, we have to trigger the change detection manually. This might be a nuisance, but it is actually a feature. It allows to test asynchronous behavior in a synchronous manner, which is much simpler.

So the last thing we need to do is to trigger change detection:

fixture.detectChanges();

TestBed + Jasmine

Now the code for rendering a component using the TestBed is complete. Let us wrap the code in a Jasmine test suite.

describe('IndependentCounterComponent', () => {
  let fixture: ComponentFixture<IndependentCounterComponent>;

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

  beforeEach(() => {
    fixture = TestBed.createComponent(IndependentCounterComponent);
    fixture.detectChanges();
  });

  it('', () => {
    /* TODO */
  });
});

Using describe, we define a test suite for the IndependentCounterComponent. Inside, there are two beforeEach blocks. The first beforeEach block configures the TestBed. The second renders the component.

Now we have built the scaffold for our test using the TestBed, we need to write the first spec. createComponent returns a fixture, an instance of ComponentFixture. What is the fixture and what does it provide?

ComponentFixture and DebugElement

The term fixture is borrowed from real-world testing of mechanical parts or electronic devices. A fixture is a standardized frame into which the test object is mounted. The fixture holds the device under test, connects to electrical contacts and allows to take measurements.

In the context of Angular, the ComponentFixture holds the component and provides a convenient interface to both the Component instance and the rendered DOM.

The fixture references the Component instance via the componentInstance property. In our example, it contains a IndependentCounterComponent instance.

const component = fixture.componentInstance;

The Component instance is mainly used to set Inputs and subscribe to Outputs, for example:

// This is a ComponentFixture<IndependentCounterComponent>
const component = fixture.componentInstance;
// Set Input
component.startCount = 10;
// Subscribe to Output
component.countChange.subscribe((count) => {
  /* … */
});

More on testing Inputs and Outputs later.

For accessing elements in the DOM, Angular has another abstraction: The DebugElement wraps the native DOM elements. The fixture’s debugElement property returns the host element of the component. For the IndependentCounterComponent, this is the app-independent-counter element.

const { debugElement } = fixture;

The DebugElement offers handy properties like properties, attributes, classes and styles to examine the node itself. The properties parent, children and childNodes help navigating in the DOM tree. They return DebugElements as well.

Often it is necessary to unwrap the DebugElement to access the native DOM element inside. Every DebugElement has a nativeElement property:

const { debugElement } = fixture;
const { nativeElement } = debugElement;
console.log(nativeElement.tagName);
console.log(nativeElement.textContent);
console.log(nativeElement.innerHTML);

nativeElement is typed as any because Angular does not know the exact type of the wrapped DOM element. Most of the time, it is a subclass of HTMLElement. For example, a button element is represented as HTMLButtonElement in the DOM. When you use nativeElement, you need to learn about the DOM interface of the specific element.

Writing the first spec

We have compiled a test suite that renders the IndependentCounterComponent. We have met Angular’s primary testing abstractions: TestBed, ComponentFixture and DebugElement.

Now let us roll up our sleeves and write the first spec! The main feature of our little counter is the ability to increment the count. Hence the spec:

it('increments the count', () => {
  /* … */
});

The Arrange, Act and Assert structure helps us to structure the spec. We have already covered the Arrange phase in the beforeEach blocks that render the Component. In the Act phase, we click on the increment button. In the Assert phase, we check that the displayed count has incremented.

it('increments the count', () => {
  // Act: Click on the increment button
  // Assert: Expect that the displayed count now reads “1”.
});

To click on the increment button, two actions are necessary:

  1. Find the increment button element in the DOM.
  2. Fire a click event on it.

Let us learn about finding elements in the DOM first.

Querying the DOM with test ids

Every DebugElement features the methdos query and queryAll for finding descendant elements (children, grandchildren etc.). query returns the first decendant element that meets a condition while queryAll returns an array of all matching elements. Both methods expect a predicate, that is a function judging every element and returning true or false.

Angular ships with predefined predicate functions that allow to query the DOM using familiar CSS selectors. For this purpose, pass By.css('…') with a CSS selector to query and queryAll.

const { debugElement } = fixture;
// Find the first h1 element
const h1 = debugElement.query(By.css('h1'));
// Find all elements with the class .user
const userElements = debugElement.query(By.css('.user'));

The return value of query is a DebugElement again, those of queryAll is an array of DebugElements (DebugElement[] in TypeScript notation).

In the example above, we have used a type selector (h1) and a class selector (.user) to find elements in the DOM. For everyone familiar with CSS, this is familiar as well. While these selectors are fine when styling Components, using them in a test needs to be challenged.

Type and class selectors introduce a tight couling between the test and the template. HTML elements are picked for semantic reasons and classes are picked mostly for visual styling. Both change frequently when the Component template is refactored. Should the test fail if the element type or class changes?

Sometimes the element type or the class are crucial for the feature under test. But most of the time, they are not relevant for the feature. Then, the test should better find the element by a feature that never changes and that bears no additional meaning: test ids.

A test id is an identifier given to an element just for the purpose of finding it in a test. The test will still find the element if unrelated features change.

The preferred way to mark an HTML element is a data attribute. In contrast to element types, class or id attributes, data attributes do not come with any predefined meaning. Data attributes never clash with each other.

For the purpose of this guide, we use the data-testid attribute. For example, we mark the increment button in the IndependentCounterComponent with data-testid="increment-button":

<button (click)="increment()" data-testid="increment-button">+</button>

In the test, we use the corresponding attribute selector:

const incrementButton = debugElement.query(
  By.css('[data-testid="increment-button"]')
);

There is a nuanced discussion around the best way to find elements during testing. Certainly, there are several valid and elaborate approaches. This guide will only present one possible approach that is simple and approachable.

The Angular testing tools are neutral when it comes to DOM querying: They allow different approaches and do not recommend a specific solution. After consideration, you should opt for way, document it as a testing convention and apply it consistently across all tests.

Triggering event handlers

Now that we have marked and got hold of the increment button, we need to click on it.

It is a common task in tests to simulate user input like clicking, typing in text, moving pointers and pressing keys. From an Angular perspective, user input causes DOM events. The Component template registers event handlers using the schema (event)="handler($event)". In the test, we need to simulate an event to call these handlers.

DebugElement has a useful method for firing events: triggerEventHandler. This method calls all event handlers for a given event type (like click). As a second parameter, it expects a fake event object that is passed to the handlers:

incrementButton.triggerEventHandler('click', {
  /* … Event properties … */
});

This example fires a click event on the increment button. Since the template contains (click)="increment()", the increment method of IndependentCounterComponent will be called.

The increment method does not access the event object. The call is simply increment(), not increment($event). Therefore, we do not need to pass a fake event object, we can simply pass null:

incrementButton.triggerEventHandler('click', null);

It is worth noting that triggerEventHandler does not dispatch a synthetic DOM event. The effect stays on the DebugElement abstraction level and does not touch the native DOM. This is fine as long as the event handler is registered on the element itself. If the event handler is registered on a parent and relies on event bubbling, you need to call triggerEventHandler directly on that parent. triggerEventHandler does not simulate event bubbling or any other effect a real event might have.

Expecting text output

We have completed the Act phase in which the test clicks on the increment button. In the Assert phase, we need to expect that the displayed count changes from “0” to “1”.

In the template, the count is rendered into a strong element:

<strong>8</strong>

In our test, we need to find this element and read its text content. For this purpose, we add a test id:

<strong data-testid="count">8</strong>

We can now find the element easily:

const countOutput = debugElement.query(
  By.css('[data-testid="count"]')
);

The next step is to read the element’s content. In the DOM, the count is a text node that is a child of strong. Unfortunately, the DebugElement does not have a method or property for reading the text content. We need to access the native DOM element which has a convenient textContent property.

countOutput.nativeElement.textContent

Finally, we expect that this string is "1" using Jasmine’s expect():

expect(countOutput.nativeElement.textContent).toBe("1");

The complete independent-counter.component.spec.ts now looks like this:

/* Incomplete! */
describe('IndependentCounterComponent', () => {
  let fixture: ComponentFixture<IndependentCounterComponent>;
  let debugElement: DebugElement;

  // Arrange
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [IndependentCounterComponent],
    }).compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(IndependentCounterComponent);
    fixture.detectChanges();
    debugElement = fixture.debugElement;
  });

  it('increments the count', () => {
    // Act
    const incrementButton = debugElement.query(
      By.css('[data-testid="increment-button"]')
    );
    incrementButton.triggerEventHandler('click', null);

    // Assert
    const countOutput = debugElement.query(
      By.css('[data-testid="count"]')
    );
    expect(countOutput.nativeElement.textContent).toBe("1");
  });
});

When we run that suite, the spec fails:

IndependentCounterComponent increments the count FAILED
  Error: Expected '0' to be '1'.

What is wrong here? Is the implementation faulty? No, the test just missed something important. We have mentioned that in the testing environment, Angular does not automatically detect changes and does not update the DOM. Clicking the increment button changes the count property of the Component instance. To update the template binding {{ count }}, we need to trigger the change detection manually.

fixture.detectChanges();

The full test suite now looks like this:

describe('IndependentCounterComponent', () => {
  let fixture: ComponentFixture<IndependentCounterComponent>;
  let debugElement: DebugElement;

  // Arrange
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [IndependentCounterComponent],
    }).compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(IndependentCounterComponent);
    fixture.detectChanges();
    debugElement = fixture.debugElement;
  });

  it('increments the count', () => {
    // Act
    const incrementButton = debugElement.query(
      By.css('[data-testid="increment-button"]')
    );
    incrementButton.triggerEventHandler('click', null);
    // Re-render the Component
    fixture.detectChanges();

    // Assert
    const countOutput = debugElement.query(
      By.css('[data-testid="count"]')
    );
    expect(countOutput.nativeElement.textContent).toBe("1");
  });
});

Congratulations! We have written our first Component test. It is not complete yet, but it already features a typical workflow. We will make small improvements to the existing code with each spec we add.

Testing helpers

The next IndependentCounterComponent feature we need to test is the decrement button. It is very similar to the increment button, so the spec looks almost the same.

First, we add a test id to the decrement button:

<button (click)="decrement()" data-testid="decrement-button">-</button>

Then we write the spec:

it('increments the count', () => {
  // Act
  const decrementButton = debugElement.query(
    By.css('[data-testid="decrement-button"]')
  );
  decrementButton.triggerEventHandler('click', null);
  // Re-render the Component
  fixture.detectChanges();

  // Assert
  const countOutput = debugElement.query(
    By.css('[data-testid="count"]')
  );
  expect(countOutput.nativeElement.textContent).toBe('-1');
});

There is nothing new here, only the test id, the variable names and the expected output changed.

Now we have two specs that are almost identical. The code is repetitive and the signal-to-noise ratio is low, meaning there is much code that does little. Let us identify the patterns repeated here:

  1. Finding an element by test id
  2. Clicking on an element (found by test id)
  3. Expecting a given text content on an element (found by test id)

These tasks are highly generic and they will appear in almost every Component spec. It is worth writing testing helpers for them.

A testing helper is a piece of code that makes writing tests easier. It makes test code more concise and more meaningful. Since a spec should describe the implementation, a readable spec is better than an obscure, convoluted one.

Your testing helpers should cast your testing conventions into code. They not only improve the individual test, but make sure all tests use the same patterns and work the same.

A testing helper can be a simple function, but it can also be an abstraction class or a Jasmine extension. For the start, we extract common tasks into plain functions.

First, let us write a helper for finding an element by test id. We have used this pattern multiple times:

const xyzElement = fixture.debugElement.query(
  By.css('[data-testid="xyz"]')
);

We move this code into a reusable function:

function findEl<T>(
  fixture: ComponentFixture<T>,
  testId: string
): DebugElement {
  return fixture.debugElement.query(
    By.css(`[data-testid="${testId}"]`)
  );
}

This function is self-contained. The Component fixture needs to be passed in explictly. Since ComponentFixture<T> requires a type parameter – the wrapped Component type –, findEl also has a type parameter called T. TypeScript will infer the Component type automatically when you pass a ComponentFixture.

Second, we write a testing helper that clicks on an element with a given test id. This helper can build on findEl.

export function click<T>(
  fixture: ComponentFixture<T>,
  testId: string,
  event: Partial<MouseEvent>
): void {
  const element = findEl(fixture, testId);
  element.triggerEventHandler('click', event);
}

This is the simplest possible implementation. We will expand the helper when required.

The click helper can be used on every element that has a (click)="…" event handler. For accessibility, make sure the element can be focussed and activated. This is the case for buttons or links.

Historically, the click event was specific to mouse input. Today, it is still triggered by a mouse click, but it transformed into a generic “activate” event. It is also triggered by a “tap” (touch input), keyboard input or voice input. So in your Component, you do not need to listen for touch or keyboard events separately. In the test, a generic click event usually suffices.

Third, we write a testing helper that expects a given text content on an element with a given test id.

export function expectText<T>(
  fixture: ComponentFixture<T>,
  testId: string,
  text: string,
): void {
  const element = findEl(fixture, testId);
  const actualText = element.nativeElement.textContent;
  expect(actualText).toBe(text);
}

Again, this is a simple implementation that will be improved later.

Using these helpers, we are going to rewrite our spec:

it('increments the count', () => {
  // Act
  click(fixture, 'decrement-button', null);
  // Re-render the Component
  fixture.detectChanges();

  // Assert
  expectText(fixture, 'count', '-1');
});

That is much better to read and less to write! You can tell what the spec is doing at first glance.

Filling out forms

We have tested the increment and decrement button successfully. The remaining user-facing feature we need to test is the reset feature.

In the user interface, there is a reset input field and a reset button. The user enters a new number into to field, then clicks on the reset button. The Component should reset the count to the user-provided number.

We already know how to click a button, but how do we fill out a form field? Unfortunately, Angular’s testing tools do not provide a solution for filling out forms easily.

The answer depends on the field type and value. The generic answer is: Find the native DOM element and set the value property to the new value.

const resetInput = debugElement.query(
  By.css('[data-testid="reset-input"]')
);
resetInput.nativeElement.value = '123';

With our testing helper:

const resetInputEl = findEl(fixture, 'reset-input').nativeElement;
resetInputEl.value = '123';

This fills in the value programmatically.

In IndependentCounterComponent’s template, the reset input has a template reference variable, #resetInput:

<input type="number" #resetInput data-testid="reset-input" />
<button (click)="reset(resetInput.value)" data-testid="reset-button">
  Reset
</button>

The click handler uses resetInput to access the input element, reads the value and passes it to the reset method.

The example already works because the form is very simple. Setting a field’s value is not a full simulation of user input and will not work with Template-driven or Reactive Forms yet.

Angular forms cannot observe value changes directly. Instead Angular listens for an input event that the browser fires when a field value changes. For compatibility with Template-driven and Reactive Forms, we need to dispatch a fake input event.

DOM elements have a dispatchEvent method for this purpose. In newer browsers, we can create a fake (so-called synthetic) input event with new Event('input').

const resetInputEl = findEl(fixture, 'reset-input').nativeElement;
resetInputEl.value = '123';
resetInputEl.dispatchEvent(new Event('input'));

If you need to run your tests in legac Internet Explorer, a bit more code is necessary. Internet Explorer does not support new Event('…'), but the document.createEvent method:

const event = document.createEvent('Event');
event.initEvent('input', true, false);
resetInputEl.dispatchEvent(event);

The full spec for the reset feature then looks like this:

it('resets the count', () => {
  const newCount = '123';

  // Act
  const resetInputEl = findEl(fixture, 'reset-input').nativeElement;
  // Set field value
  resetInputEl.value = newCount;
  // Dispatch input event
  const event = document.createEvent('Event');
  event.initEvent('input', true, false);
  resetInputEl.dispatchEvent(event);

  // Click on reset button
  click(fixture, 'reset-button');
  // Re-render the Component
  fixture.detectChanges();

  // Assert
  expectText(fixture, 'count', newCount);
});

Filling out forms is a common task in tests, so it makes sense to extract the code and put it into a helper. The helper function takes a test id and a string value. It finds the corresponding element, sets the value and dispatches an input event.

export function setFieldValue<T>(
  fixture: ComponentFixture<T>,
  testId: string,
  value: string,
): void {
  const element = findEl(fixture, testId).nativeElement;
  element.value = value;
  // Dispatch a fake input event so Angular form bindings
  // take notice of the change.
  const event = document.createEvent('Event');
  event.initEvent('input', true, false);
  element.dispatchEvent(event);
}

Using the newly created setFieldValue helper, we can simplify the spec:

it('resets the count', () => {
  const newCount = '123';

  // Act
  setFieldValue(fixture, 'reset-input', newCount);
  click(fixture, 'reset-button');
  fixture.detectChanges();

  // Assert
  expectText(fixture, 'count', newCount);
});

That is it! While the reset feature is simple, this is how to test most form logic.

Testing Inputs

IndependentCounterComponent has an Input startCount that sets the initial count. We need to test that the counter reacts to the Input properly. For example, if we set startCount to 123, the rendered count should be 123 as well. If the Input is empty, the rendered count should be 0, the default value.

Setting an Input during testing is rather easy: An Input is a special property of the Component instance. We can set this property in the Arrange phase.

const component = fixture.componentInstance;
component.startCount = 10;

It is a good practice not to change an Input value within a Component. An Input property should always reflect the data passed in by the parent Component. That is why IndependentCounterComponent has a public Input named startCount as well as an internal property named count. When the user clicks the increment or decrement buttons, count is changed, but startCount remains unchanged.

Whenever the startCount Input changes, count needs to be set to startCount. The safe place to do that is the ngOnChanges lifecycle function:

public ngOnChanges(): void {
  this.count = this.startCount;
}

ngOnChanges is called whenever a “data-bound property” changes, including Inputs and Outputs.

Let us write a test for the startCount Input. We set the Input in the beforeEach block that creates the component, before calling detectChanges. The spec itself checks that the correct count is rendered.

/* Incomplete! */
beforeEach(() => {
  fixture = TestBed.createComponent(IndependentCounterComponent);
  component = fixture.componentInstance;
  // Set the Input
  component.startCount = startCount;
  fixture.detectChanges();
});

it('shows the start count', () => {
  expectText(fixture, 'count', String(count));
});

When we run this spec, we find that it fails:

IndependentCounterComponent > shows the start count
  Expected '0' to be '123'.

What is wrong here? Did we forget to call detectChanges again? No, but we forgot to call ngOnChanges! In the testing environment, ngOnChanges is not called automatically. We have to call it manually after setting the Input.

Here is the full corrected example:

describe('IndependentCounterComponent', () => {
  let component: IndependentCounterComponent;
  let fixture: ComponentFixture<IndependentCounterComponent>;

  const startCount = 123;

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

  beforeEach(() => {
    fixture = TestBed.createComponent(IndependentCounterComponent);
    component = fixture.componentInstance;
    component.startCount = startCount;
    // Call ngOnChanges, then re-render
    component.ngOnChanges();
    fixture.detectChanges();
  });

  /* … */

  it('shows the start count', () => {
    expectText(fixture, 'count', String(startCount));
  });
});

The IndependentCounterComponent expects a number Input and renders it into the DOM. When reading text from the DOM, we always deal with strings. What is why we pass in a number 123 but expect to find the string '123'.

In addition to primitive values, Components usually expect complex objects, arrays or even Observables. Sometimes they process the Input values before displaying them. Testing this behavior takes more effort. The test needs to define meaningful fake data first. Setting the Input properties is straightforward. The Assert phase get more complex because the Input values are not directly rendered.

TODO

Testing Outputs

While Inputs pass data from parent to child, Outputs allow to send data from child to parent. In combination, a Component can perform a specific operation just with the required data. For example, a Component may render a form so the user can edit or review the data. Once completed, the Component emits the data as an Output.

Outputs are not a user-facing feature, but a vital part of the public Component API. Technically, Output are a Component instance properties. A unit test must inspect the Outputs thoroughly to proof that the Component plays well with other Components.

The IndependentCounterComponent has an output named countChange. Whenever the count changes, the countChange Output emits the new value.

export class IndependentCounterComponent implements OnChanges {
  /* … */
  @Output()
  public countChange = new EventEmitter<number>();
  /* … */
}

The Output’s type EventEmitter is a subclass of RxJS Subject, which is itself extends RxJS Observable. The Component uses the emit method to publish new values. The parent Component uses the subscribe method to listen for emitted values. In the testing environment, we will do the same.

Let us write a spec for the countChange Output!

it('emits countChange events', () => {
    /* … */
});

Within the spec, we access the Output via fixture.componentInstance.countChange. In the Arrange. phase, we subscribe to the EventEmitter.

it('emits countChange events on increment', () => {
  // Arrange
  component.countChange.subscribe((count) => {
    /* … */
  });
});

We need to verify that the observer function is called with the right value when the increment button is clicked. In the Act phase, we click on the button using our helper function:

it('emits countChange events on increment', () => {
  // Arrange
  component.countChange.subscribe((count) => {
    /* … */
  });

  // Act
  click(fixture, 'increment-button', null);
});

In the Assert phase, we expect that count has the correct value. The easiest way is to declare a variable in the spec scope. Let us name it actualCount. Initially, it is undefined. The observer function sets a value – or not, if it is never called.

it('emits countChange events on increment', () => {
  // Arrange
  let actualCount: number | undefined;
  component.countChange.subscribe((count: number) => {
    actualCount = count;
  });

  // Act
  click(fixture, 'increment-button', null);

  // Assert
  expect(actualCount).toBe(1);
});

The click on the button emits the count and calls the observer function synchronously. That is why the next line of code can expect that actualCount has been changed.

You might wonder why we did not put the expect call in the observer function:

/* Not recommended! */
it('emits countChange events on increment', () => {
  // Arrange
  component.countChange.subscribe((count: number) => {
    // Assert
    expect(count).toBe(1);
  });

  // Act
  click(fixture, 'increment-button', null);
});

This works as well, but it is might produce false positives. If the feature under test is broken and the Output does not emit, expect is never called. Per default, Jasmine warns you that the spec has no expectations but treats the spec as successful. We want the spec to fail explicitly in this case, so we make sure the expectation is always run.

Now we have verified that countChange emits when the increment button is clicked. We also need to proof that the Output emits when using the decrement or reset features. We could do that by copying the code and adding two more specs:

it('emits countChange events on decrement', () => {
  // Arrange
  let actualCount: number | undefined;
  component.countChange.subscribe((count: number) => {
    actualCount = count;
  });

  // Act
  click(fixture, 'decrement-button', null);

  // Assert
  expect(actualCount).toBe(-1);
});

it('emits countChange events on reset', () => {
  const newCount = '123';

  // Arrange
  let actualCount: number | undefined;
  component.countChange.subscribe((count: number) => {
    actualCount = count;
  });

  // Act
  setFieldValue(fixture, 'reset-input', newCount);
  click(fixture, 'reset-button');

  // Assert
  expect(actualCount).toBe(newCount);
});

TODO: section

This works fine, but the spec code is highly repetitive. Experts disagree on whether this is a problem.

On the one hand, abstractions like helper functions make tests more complex and therefore harder to understand. After all, tests should be more readable than the implementation code.

On the other hand, it is hard to grasp the essence of repetitive specs. Testing helpers form a high-level language to express testing instructions clear and brief. For example, if all your specs find DOM elements via test ids, they should hide that detail behind a testing helper.

There is a controversial debate in software development around “do not repeat yourself” and the value of abstractions. Sandi Metz famously stated “duplication is far cheaper than the wrong abstraction”.

This is especially true when writing specs. You should try to eliminate duplication and boilerplate code with beforeEach/beforeAll, simple helper functions and even testing libraries. But do not try to apply your optimization habits and skills to test code. A test is supposed to reproduce all relevant logical cases. Finding an abstraction for all these diverse, sometimes mutually exclusive cases is often futile.

Your mileage may vary on this question. For completeness, let us discuss how we could reduce the repetition in the code above.

As Output is an EventEmitter, which is a fully-functional RxJS Observable. This allows us to transform the Observable as we please. Specifically, we can click all three buttons and then expect that the countChange Output has emitted three values.

it('emits countChange events', () => {
  // Arrange
  const newCount = 123;

  // Capture all emitted values in an array
  let actualCounts: number[] | undefined;

  // Transform the Observable, then subscribe
  component.countChange.pipe(
    // Close the Observable after three values
    take(3),
    // Collect all values in an array
    toArray()
  ).subscribe((counts) => {
    actualCounts = counts;
  });

  // Act
  click(fixture, 'increment-button', null);
  click(fixture, 'decrement-button', null);
  setFieldValue(fixture, 'reset-input', String(newCount));
  click(fixture, 'reset-button', null);

  // Assert
  expect(actualCounts).toEqual([1, 0, newCount]);
});

This example requires some RxJS knowledge. We are going to encounter RxJS Observables again and again when testing Angular applications. If you do not understand the example above, that is totally fine. It is just optional way to merge three specs into one.

Black vs. white box testing an Angular Component

Component tests turn out to be most meaningful if they closely mimic how the user interacts with the Component. The tests we have written try to apply this principle. We have worked directly with the DOM to read text, click on buttons and fill out form fields because that is what the user does.

These tests are black box tests. We have already talked about black box vs. white box testing in theory. Both are valid testing methods. As stated, this guide advises to use black box testing first and foremost.

A common technique to enforce black box testing is to mark internal methods as private so they cannot be called in the test. The test should only inspect the documented, public API.

In Angular Components, the difference between external and internal properties and methods does not coincide with their TypeScript visibility (public vs. private). Properties and methods need to be public so that the template is able to access them. This makes sense for Input and Output properties. They need to be read and written from the outside, from your test. However, internal properties and methods exist that are public only for the template.

For example, the IndependentCounterComponent has an Input startCount and an Output countChange. Both are public:

@Input()
public startCount = 0;

@Output()
public countChange = new EventEmitter<number>();

They form the public API. However, there are several more properties and methods that are public:

public count = 0;
public increment(): void { /* … */ }
public decrement(): void { /* … */ }
public reset(newCount: string): void { /* … */ }

These properties and methods are internal, they are used only within the Component. Yet they need to be public so the template may access them. Angular compiles templates into TypeScript code, and TypeScript ensures that the template code only accesses public properties and methods.

Many Angular testing tutorials conduct Component white box tests. How does such a test look like? In our IndependentCounterComponent black box test, we increment the count by simulating a click on the “+” button. A white box test would call the increment method directly:

/* Not recommended! */
describe('IndependentCounterComponent', () => {
  /* … */
  it('increments the count', () => {
    component.increment();
    fixture.detectChanged();
    expectText(fixture, 'count', '1');
  });
});

This white box test reaches into the Component to access an internal, yet public method. This is not wrong and sometimes valuable, but it is mostly misused. As we have learned, a Component test is meaningful if it interacts with the Component using Inputs, Outputs and the rendered DOM. Calling internal methods or accessing internal properties runs the risk of failing to cover important behavior like template logic and event handling.

The spec above deals with the increment feature. It calls the increment method, but does not test the corresponding template code, the increment button:

<button (click)="increment()" data-testid="increment-button">+</button>

If we remove the increment button from the template entirely, the feature is obviously broken. But the white box test does not fail, it produces a false positive.

When applied to Angular Components, black box testing is more intuitive and easier for beginners. When writing a black box test, ask what the Component does for the user and for the parent Components. Then imitate the real usage in your test.

A white box test that does not examine the Component from the DOM perspective runs the risk of missing crucial Component behavior. It gives the illusion that all code is tested.

That being said, white box testing is viable advanced technique. If you are a testing expert, you can write efficient Component specs this way that still test out all features and cover all code.

The following table shows which properties and methods of an Angular Component you should access or not in a black box test.

Black box testing an Angular component
Class member Access from test
@Input properties Yes (write)
@Output properties Yes (subscribe)
Lifecycle methods Avoid except for ngOnChanges
Other public methods Avoid
Private properties
and methods
No access



Synthetisches Event-Objekt

debugElement.triggerEventHandler("click", null);
debugElement.triggerEventHandler("click", {
  preventDefault() {},
  stopPropagation() {},
  target: debugElement.nativeElement,
  currentTarget: debugElement.nativeElement,
  pageX: 100,
  pageY: 200,
});

Interaktivität testen

  1. Interaktives Element heraussuchen (Button)
  2. Ereignis simulieren
  3. Manuell neu rendern: fixture.detectChanges()
  4. Ausgabe prüfen

Counter-Test

  • ✅ it('renders the initial count', …)
  • ✅ it('increments', …)

Counter-Funktionalität erweitern

  • Decrement
  • Reset: Eingabefeld und Reset-Button

Counter-Reset testen

  1. Eingabefeld heraussuchen und value setzen
  2. Klick auf Reset-Button simulieren
  3. detectChanges()
  4. Ausgabe prüfen

Helferlein zum Testen von Komponenten

  • findEl(fixture, testId): DebugElement
  • findEls(fixture, testId): DebugElement[]
  • getText(fixture, testId): string
  • expectText(fixture, testId, text)
  • setFieldValue(fixture, testId, text)
  • click(fixture, testId)

Input und Outputs

  • Counter bekommt einen Input: startCount
  • Counter bekommt einen Output: countChange
<app-counter [startCount]="5" (countChange)="logCount($event)"></app-counter>

Inputs im Test setzen

Inputs sind Eigenschaften der Komponenteninstanz

component.startCount = startCount;
component.ngOnChanges();
fixture.detectChanges();

Output testen (1)

  • Outputs sind EventEmitter
  • EventEmitter sind Observables
  • component.countChange.subscribe()

Output testen (2)

let actualValue: number | undefined;
component.countChange.subscribe((value: number) => {
  actualValue = value;
});

click(fixture, "increment-button");
expect(actualValue).toBe(1);

Einfache Komponenten: ✅ getestet

  • Ausgabe testen
  • User-Eingaben simulieren
  • Inputs & Outputs
  • Verwendet Helferlein!

Komponente als Black Box testen

  • Nur ins DOM schauen und Ereignisse auslösen
  • Nur über Inputs und Outputs mit der Komponente sprechen
  • Keine Methoden aufrufen
  • Nicht auf Eigenschaften zugreifen
    (auch nicht wenn sie öffentlich sind)

Tests debuggen

  • Chrome benutzen, Developer Tools öffnen
  • Test-Focus setzen mit fdescribe() und fit()
  • console.log(…) ist Gold wert
  • Debug-Ausgaben in Lifecycle-Methoden, Handlern und im Template

Komplexe Komponenten

  • Eigenständig vs. verbunden
  • »Smart« vs. »dumb«
  • Abhängigkeiten:
    • Verschachtelte Komponenten
    • Services
    • NgRx Store

Verschachtelte Komponenten testen

  • Unit Test – Shallow Rendering –
    Kinder nicht rendern
  • Integration Test – Deep Rendering –
    Kinder mitrendern

Deep Rendering

  • Beispiel: app.component.html referenziert <app-counter>
  • Standardmäßig werden die Kinder mitgerendert
  • Alle Komponenten müssen im Testmodul deklariert werden

Shallow Rendering (1)

  • Beispiel AppComponent
  • Kinder nicht mitrendern:
    schemas: [ NO_ERRORS_SCHEMA ]
  • Wrapper-Elemente bleiben leer, Kindkomponenten werden nicht instantiiert
  • <app-counter …></app-counter>

Shallow Rendering (2):
Was testen?

  1. Kindkomponente wird gerendert
    (Wrapper app-counter ist vorhanden)
  2. Input-Daten werden korrekt übergeben
  3. Auf Events (Outputs) wird korrekt reagiert

Shallow Rendering (3):
Kindkomponente vorhanden?

const el = fixture.debugElement.query(By.css("app-counter"));
expect(el).toBeTruthy();

Shallow Rendering (4):
Kindkomponente vorhanden?

// Helferlein: Wirft einen Fehler, wenn nichts gefunden
const el = findComponent(fixture, "app-counter");
expect(el).toBeTruthy();
// Oder
expect().nothing();

Shallow Rendering (5):
Input testen

DebugElement hat eine Eigenschaft properties

<app-counter [startCount]="5"></app-counter>
<app-counter [startCount]="5"></app-counter>
const el = findComponent(fixture, "app-counter");
expect(el.properties.startCount).toBe(5);

Shallow Rendering (6):
Output testen

  • Outputs sind aus Sicht der Elternkomponente Ereignisse
  • Simulieren mit dem bekannten triggerEventHandler

Shallow Rendering (7):
Output testen

  • (countChange)="handleCountChange($event)"
  • triggerEventHandler('countChange', 5)
  • Auswirkung prüfen (z.B. mit Jasmine Spies)

Komponente mit Service-Abhängigkeit (1)

Beispiel ServiceCounterComponent

class ServiceCounterComponent {
  constructor(private counterService: CounterService) {
    this.count$ = this.counterService.getCount();
  }
}

Komponente mit Service-Abhängigkeit (2)

  • Unit Test – Service wird gemockt
  • Integration Test – Service wird mitgetestet

Service-Abhängigkeit mocken

  • Verschiedene Mocking-Strategien 🤷‍♀️
  • Testing with the real service
  • Mocking with fake classes
  • Mocking by overriding functions
  • Mock by using a real instance with Spy

Anforderungen an Mocks

  • Original darf nie aufgerufen werden (Nebenwirkungen!)
  • ⇒ Es darf nicht möglich sein, das Überschreiben einer Methode zu vergessen
  • Mock und Original müssen auf dem gleichen Stand sein
  • ⇒ Mock muss eine Typableitung des Originals sein

Anforderungen erfüllt?

  • ⛈ Testing with the real service
  • ⛅️ Mocking with fake classes
  • 🌧 Mocking by overriding functions
  • 🌧 Mock by using a real instance with Spy

🙍‍♀️🤦‍♀️

Service-Abhängigkeit mocken

  • 👩‍💻 Basis: Mocking with fake classes
  • 👩‍🔬 Entweder eine Klasse oder Instanz
  • 👩‍🔧 Typableitung hinzufügen
  • 💆‍♀️ 🌈 ☀️

Typ vorbereiten

Einzelne Methoden

type PartialCounterService = Pick<
  CounterService,
  "getCount" | "increment" | "decrement" | "reset"
>;

Typ vorbereiten (Alternative)

Alle öffentlichen Methoden

type PartialCounterService = Pick<CounterService, keyof CounterService>;

☀️ Mock-Service als Klasse

class MockCounterService implements PartialCounterService {
  getCount() {
    return of(count);
  }
  increment() {}
  decrement() {}
  reset() {}
}

☀️ Mock-Service als Objekt

const mockCounterService: PartialCounterService = {
  getCount() {
    return of(count);
  },
  increment() {},
  decrement() {},
  reset() {},
};

Mock anstelle des Originals verwenden

Im Testing Module:

useClass

providers: [{ provide: CounterService, useClass: MockCounterService }];

useValue

providers: [{ provide: CounterService, useValue: mockCounterService }];

Interaktion mit dem Mock testen

  • Mock liefert feste Rückgabewerte
  • Mock erwartet gewisse Parameter
  • Parameter-Übergabe testen mit Jasmine Spies

Jasmine Spy

  • Funktion, die alle Aufrufe aufzeichnet
  • Später ist Prüfung möglich
  • Wurde der Spy aufgerufen? Wie oft?
  • Wurde der Spy mit gewissen Parametern aufgerufen?

Unabhängigen Spy erzeugen

const spy = jasmine.createSpy('name');
const spy = jasmine.createSpy('name').and.returnValue();
const spy = jasmine.createSpy('name').and.callFake(() => {});

Spy wrappt eine vorhandene Methode

spyOn(object, "method");
spyOn(object, "method").and.callThrough();
spyOn(object, "method").and.returnValue(value);

Spies verifizieren

expect(spy).toHaveBeenCalled();
expect(spy).not.toHaveBeenCalled();
expect(spy).toHaveBeenCalledTimes(5);
expect(spy).toHaveBeenCalledWith(param1 /* … */);
expect(object.method).toHaveBeenCalled();
expect(object.method).toHaveBeenCalledWith(param1 /* … */);

Spies am Mock-Service installieren

spyOn(mockCounterService, "getCount").and.callThrough();
spyOn(mockCounterService, "increment");
spyOn(mockCounterService, "decrement");
spyOn(mockCounterService, "reset");

Mock-Service als Spy-Objekt

jasmine.createSpyObj()

const mockCounterService = jasmine.createSpyObj<CounterService>(
  'CounterService',
  {
    getCount: of(count),
    increment: undefined,
    decrement: undefined,
    reset: undefined
  }
]);

Spies verifizieren

expect(mockCounterService.getCount).toHaveBeenCalled();
expect(mockCounterService.increment).toHaveBeenCalled();
expect(mockCounterService.decrement).toHaveBeenCalled();
expect(mockCounterService.reset).toHaveBeenCalledWith(newCount);

Service-Mocking: Fazit

  • Mocking ist aufwändig und erfordert Übung
  • Services lassen sich gut mocken …
  • … wenn das Interface übersichtlich und die Funktionalität klar sind
  • Sinnvolle Testdaten (Stubs) sind nötig

Komponente mit NgRx-Store-Abhängigkeit

NgRxCounterComponent

class NgRxCounterComponent {
  constructor(private store: Store<AppState>) {
    this.count$ = store.pipe(select("counter"));
  }
}

Mock-Store bereitstellen (1)

Offizielle Methode

provideMockStore({ initialState: {}, selectors: {} })

Mock-Store bereitstellen (2)

TestBed.configureTestingModule({
  declarations: [NgRxCounterComponent],
  providers: [provideMockStore({ initialState: mockState })],
}).compileComponents();

Mock-State erzeugen

const mockState: Partial<AppState> = {
  counter: 1,
};

Store-Abhängigkeit: Was testen?

  • Komponente zieht sich Daten aus dem Store
  • Komponente transformiert ggf. diese Daten
  • Komponente rendert diese Daten
  • Komponente dispatcht Actions

Dispatch von Actions testen (1)

In beforeEach einen Spy auf MockStore#dispatch installieren

store = TestBed.get(Store);
spyOn(store, "dispatch");

Dispatch von Actions testen (2)

it("resets the count", () => {
  const newCount = 15;
  findEl(fixture, "reset-input").nativeElement.value = newCount;
  click(fixture, "reset-button");
  expect(store.dispatch).toHaveBeenCalledWith(reset({ count: newCount }));
});

Verschiedene States testen

  • Wenn der State komplex sein kann, müssen alle Fälle getestet werden
  • Pro Spec ein anderer State
  • Flexible setup-Funktion statt fester beforeEach-Logik

Setup-Funktion

function setup(mockState: Partial<AppState>) {
  TestBed.configureTestingModule({
    declarations: [NgRxCounterComponent],
    providers: [provideMockStore({ initialState: mockState })],
  }).compileComponents();

  const store: Store<AppState> = TestBed.get(Store);
  spyOn(store, "dispatch");

  const fixture = TestBed.createComponent(NgRxCounterComponent);
  fixture.detectChanges();

  return { fixture, store };
}

Verschiedene States testen

it("renders the data from the store", () => {
  const mockState: Partia<AppState> = { counter: 1 };
  const { fixture, store } = setup(mockState);
  expectText(fixture, "count", String(mockState.counter));
});

Baut Helferlein, die komplexen Mock-State generieren

Zusammenfassung Komponenten-Tests

  1. Eigenständige Komponenten:
    Ausgabe, Interaktivität, Inputs, Outputs
  2. Komponenten mit Service-Abhängigkeit:
    DI, Mocking, Spies, Mock-Daten
  3. Komponenten mit Store-Abhängigkeit:
    DI, Mock State + Store, Action-Dispatch

Weitere Teile der Anwendung

  • Services
  • Effects
  • Reducer
  • (Pipes, Directives, Resolver…)

Services – Was testen?

  • Methoden liefern Werte zurück
  • Methodenaufrufe ändern privaten State
    → Indirekt testen
  • Interaktion mit Abhängigkeiten (z.B. HttpClient)

Services testen: CounterService

  • Standard-TestBed
  • Eine Spec für jede öffentliche Methode:
    getCount, increment, decrement, reset
  • Auswirkung testen durch getCount-Aufruf

Services mit Abhängigkeit testen:
CounterApiService

TestBed.configureTestingModule({
  imports: [HttpClientTestingModule],
  providers: [CounterApiService],
});

HttpClientTestingModule (1)

HTTP-Requests finden

const httpMock: HttpTestingController = TestBed.get(HttpTestingController);

const request = httpMock.expectOne({
  method: "GET",
  url: expectedURL,
});

const predicate = (candidateRequest) => candidateRequest.method === "GET";
const request = httpMock.expectOne(predicate);
const requests = httpMock.match(predicate);

HttpClientTestingModule (2)

Gefundene Requests beantworten

request.flush(serverResponse);

Fehler simulieren

request.error(new ErrorEvent("API error"), {
  status: 404,
  statusText: "Not Found",
});

HttpClientTestingModule (3)

Verfizieren, dass alle Requests gefunden und beantwortet wurden

httpMock.verify();

Fazit: Services testen

  • Relativ einfach
  • Nichts fundamental Neues
  • Gleicher Aufwand wie beim Service-Mocking für Komponenten-Tests

NgRx Effects

  • Die Redux-Architektur lässt es offen, wie Nebenwirkungen (Side Effects) umgesetzt werden
  • Effects sind eine hervorragende Lösung
  • Alle anderen Lösungen sind m.E. komplizierter oder schwerer zu testen

Grundschema eines Effects

  • WENN eine gewisse Action eintritt
  • DANN Nebenwirkung ausführen
  • DANN Erfolgs-Action ausgeben
  • ODER Fehler-Action ausgeben

CounterEffects saveOnChange$

  • WENN Action increment eintritt
  • DANN aktuellen Count aus dem Store auslesen
  • DANN den Count an den Server senden
  • DANN saveSuccess ausgeben
  • ODER saveError ausgeben

Effects: Umsetzung

  • Ein Effect ist ein Observable
  • Bildet Actions auf Actions ab
  • Input:
    • Actions: Observable<Action>
    • ggf. Store: Observable<AppState>
  • Output: Observable<Action>

Effect schematisch

  1. Observable<Action>
  2. Filter mit ofType()
  3. State holen mit withLatestFrom(store)
  4. Nebenwirkung (Service-Call)
  5. Map auf Success-Action
  6. catchError mit Error-Action

Effects aus Sicht von Angular

  • CounterEffects ist eine Klasse mit Eigenschaften vom Typ Observable<Action>
  • Die Klasse nimmt an der Dependency Injection Teil (@Injectable)
  • Abhängigkeiten sind deklariert

Effects: Abhängigkeiten

public constructor(
  private actions$: Actions,
  private store: Store<AppState>,
  private counterApiService: CounterApiService
) {}

Effects: Abhängigkeiten mocken

TestBed.configureTestingModule({
  providers: [
    { provide: Actions, useValue: ??? },
    { provide: Store, useValue: ??? },
    { provide: CounterApiService, useValue: ??? },
    CounterEffects
  ]
});

Effects testen

  1. Input-Observable mit Actions bereitstellen (actions$)
  2. ggf. Mock-Store bereitstellen (store)
  3. Service-Mock bereitstellen (counterApiService)
  4. Output-Observable abonnieren, Werte prüfen
  5. Service-Mock verifizieren

Input-Observable mit Actions bereitstellen

import { from, of } from "rxjs";
import { provideMockActions } from "@ngrx/effects/testing";

const action = reset({ count: 123 });
provideMockActions(of(action));

const actions = [reset({ count: 123 })];
provideMockActions(from(actions));

Mock-State erzeugen

const mockState: Partial<AppState> = {
  counter: 1,
};

Mock-Store bereitstellen

  • provideMockStore()
  • Es geht noch einfacher, wenn dispatch und select nicht aufgerufen werden
  • Der Store ist ein Observable
  • { provide: Store, useValue: of(mockState) }

Service-Mock bereitstellen

Mock für CounterApiService

type PartialCounterApiService = Pick<CounterApiService, "saveCounter">;

const mockCounterApi: PartialCounterApiService = {
  saveCounter() {
    return of({});
  },
};

spyOn(mockCounterApi, "saveCounter").and.callThrough();

Mock für CounterApiService (Alternative)

const mockCounterApi = jasmine.createSpyObj<CounterApiService>(
  "CounterApiService",
  ["saveCounter"]
);
mockCounterApi.saveCounter.and.returnValue(of({}));

Output-Observable prüfen

counterEffects.saveOnChange$.subscribe((outputAction) => {
  expect(outputAction).toEqual(saveSuccess());
});

counterEffects.saveOnChange$.pipe(toArray()).subscribe((outputActions) => {
  expect(outputActions).toEqual([saveSuccess()]);
});

Output-Observable prüfen: Helferlein

function expectActions(effect: Observable<Action>, actions: Action[]) {
  effect.pipe(toArray()).subscribe((actualActions) => {
    expect(actualActions).toEqual(actions);
  }, fail);
}

expectActions(counterEffects.saveOnChange$, [saveSuccess()]);

Service-Mock verifizieren

expect(mockCounterApi.saveCounter).toHaveBeenCalledWith(mockState.counter);

Fehlerfall testen

Zweiter Service-Mock, der einen Fehler wirft

const mockCounterApiError: PartialCounterApiService = {
  saveCounter() {
    return throwError(apiError);
  },
};

Eine Error-Action erwarten

expectActions(counterEffects.saveOnChange$, [saveError({ error: apiError })]);

Komplexe Effects testen

  • Die meisten Effects haben eine einfache RxJS-Logik
  • Input Actions + Store + Nebenwirkung
    → Output Action(s)
  • Komplexen Effect in mehrere einfache zerlegen
  • Marble Testing

Effects testen: Zusammenfassung

  • Setup erfordert tieferes Verständnis von RxJS und NgRx
  • Mocking: Input-Actions, Store, Service(s)
  • Helferlein sinnvoll
  • Dann relativ wenig Arbeit

Reducer testen (1)

  • Reducer sind Pure Functions
  • Daher einfach zu testen
  • Werte rein, Werte raus
  • Nur Stubs, keine Mocks

Reducer testen (2)

function partReducer(state: StatePart, action: Action): StatePart {}

const state: StatePart = {
  /* … */
};
const action = someAction();
const newState: StatePart = {
  /* … */
};
expect(partReducer(state, action)).toEqual(newState);

Reducer testen (3)

  • Beispiel counterReducer
  • Initialisierung: state = initialState
  • Default-Fall (return state)
  • State-Änderung bei den relevanten Actions

Immutability in Reducern

  • Reducer dürfen den State nicht ändern
  • Müssen einen neues Objekt (Kopie) erzeugen
  • { ...state, property: newValue }

Immutability-Helferlein

Code Coverage

100% Code Coverage

  • 100% Coverage ist möglich und sinnvoll
  • Bei den letzten Prozent wird es erst interessant
  • Edge Cases und schwer zu testbare Fälle

Wert der Code Coverage

  • Jede Zeile wurde mindestens einmal ausgeführt
  • Bedeutet nicht, dass alle Fälle sinnvoll getestet wurden

End-to-End Tests in Angular

End-to-End Test

  • Navigiert zu einer URL
  • Sucht Elemente heraus
  • Simuliert Maus- und Tastatur-Eingaben
  • Testet Elementinhalte

End-to-End Tests starten

  • ng e2e
  • e2e/app.e2e-spec.ts

Protractor: Browser steuern

Protractor: Einzelne Elemente finden

element(by.id(""));
element(by.name(""));
element(by.className(""));
element(by.css(""));
$("");
element(by.css('[data-testid="count"]'));
findEl("count");

Helferlein: findEl

Protractor: Viele Elemente finden

element.all(by.id(""));
element.all(by.name(""));
element.all(by.className(""));
element.all(by.css(""));
$$("");
element.all(by.css('[data-testid="count"]'));
findEls("count");

Helferlein: findEls

Protractor: Textinhalt lesen

// <h1 data-testid="count">Hello</h1>
const el = findEl("heading");
expect(heading.getText()).toBe("Hello");

Protractor: Klicks

// <button data-testid="increment-button">+</button>
findEl("increment-button").click();

Protractor: Tastatureingaben

// <input data-testid="reset-input">
findEl("reset-input").sendKeys("123");

Protractor: Page Objects

  • Page Object ist eine einfache Klasse, die eine Seite repräsentiert
  • Page Object: Low-level, Test: High-Level
  • Ziel: Prägnanz und Lesbarkeit des Tests erhöhen
  • Wenn sich das Markup ändert:
    Page Object ändern, Test nicht

Protractor: Page Objects

  • *.po.ts
  • Einfache Klasse mit Methoden, meist Element-Getter
  • Selektoren (data-testid-Namen)
  • findEl- und findEls-Aufrufe
  • Komplexere Eingabesequenzen

Counter-App

End-to-End Tests: Fallstricke

  • Alle WebDriver-Aktionen sind asynchron und geben Promises zurück
  • jasminewd ermöglicht es Tests zu schreiben, als wären die Aktionen synchron
  • E2E-Tests sehen Unit-Tests ähnlich, laufen aber fundamental anders

End-to-End Tests: Asynchronität

const el = findEl();
el.click();
expect(el.getText()).toBe('Hello');

Intern:

findEl()
  .then((el) => el.click())
  .then((el) => el.getText())
  .then((text) => expect(text).toBe('Hello');

End-to-End Tests: Fazit

  • Äußerst effektiv, um ein Feature unter realen Bedingungen zu testen
  • Hochkomplex, daher unzuverlässig und fehleranfällig
  • Konventionen nötig
  • Protractor stammt aus Angular-1-Zeiten
  • Simulierte Synchronität ist schwarze Magie

Testen und Testbarkeit

  • Testing lehrt testbaren Code zu schreiben
  • Testbarer Code ist besserer Code
  • Do one thing and do it well
  • Logik in kleine, wohldefinierte Einheiten aufbrechen
  • Einheiten einzeln und im Verbund testen