0% found this document useful (0 votes)
228 views436 pages

Testing Angular - Cleaned

This document provides an overview of testing Angular applications. It covers testing principles and different types of tests, including unit tests, integration tests, and end-to-end tests. It also discusses testing specific Angular components like services, pipes, directives, and modules. The document is intended for intermediate Angular developers and assumes knowledge of Angular concepts, JavaScript, and TypeScript. It does not focus on implementation details but rather teaches how to properly test different parts of an Angular application.
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
228 views436 pages

Testing Angular - Cleaned

This document provides an overview of testing Angular applications. It covers testing principles and different types of tests, including unit tests, integration tests, and end-to-end tests. It also discusses testing specific Angular components like services, pipes, directives, and modules. The document is intended for intermediate Angular developers and assumes knowledge of Angular concepts, JavaScript, and TypeScript. It does not focus on implementation details but rather teaches how to properly test different parts of an Angular application.
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 436

Table of Contents

1. Introduction
2. Target audience
3. Terminology
4. Testing principles
1. What makes a good test
2. What testing can achieve
3. Tailoring your testing approach
4. The right amount of testing
5. Levels of testing
1. End-to-end tests
2. Unit tests
3. Integration tests

6. Distribution of testing efforts


7. Black box vs. white box testing

5. Example applications
1. The counter Component
2. The Flickr photo search

6. Angular testing principles


1. Testability
2. Dependency injection and faking
3. Testing tools
4. Testing conventions
5. Running the unit and integration tests
6. Configuring Karma and Jasmine

7. Test suites with Jasmine


1. Creating a test suite
2. Specifications
3. Structure of a test
4. Expectations
5. Efficient test suites

8. Faking dependencies
1. Equivalence of fake and original
2. Effective faking
3. Faking functions with Jasmine spies
4. Spying on existing methods

9. Debugging tests
1. Test focus
2. Developer tools
3. Debug output and the JavaScript debugger
4. Inspect the DOM
5. Jasmine debug runner

10. Testing Components


1. Unit test for the counter Component
2. TestBed
3. Configuring the testing Module
4. Rendering the Component
5. TestBed and Jasmine
6. ComponentFixture and DebugElement
7. Writing the first Component spec
8. Querying the DOM with test ids
9. Triggering event handlers
10. Expecting text output
11. Testing helpers
12. Filling out forms
13. Testing Inputs
14. Testing Outputs
15. Repetitive Component specs
16. Black vs. white box Component testing

11. Testing Components with children


1. Shallow vs. deep rendering
2. Unit test
3. Faking a child Component
4. Faking a child Component with ng-mocks

12. Testing Components depending on Services


1. Service dependency integration test
2. Faking Service dependencies
3. Fake Service with minimal logic
4. Faking Services: Summary
13. Testing complex forms
1. Sign-up form Component
2. Form validation and errors
3. Test plan
4. Test setup
5. Successful form submission
6. Invalid form
7. Form submission failure
8. Required fields
9. Asynchronous validators
10. Dynamic field relations
11. Password type toggle
12. Testing form accessibility
1. pa11y
2. pa11y-ci
3. Start server and run pa11y-ci

13. Form accessibility: Summary

14. Testing Components with Spectator


1. Component with an Input
2. Component with children and Service dependency
3. Event handling with Spectator
4. Spectator: Summary

15. Testing Services


1. Testing a Service with internal state
2. Testing a Service that sends HTTP requests
1. Call the method under test
2. Find pending requests
3. Respond with fake data
4. Check the result of the method call
5. Verify that all requests have been answered
6. Testing the error case
7. Alternatives for finding pending requests

3. Testing Services: Summary

16. Testing Pipes


1. GreetPipe
2. GreetPipe test
3. Testing Pipes with dependencies
1. TranslateService
2. TranslatePipe
3. TranslatePipe test

17. Testing Directives


1. Testing Attribute Directives
1. ThresholdWarningDirective
2. ThresholdWarningDirective test

2. Testing Structural Directives


1. PaginateDirective
2. PaginateDirective test

18. Testing Modules


19. Measuring code coverage
1. Coverage report
2. How to use the coverage report

20. End-to-end testing


1. Strengths of end-to-end tests
2. Deployment for end-to-end tests
3. How end-to-end tests work
4. End-to-end testing frameworks
5. Introducing Protractor
6. Introducing Cypress
7. Installing Cypress
8. Writing an end-to-end test with Cypress
9. Testing the counter Component
10. Running the Cypress tests
11. Asynchronous tests
12. Automatic retries and waiting
13. Testing the counter increment
14. Finding elements
15. Interacting with elements
16. Custom Cypress commands
17. Testing the Flickr search
1. Testing the search form
2. Testing the full photo

18. Page objects


19. Faking the Flickr API
20. End-to-end testing: Summary

21. Summary
22. Index of example applications
23. References
24. Acknowledgements
25. About
26. License
Flying probes testing a printed circuit board. Photo by genkur from iStock.
Testing Angular
A Guide to Robust Angular
Applications

The Angular Framework is a mature and


comprehensive solution for 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?

This guide explains the principles of


automated testing as well as the practice of
testing Angular web applications. It
empowers you and your team to write
effective tests on a daily basis.
In this book, you learn how to set up your
own testing conventions, write your own
testing helpers and apply proven testing
libraries. The book covers different tests that
complement each other: unit tests,
integration tests and end-to-end tests.

With a strong focus on testing Angular


Components, this guide teaches you to write
realistic specs that describe and verify a
Component’s behavior. It demonstrates how
to properly mock dependencies like Services
to test a Component in isolation. The guide
introduces the Spectator and ng-mocks
libraries, two powerful testing libraries.

Apart from Components, the book illustrates


how to test the other application parts:
Services, Pipes, Directives as well as Modules.
Last but not least, it covers end-to-end tests
with Cypress.
Introduction
LEARNING OBJECTIVES

The benefits of automated software testing


Why testing is difficult to learn
The difference between implementation and testing
Establishing a steady testing practice

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 people, 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 usage
errors or system failure? Testing gives 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 is
inherently complicated and 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.
Target audience
LEARNING OBJECTIVES

Angular topics you need to know when reading this book


Where to learn about Angular core concepts
Picking chapters you are interested in

The target audience of this guide are intermediate Angular


developers. You should be familiar with Angular’s core concepts.

TESTING, NOT IMPLEMENTATION

This guide teaches you how to test Angular application parts like
Components and Services. It assumes you know how to
implement them, but not how to test them properly. If you have
questions regarding Angular’s core concepts, please refer to the
official Angular documentation.

If you have not used individual concepts yet, like Directives, that
is fine. You can simply skip the related chapters and pick
chapters you are interested in.

JAVASCRIPT & TYPESCRIPT PROFICIENCY

Furthermore, this guide is not an introduction to JavaScript or


TypeScript. It assumes you have enough JavaScript and
TypeScript knowledge to write the implementation and test code
you need. Of course, this guide will explain special idioms
commonly used for testing.

The official Angular documentation offers a comprehensive


guide on testing. It is a recommended reading, but this guide
does not assume you have read it.
Terminology
LEARNING OBJECTIVES

Terms used in this book


Notation conventions

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 these meanings by using a
different letter 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 general sense, this guide uses
lower case:

module, component, service, input, output, etc.


Testing principles
LEARNING OBJECTIVES

The goals of testing and how to achieve them


Establishing regular testing practice in your team
Classify tests by their proximity to the code
Classify tests by their knowledge of code internals

There is a gap between practical introductions – how to test a


feature – and fundamental discussions on the core concepts –
what does testing achieve, which types of tests are beneficial,
etc. Before we dive into the tutorial, we need to reflect on a few
basics about testing.

What makes a good test


When writing tests, you need to keep the goals of testing in
mind. 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 pick a few that are useful to judge
a test:

1. Testing saves time and money. Testing tries to nip software


problems in the 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 than letting 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 analyze and fix the bug in order to regain the user’s trust.

COST-EFFECTIVE

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 it prevents.

2. 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 the
original developers to understand the requirements they
have to implement. It helps fellow developers to understand
the challenges they had to deal with.

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

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

SUCCESS AND ERROR CASES

A valuable test covers the important scenarios – both


correct and incorrect input, expected cases as well as
exceptional cases.

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

PREVENT BREAKAGE

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. A basic
concept is that testing helps to build 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.

DISCOVER BUGS

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.

TEST HIGH-RISK CASES

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.

ADAPT TESTING APPROACH

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 tool chain will only discover certain types of bugs.
You need to try different approaches to find new classes of bugs.
Likewise, an existing test suite needs to be updated regularly so
that it still finds regressions.

🔗 International Software Testing Qualifications Board: Certified Tester Foundation


Level Syllabus, Version 2018 V3.1, Page 16: Seven Testing Principles

Tailoring your testing approach


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

EXAMINE YOUR APPLICATION

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.

What are the frequently reported technical problems and


obstacles? For example, your application may lack error
handling or cross-browser compatibility.

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.

DEVELOPMENT PROCESS

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 routine?


For example, developers have not been trained on the testing
tools.

Are tests well-integrated into your development workflow?


For example, a continuous 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 implement steps to achieve it.

RETURN ON INVESTMENT

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 require little effort but cover large parts of
the code.

NORMALIZE TESTING
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.

Hire dedicated software testers, if applicable.

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.

MEANINGFUL TESTS

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 are executed at all.

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.

FIND UNCOVERED CODE

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.

COVER MAIN FEATURES

If you write tests for the main features of your app from a user’s
perspective, you can achieve a code coverage of 60-70%. Every
extra percent gain takes more and more time and bears 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.

🔗 Angular guide: Code Coverage

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

End-to-end tests
SIMULATE REAL USAGE
Some tests have a high-level, bird’s-eye view on the application.
They simulate 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. These
tests 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.

END-TO-END TESTS

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.

ISOLATE ONE PIECE

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.

UNIT TESTS

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

Integration tests
COHESIVE GROUPS

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.

INTEGRATION TESTS

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

SPEED

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.

RELIABILITY

Because of their inherent complexity, end-to-end tests tend to be


unreliable. They often fail even though the software is without
fault. Sometimes they fail for no apparent reason. When you run
the same tests again, they suddenly pass. Even if the test
correctly fails, it is hard to find the root cause of the problem.
You need to wander through the full stack to locate the bug.

SETUP COSTS

End-to-end tests use a real browser and run against the full
software stack. Therefore 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.

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 against a build of the code under test.

COST VS. BENEFIT

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 unreliable, imprecise and hard to debug. The
business value of integration and unit tests is estimated higher.

DISTRIBUTION

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.

DESIGN GUIDE

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

CONFIDENCE
On the other hand, unit tests 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 refactoring. 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.

MIDDLE GROUND

Integration tests provide a better trade-off. 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

Inte­‐
Level End-to-End Unit
gration

Coverage full large small

Performance slow fast fastest

least most
Reliability reliable
reliable reliable

Isolate Failures hard fair easy

Simulate the Real


yes no no
User

(Table adapted from a Google Testing Blog article by Mike Wacker.)

🔗 Martin Fowler: Test Pyramid


🔗 Google Testing Blog: Just Say No to More End-to-End Tests
🔗 Kent C. Dodds: Write tests. Not too many. Mostly integration.

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, unlit box – a black box – or an open, lit box – a white
box. In this metaphor, the code under test is a machine in a box
with holes for inputs and outputs.

OUTSIDE

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. The inner state and workings are not
examined.

In p u
t

Black box

u t
O u tp
INSIDE
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 may call methods that are not part of the public
API, but still technically tangible. Then it checks the internal state
and expects that it has changed accordingly.

IRRELEVANT INTERNALS

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.

RELEVANT BEHAVIOR

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.

PUBLIC API

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
LEARNING OBJECTIVES

Example Angular applications used in the book


Features demonstrated by the examples

In this guide, we will explore the different aspects of testing


Angular applications by looking at two examples.

The counter Component


🔗 Counter Component: Source code
🔗 Counter Component: Run the app
The counter is a reusable Component that increments,
decrements and resets a number using buttons and input fields.

CHALLENGING TO TEST

For intermediate 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
struggle testing the ins and outs.

The goals of this example are:


Simplicity: Quickly grasp what the Component is supposed
to do.

Cover core Angular features: Reusable Components with


state, Inputs, Outputs, templates, event handling.

Scalability: Starting point for more complex application


architectures.

STATE MANAGEMENT

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 popular state management library.) The counter changes
the state indirectly by dispatching NgRx Actions.

While the counter seems easy to implement, it already offers


valuable challenges from a testing perspective.
The Flickr photo search
🔗 Flickr photo search: Source code
🔗 Flickr photo search: Run the app
This application allows you to search for photos on Flickr, the
popular photo hosting site.

TYPICAL APPLICATION FLOW

First, you enter a search term and start the search. The Flickr
search API is queried. Second, the search results with thumbnails
are rendered. Third, you can select a search result to see the
photo details.

This application is straight-forward 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:
STATE MANAGEMENT

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
LEARNING OBJECTIVES

Angular’s architectural principles that facilitate testing


Standard and alternative testing tools
Running and configuring unit and integration tests with Karma and Jasmine

Testability
In contrast to other popular front-end JavaScript libraries,
Angular is 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.

TESTABLE ARCHITECTURE

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.

WELL-STRUCTURED CODE

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 chunks loosely.

Dependency injection and faking


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.

LOOSE COUPLING

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.

ORIGINAL OR FAKE

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 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 guide: Dependency injection

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

BALANCED DEFAULTS

The Angular team already made decisions for you: Jasmine as


testing framework and Karma as test runner. Implementation
and test code is bundled with Webpack. Application parts are
typically tested inside Angular’s TestBed.

This setup is a trade-off with strengths and weaknesses. Since it


is just one possible way to test Angular applications, you can
compile your own testing tool chain.

ALTERNATIVES

For example, some Angular developers use Jest instead of


Jasmine and Karma. Some use Spectator or the Angular Testing
Library instead of using TestBed directly.

These alternatives are not better or worse, they simply make


different trade-offs. This guide uses Jasmine and Karma for unit
and integration tests. Later, you will learn about Spectator.

Once you have reached the limits of a particular setup, you


should investigate whether alternatives make testing your
application easier, faster and more reliable.
Testing conventions
Angular offers some tools and conventions on testing. By design,
they are flexible enough to support different ways of testing. So
you need to decide how to apply them.

MAKING CHOICES

This freedom of choice benefits experts, but confuses beginners.


In your project, there should be one preferable way how to test a
specific application part. You should make choices and set up
project-wide conventions and patterns.

CAST CONVENTIONS INTO CODE

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.

Therefore, you should 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 helper


functions that codify these conventions. Again, your mileage
may vary. You are free to adapt these tools to your needs or
build other testing helpers.
Running the unit and integration
tests
The Angular command line interface (CLI) allows you to run the
unit, integration and end-to-end tests. If you have not installed
the CLI yet or need to update to the latest version, run this
command on your shell:

npm install -g @angular/cli

This installs Angular CLI globally so the ng command can be


used everywhere. ng itself does nothing but exposing a couple of
Angular-specific commands.

For example, ng new creates a new Angular project directory


with a ready-to-use application scaffold. ng serve starts a
development server, and ng build makes a build.

The command for starting the unit and integration tests is:

ng test

This command does several things at once.

TEST.TS

First, ng test uses Webpack to compile your code into a


JavaScript bundle. The entry point for the bundle is
src/test.ts. This file initializes the Angular testing
environment – the TestBed – and then imports all files in the
directory tree that match the pattern .spec.ts.

MAIN.TS

You might be familiar with the entry point for the application,
src/main.ts. This file also initializes Angular, but then it
typically bootstraps (loads and starts) the AppModule. The
AppModule imports other Modules, Components, Services, etc.
This way, the bundler finds all parts of the application.

The test bundle with the entry point test.ts works differently.
It does not start with one Module and walks through all its
dependencies. It merely imports all files whose name ends with
.spec.ts.

.SPEC.TS

Each .spec.ts file represents a test. Typically, one .spec.ts


file contains at least one Jasmine test suite (more on that in the
next chapter). The files are co-located with the implementation
code.

In our example application, the CounterComponent is located in


src/app/components/counter/counter.component.ts. The
corresponding test file sits in
src/app/components/counter/counter.component.spec.ts. This is
an Angular convention, not a technical necessity, and we are
going to stick to it.

KARMA

Second, ng test launches Karma, the test runner. Karma starts


a development server at http://localhost:9876/ that serves the
JavaScript bundles compiled by Webpack.

Karma then launches one or more browsers. The idea of Karma


is to run the same tests in different browsers to ensure cross-
browser interoperability. All widely used browsers are supported:
Chrome, Internet Explorer, Edge, Firefox and Safari. Per default,
Karma starts Chrome.

TEST RUNNER

The launched browser navigates to http://localhost:9876/.


As mentioned, this site serves the test runner and the test
bundle. The tests start immediately. You can track the progress
and read the results in the browser and on the shell.

When running the tests in the counter project, the browser


output looks like this:
This is the shell output:

INFO [karma-server]: Karma v5.0.7 server started at


http://0.0.0.0:9876/

INFO [launcher]: Launching browsers Chrome with concurrency


unlimited

INFO [launcher]: Starting browser Chrome

WARN [karma]: No captured browser, open http://localhost:9876/

INFO [Chrome 84.0.4147.135 (Mac OS 10.15.6)]: Connected on socket


yH0-wtoVtflRWMoWAAAA with id 76614320

Chrome 84.0.4147.135 (Mac OS 10.15.6): Executed 46 of 46 SUCCESS


(0.394 secs / 0.329 secs)

TOTAL: 46 SUCCESS

Webpack watches changes on the .spec.ts files and files


imported by them. When you change the implementation code,
counter.component.ts for example, or the test code,
counter.component.spec.ts for example, Webpack
automatically re-compiles the bundle and pushes it to the open
browsers. All tests will be restarted.

RED-GREEN CYCLE

This feedback cycle allows you to work on the implementation


and test code side-by-side. This is important for test-driven
development. You change the implementation and expect the
test to fail – the test is “red”. You adapt the test so it passes again
– the test is “green”. Or you write a failing test first, then adapt
the implementation until the test passes.

Test-driven development means letting the red-green cycle


guide your development.

🔗 Angular CLI reference: ng test

Configuring Karma and Jasmine


Karma and Jasmine are configured in the file karma.conf.js in
the project’s root directory. There are many configuration
options and plenty of plugins, so we will only mention a few.

LAUNCHERS

As mentioned, the standard configuration opens Chrome. To run


the tests in other browsers, we need to install different
launchers.

The launcher needs to be loaded in the plugins array:

plugins: [

require('karma-jasmine'),

require('karma-chrome-launcher'),

require('karma-jasmine-html-reporter'),

require('karma-coverage'),

require('@angular-devkit/build-angular/plugins/karma')

],

There is already one launcher, karma-chrome-launcher. This is


an npm package.

To install other launchers, we first need to install the respective


npm package. Let us install the Firefox launcher. Run this shell
command:

npm install --save-dev karma-firefox-launcher

Then we require the package in karma.conf.js:

plugins: [

require('karma-jasmine'),

require('karma-chrome-launcher'),

require('karma-firefox-launcher'),

require('karma-jasmine-html-reporter'),

require('karma-coverage'),

require('@angular-devkit/build-angular/plugins/karma'),

],

If we want Karma to launch Firefox when the tests run, we also


need to add it to the list of browsers: browsers: ['Chrome']
becomes browsers: ['Chrome', 'Firefox'].

REPORTERS

Another important concept of Karma are reporters. They format


and output the test results. In the default configuration, three
reporters are active:

1. The built-in progress reporter is responsible for the text


output on the shell. While the tests run, it outputs the
progress:

Chrome 84.0.4147.135 (Mac OS 10.15.6): Executed


9 of 46 SUCCESS (0.278 secs / 0.219 secs)

And finally:

Chrome 84.0.4147.135 (Mac OS 10.15.6): Executed


46 of 46 SUCCESS (0.394 secs / 0.329 secs)

TOTAL: 46 SUCCESS

2. The standard HTML reporter kjhtml (npm package: karma-


jasmine-html-reporter) renders the results in the
browser.
3. The coverage reporter (npm package: karma-coverage)
creates the test coverage report. See measuring code
coverage.

By editing the reporters array, you can add reporters or


replace the existing ones:

reporters: ['progress', 'kjhtml'],

For example, to add a reporter that creates JUnit XML reports,


first install the npm package:

npm install --save-dev karma-junit-reporter

Next, require it as a plugin:

plugins: [

require('karma-jasmine'),

require('karma-chrome-launcher'),

require('karma-jasmine-html-reporter'),

require('karma-coverage'),

require('karma-junit-reporter'),

require('@angular-devkit/build-angular/plugins/karma'),

],

Finally, add the reporter:

reporters: ['progress', 'kjhtml', 'junit'],

When we run the tests with ng test, we will find an XML report
file in the project directory.

JASMINE CONFIGURATION

The configuration for the Jasmine adapter is located in the


client object. To configure Jasmine itself, we need to add the
jasmine property:

client: {

// leave Jasmine Spec Runner output visible in browser

clearContext: false,

jasmine: {

// Jasmine configuration goes here!

},

},

This guide recommends to activate one useful Jasmine


configuration option: failSpecWithNoExpectations lets the
test fail if it does not contain at least one expectation. (More on
expectations later.) In almost all cases, specs without
expectations stem from an error in the test code.

client: {

// leave Jasmine Spec Runner output visible in browser

clearContext: false,

jasmine: {

failSpecWithNoExpectations: true,

},

},

🔗 Karma documentation: Configuration File


🔗 Karma documentation: Plugins
🔗 npm: List of Karma plugins
🔗 Jasmine reference: Configuration options
Test suites with Jasmine
LEARNING OBJECTIVES

Introducing the Jasmine testing framework


Writing test suites, specs and assertions
Structuring a spec with Arrange, Act, Assert
Efficient test suites with setup and teardown logic

Angular ships with Jasmine, a JavaScript framework that enables


you to write and execute unit and integration tests. Jasmine
consists of three important 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, it is recommended to read the official


Jasmine tutorial. This guide provides a short introduction to
Jasmine, exploring the basic structure and terminology that will
be used throughout this guide.

Creating a test 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: SUITE

describe is a function that takes two parameters.

1. A string with a human-readable name. Typically the name of


the function or class under test. For example,
describe('CounterComponent', /* … */) is the suite
that tests the CounterComponent class.

2. A function containing the suite definition.

A describe block groups related specs that we will learn about


in the next chapter.

NESTING DESCRIBE

describe blocks can be nested to structure big suites and divide


them into logical sections:

describe('Suite description', () => {

describe('One aspect', () => {

/* … */

});

describe('Another aspect', () => {

/* … */

});

});

Nested describe blocks add a human-readable description to a


group of specs. They can also host their own setup and teardown
logic.

Specifications
IT: SPEC

Each suit consists of one or more specifications, or short, 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 description. The
second parameter is a function containing the spec code.

READABLE SENTENCE
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 spec code then proves 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 CounterComponent, 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.

NO “SHOULD”
Some people prefer to write it('should increment the
count', /* … */), but should bears no additional meaning.
The nature of a spec is to state what the code under test should
do. The word “should” is redundant and just makes the sentence
longer. This guide recommends to simply state what the code
does.

🔗 Jasmine tutorial: Your first suite

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.

ARRANGE, ACT, 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 CounterComponent look like?

1. Arrange:

Create an instance of CounterComponent.

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

STRUCTURE A TEST

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)

GIVEN, WHEN, THEN

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 pose a natural way to think of a test’s structure:
“Given these 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}`

);

EXPECT

We could write that code in a Jasmine spec, but Jasmine allows


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

MATCHERS

toBe is the simplest matcher that applies to all possible


JavaScript values. Internally, it uses JavaScript’s strict equality
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 the actual and
the expected value are the very same object. toBe 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, the two objects are not identical

expect({ name: 'Linda' }).toBe({ name: 'Linda' });

// Passes, the two 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.

READABLE SENTENCE

The pattern
expect(actualValue).toEqual(expectedValue) originates
from Behavior-Driven Development (BDD) again. The expect
function call and the matcher methods form a human-readable
sentence: “Expect the actual value to equal the expected value.”
The goal is to write a specification that is as readable as a plain
text but can be verified automatically.
🔗 Jasmine documentation: Built-in matchers
🔗 Jasmine tutorials: Custom matchers

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 CounterComponent, the Arrange
phase always consists of creating a Component instance and
rendering it into the document.

REPETITIVE SETUP

This setup is repeated over and over, so it should be defined


once in a central place. You could write a setup function and call
it at the beginning of each spec. But using Jasmine, you can
declare code that is called before and after each spec, or before
and after all specs.

For this purpose, Jasmine provides 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

Most tests we are going to write will have a beforeEach block to


host the Arrange code.
Faking dependencies
LEARNING OBJECTIVES

Testing a code unit in isolation


Replacing dependencies with fakes
Rules for creating fakes to avoid pitfalls
Using Jasmine spies to fake functions and methods

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.

ALSO KNOWN AS MOCKING

These replacements are also called test doubles, stubs or mocks.


Replacing a dependency is called stubbing or mocking.

Since these terms are used inconsistently and their difference is


subtle, this guide uses the term “fake” and “faking” for any
dependency substitution.

FAKING SAFELY

Creating and injecting fake dependencies is essential for unit


tests. This technique is double-edged – powerful and dangerous
at the same time. Since we will create many fakes throughout
this guide, we need to set up rules for faking dependencies to
apply the technique safely.

Equivalence of fake and original


A fake implementation must have the same shape of 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 public methods and
properties.

REPLACEABILITY

The fake does not need to be complete, but sufficient enough to


act as a replacement. The fake needs to be equivalent to the
original as far as the code under test is concerned, not fully
equal to the original.

Imagine a fake building on a movie set. The outer shape needs


to be indistinguishable from an original building. But behind the
authentic facade, there is only a wooden scaffold. The building is
an empty shell.

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 out 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. When the fake is outdated, the unit test becomes a
fantasy world where everything magically works. The test passes
but in fact the code under test is broken.

KEEP FAKE IN SYNC

How can we ensure that the fake is up-to-date with the original?
How can we ensure the equivalence of original and fake in the
long run and prevent any possible divergence?

We can use TypeScript to enforce that the fake has a matching


type. The fake needs to be strictly typed. The fake’s type needs
to be a subset of the original’s type.

TYPE EQUIVALENCE

Then, TypeScript assures the equivalence. The compiler reminds


us to update the implementation and the fake. The TypeScript
code simply does not compile if we forget that. We will learn how
to declare matching types in the upcoming examples.

Effective faking
The original dependency code has side effects that need to be
suppressed during testing. The fake needs to effectively prevent
the original code from being executed. Strange errors may
happen if a mix of fake and original code is executed.

DO NOT MIX FAKE AND ORIGINAL

In some faking approaches, the fake inherits from the original.


Only those properties and methods are overwritten that are
currently used by the code under test.

This is dangerous since we may forget to overwrite methods.


When the code under test changes, the test may accidentally call
original methods of the dependency.

This guide will present thorough faking techniques that do not


allow a slip. They imitate the original code while shielding the
original from calls.

Faking functions 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.

CALL RECORD
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 is a fixed value. The spy will always
return the same value, regardless of the input parameters. In a
more complex case, the return value originates from an
underlying fake function.

CREATESPY

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.

Assume 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 TypeScript 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();

INJECT FAKE

The TodoService uses the constructor injection pattern. 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. Let us start with 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.

FAKE RESPONSE

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. (These fetch details are not relevant to
grasp the spy 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.

INJECT SPY

We also create an instance of TodoService, the class under test.


We pass the spy into the constructor. This is a form of manual
dependency injection.

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

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 create two expectations:


expect(actualTodos).toEqual(todos);

expect(fetchSpy).toHaveBeenCalledWith('/todos');

DATA PROCESSING

First, we verify the return value. We compare the actual data


(actualTodos) with the fake data the spy returns (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 deduce
that the spy has been called.)

VERIFY CALL RECORD

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.

HAPPY AND UNHAPPY PATHS

After having written the first spec for getTodos, we need to ask
ourselves: Does the test fully cover its behavior? We have tested
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 error


response.

CATCHING ERRORS

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 TypeScript example to illustrate the usage of


spies. Usually, an Angular Service does not use fetch directly
but uses HttpClient instead. We will get to know testing this
later (see Testing a Service that sends HTTP requests).

🔗 TodoService: Implementation and test code


🔗 Jasmine reference: Spies

Spying on existing methods


We have used jasmine.createSpy('name') to create a
standalone spy and have injected it into the constructor. Explicit
constructor injection is straight-forward and used extensively in
Angular code.

SPY ON OBJECT METHODS

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');

OVERWRITE AND RESTORE

This installs a spy on the global fetch method. Under the hood,
Jasmine 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, enabling us 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.

🔗 Jasmine reference: spyOn


Debugging tests
LEARNING OBJECTIVES

Fixing problems in your test


Finding the cause of a failing test
Applying familiar debugging techniques to tests
Using Jasmine debugging features

Writing tests is as arduous as writing implementation code. You


will be stuck quite often and ask yourself why the test fails – and
sometimes why the test passes when it should rather fail.

The good news is that you can apply familiar debugging


techniques to tests as well.

Test focus
Some tests require an extensive Arrange phase, the Act phase
calls several methods or simulates complex user input. These
tests are hard to debug.

ISOLATE THE PROBLEM

When locating an error, narrow down the scope gradually:


Execute only one test, one suite, one spec, one expectation.
Per default, Karma and Jasmine compile and run all specs again
with every code change. This leads to a slow feedback cycle
when you work on a particular spec. After a code change, it may
take 10-20 seconds before you see the test result. Also one spec
might interfere with another spec.

The easiest way to narrow down the scope is to set a focus on a


suite or spec. Let us assume you have a test suite with two specs:

describe('Example spec', () => {

it('one spec', () => { /* … */ });

it('another spec', () => { /* … */ });

});

FDESCRIBE

If you want Jasmine to run only this test suite and skip all others,
change describe to fdescribe:

fdescribe('Example spec', () => {

it('one spec', () => { /* … */ });

it('another spec', () => { /* … */ });

});

FIT

If you want Jasmine to run only one spec, change it to fit:

describe('Example spec', () => {

fit('one spec', () => { /* … */ });

it('another spec', () => { /* … */ });

});

This improves the developing experience tremendously.

The Webpack module bundler still re-emits the whole bundle


even if you have only changed one line of code and even if there
is a test focus on one suite.

BUNDLE ONE FILE

In this case, you can instruct ng test to consider only the file
you are currently working on. Webpack then includes all its
dependencies, like the Angular framework, but not more.

For example, to include only tests called


counter.component.spec.ts, we call ng test with the --
include option.

ng test --include **/counter.component.spec.ts

**/counter.component.spec.ts means all files called


counter.component.spec.ts in any subdirectory.

The bundling is now fast and the feedback is almost instant


when we change implementation or test code.

Keep in mind to remove the test focus before committing your


code. There are several tools that prevent fdescribe and fit
from being committed.

🔗 Jasmine API reference: fdescribe


🔗 Jasmine API reference: fit
🔗 Angular CLI reference: ng test
🔗 Tim Deschryver: Don’t commit focused tests

Developer tools
The Jasmine test runner is just another web page made with
HTML, CSS and JavaScript. This means you can debug it in the
browser using the developer tools.

FAMILIAR DEBUGGING TOOLS

Focus the browser window and open the developer tools. In


Chrome, Firefox and Edge, you can use the F12 key.

You can use the developer tools to:

Write debug output to the console using console.log,


console.debug and friends.

Use the JavaScript debugger. You can either set breakpoints


in the developer tools or place a debugger statement.

Inspect the DOM of rendered Components.

Debug output and the JavaScript


debugger
The most primitive tool, console.log, is in fact invaluable when
debugging tests. You can place debug output both in the test
code and the implementation code.

VERSATILE CONSOLE.LOG

Use debug output to answer these questions:

Is the test, suite, spec run at all?

Does the test execution reach the log command?

Did the test call the class, method, function under test
correctly?

Are callbacks called correctly? Do Promises complete or fail?


Do Observables emit, complete or error?

For Component tests:

Is Input data passed correctly?

Are the lifecycle methods called correctly?

DEBUGGER

Some people prefer to use debugger instead of console output.


While the debugger certainly gives you more control, it halts the
JavaScript execution. It may disturb the processing of
asynchronous JavaScript tasks and the order of execution.

ASYNC LOGGING

The console methods have their own pitfalls. For performance


reasons, browsers do not write the output to the console
synchronously, but asynchronously.

If you output a complex object with console.log(object),


most browsers render an interactive representation of the object
on the console. You can click on the object to inspect its
properties.

const exampleObject = { name: 'Usagi Tsukino' };

console.log(exampleObject);

It is important to know that the rendering happens


asynchronously. If you change the object shortly after, you might
see the changed object, not the object at the time of the
console.log call.

const exampleObject = { name: 'Usagi Tsukino' };

console.log(exampleObject);

exampleObject.name = 'Sailor Moon';

On the console, the object representation may show name:


'Sailor Moon' instead of name: 'Usagi Tsukino'.

One way to prevent this confusion is to create a snapshot of the


object. You convert the object to a JSON string:

const exampleObject = { name: 'Usagi Tsukino' };

console.log(JSON.stringify(exampleObject, null, ' '));

exampleObject.name = 'Sailor Moon';

LOG A SNAPSHOT

If you want an interactive representation on the console, create a


copy of the object with JSON.stringify followed by
JSON.parse:

const exampleObject = { name: 'Usagi Tsukino' };

console.log(JSON.parse(JSON.stringify(exampleObject)));

exampleObject.name = 'Sailor Moon';

Obviously, this only works for objects that can be serialized as


JSON.
Inspect the DOM
In the next chapter, we will learn how to test Components. These
tests will render the Component into the DOM of the Jasmine
test runner page. This means you can briefly see the states of
the rendered Component in the browser.

In the screenshot above, you see the rendered Component on


the left side and the inspected DOM on the right side.

ROOT ELEMENT

The Component’s root element is rendered into the last element


in the document, below the Jasmine reporter output. Make sure
to set a focus on a single spec to see the rendered Component.
The rendered Component is interactive. For example, you can
click on buttons and the click handlers will be called. But as we
will learn later, there is no automatic change detection in the
testing environment. So you might not see the effect of the
interaction.

Jasmine debug runner


The Karma page at http://localhost:9876 loads an iframe with the
actual Jasmine instance, http://localhost:9876/context.html. This
iframe complicates debugging because the developer tools
operate on the topmost document per default.

In the developer tools, you can select the iframe window context
(Chrome is pictured):
This way you can access global objects and the DOM of the
document where the tests run.

DEBUG RUNNER WITHOUT IFRAME

Another helpful feature is Karma’s debug test runner. Click on


the big “DEBUG” button on the top-right. Then a new tab opens
with http://localhost:9876/debug.html.

The debug test runner does not have an iframe, it loads Jasmine
directly. Also it automatically logs spec runs on the shell.

If you change the test or implementation code, the debug


runner does not re-run the tests. You have to reload the page
manually.
Testing Components
LEARNING OBJECTIVES

Setting up a Component test using Angular’s testing Module


Getting familiar with Angular’s Component testing abstractions
Accessing the rendered DOM and checking text content
Simulating user input like clicks and form field input
Testing Component Input and Outputs
Using helpers functions for common Component testing tasks

Components are the power houses of an Angular application.


Components are composed to form the user interface.

A Component deals with several concerns, among others:

It renders the template into the HTML DOM.

It accepts data from parent Components using Input


properties.

It emits data to parent Components using Outputs.

It reacts to user input by registering event handlers.

It renders the content (ng-content) and templates (ng-


template) that are passed.

It binds data to form controls and allows the user to edit the
data.
It talks to Services or other state managers.

It uses routing information like the current URL and URL


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

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?

COUNTER FEATURES

We will test the following features of the CounterComponent:

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 to
specs in a test suite.

🔗 CounterComponent: full code

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.

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

🔗 Angular API reference: TestBed


🔗 Testing Utility APIs: TestBed

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 that accepts a Module
definition:

TestBed.configureTestingModule({

imports: [ /*… */ ],

declarations: [ /*… */ ],

providers: [ /*… */ ],

});

DECLARE WHAT IS NECESSARY

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
CounterComponent, 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: [CounterComponent],

});

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.

CONFIGURE AND COMPILE

Since configureTestingModule returns the TestBed again,


we can chain those two calls:

TestBed

.configureTestingModule({

declarations: [CounterComponent],

})

.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(CounterComponent);

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 div container


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 {{
count }} in the example, are not evaluated.

MANUAL CHANGE DETECTION

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 us 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();

🔗 Angular API reference: ComponentFixture

TestBed and Jasmine


The code for rendering a Component using the TestBed is now
complete. Let us wrap the code in a Jasmine test suite.

describe('CounterComponent', () => {

let fixture: ComponentFixture<CounterComponent>;

beforeEach(async () => {

await TestBed.configureTestingModule({

declarations: [CounterComponent],

}).compileComponents();

fixture = TestBed.createComponent(CounterComponent);

fixture.detectChanges();

});

it('…', () => {

/* … */

});

});

Using describe, we define a test suite for the


CounterComponent. In contains a beforeEach block that
configures the TestBed and renders the Component.

ASYNC COMPILATION

You might wonder why the function passed to beforeEach is


marked as an async function. It is because
compileComponents is an asynchronous operation. To compile
the Components, Angular needs to fetch external the template
files referenced by templateUrl.

If you are using the Angular CLI, which is most likely, the
template files are already included in the test bundle. So they are
available instantly. If you are not using the CLI, the files have to
be loaded asynchronously.

This is an implementation detail that might change in the future.


The safe way is wait for compileComponents to complete.

ASYNC AND AWAIT

Per default, Jasmine expects that your testing code is


synchronous. The functions you pass to it but also
beforeEach, beforeAll, afterEach, afterAll need to finish
in a certain amount of time, also known as timeout. Jasmine also
supports asynchronous specs. If you pass an async function,
Jasmine waits for it to finish.
🔗 Test suites with Jasmine

ComponentFixture and
DebugElement
TestBed.createComponent(CounterComponent) returns a
fixture, an instance of ComponentFixture. What is the fixture
and what does it provide?

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 object and connects to electrical contacts in order to provide
power and to take measurements.

COMPONENT­
F IXTURE

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
CounterComponent instance.

const component = fixture.componentInstance;

The Component instance is mainly used to set Inputs and


subscribe to Outputs, for example:

// This is a ComponentFixture<CounterComponent>

const component = fixture.componentInstance;

// Set Input

component.startCount = 10;

// Subscribe to Output

component.countChange.subscribe((count) => {

/* … */

});

We will learn more on testing Inputs and Outputs later.

DEBUGELEMENT

For accessing elements in the DOM, Angular has another


abstraction: The DebugElement wraps the native DOM element.
The fixture’s debugElement property returns the Component’s
host element. For the CounterComponent, this is the app-
counter element.

const { debugElement } = fixture;

The DebugElement offers handy properties like properties,


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

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

When you use nativeElement, you need to learn about the


DOM interface of the specific element. For example, a button
element is represented as HTMLButtonElement in the DOM.

🔗 Angular API reference: ComponentFixture


🔗 Angular API reference: DebugElement

Writing the first Component spec


We have compiled a test suite that renders the
CounterComponent. 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 phases help us to structure the


spec:

We have already covered the Arrange phase in the


beforeEach block that renders 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 methods query and
queryAll for finding descendant elements (children,
grandchildren and so forth).

QUERY AND QUERYALL

query returns the first descendant element that meets a


condition.

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.

BY.CSS

Angular ships with predefined predicate functions 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.queryAll(By.css('.user'));

The return value of query is a DebugElement again, that 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.

AVOID TIGHT COUPLING

Type and class selectors introduce a tight coupling between the


test and the template. HTML elements are picked for semantic
reasons. Classes are picked mostly for 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 and the class are crucial for the
feature under test. But most of the time, they are not relevant
for the feature. The test should better find the element by a
feature that never changes and that bears no additional
meaning: test ids.

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 the
element type or unrelated attributes 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.

DATA-TESTID

For the purpose of this guide, we use the data-testid


attribute. For example, we mark the increment button in the
CounterComponent 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"]')

);

ESTABLISH A CONVENTION

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 tolerate different approaches. After
consideration, you should opt for one specific solution,
document it as a testing convention and apply it consistently
across all tests.

🔗 Angular API reference: By.css

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.

TRIGGER EVENT HANDLER

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 CounterComponent will be called.

EVENT OBJECT

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.

NO BUBBLING

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>{{ count }}</strong>

FIND BY TEST ID

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">{{ count }}</strong>

We can now find the element as usual:

const countOutput = debugElement.query(

By.css('[data-testid="count"]')

);

TEXT CONTENT

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 that 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 counter.component.spec.ts now looks like this:

/* Incomplete! */

describe('CounterComponent', () => {

let fixture: ComponentFixture<CounterComponent>;

let debugElement: DebugElement;

// Arrange

beforeEach(async () => {

await TestBed.configureTestingModule({

declarations: [CounterComponent],

}).compileComponents();

fixture = TestBed.createComponent(CounterComponent);

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:

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

MANUAL CHANGE DETECTION

We have mentioned that in the testing environment, Angular


does not automatically detect changes in order to 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('CounterComponent', () => {

let fixture: ComponentFixture<CounterComponent>;

let debugElement: DebugElement;

// Arrange

beforeEach(async () => {

await TestBed.configureTestingModule({

declarations: [CounterComponent],

}).compileComponents();

fixture = TestBed.createComponent(CounterComponent);

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.

🔗 CounterComponent: test code

Testing helpers
The next CounterComponent 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('decrements 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.

REPEATING PATTERNS
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.

TESTING HELPERS

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.
FIND BY TEST ID

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. We need to pass in the


Component fixture explicitly. 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.

CLICK

Second, we write a testing helper that clicks on an element with


a given test id. This helper builds on findEl.
export function click<T>(

fixture: ComponentFixture<T>,

testId: string

): void {

const element = findEl(fixture, testId);

const event = makeClickEvent(element.nativeElement);

element.triggerEventHandler('click', event);

To create a fake click event object, click calls another function,


makeClickEvent.

export function makeClickEvent(

target: EventTarget

): Partial<MouseEvent> {

return {

preventDefault(): void {},

stopPropagation(): void {},

stopImmediatePropagation(): void {},

type: 'click',

target,

currentTarget: target,

bubbles: true,

cancelable: true,

button: 0

};

This function returns a partial MouseEvent fake object with the


most important methods and properties of real click events. It is
suitable for clicks on buttons and links when the pointer position
and modifier keys do not matter.
CLICK MEANS ACTIVATE

The click testing 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 already the case
for buttons (button element) and links (a elements).

Historically, the click event was only triggered by mouse input.


Today, it is a generic “activate” event. It is also triggered by touch
input (“tap”), 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.

EXPECT TEXT CONTENT

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 we will improve later.


Using these helpers, we rewrite our spec:

it('decrements the count', () => {

// Act

click(fixture, 'decrement-button');

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

🔗 CounterComponent: test code


🔗 Element spec helpers: full code

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 the field, then clicks
on the button. The Component resets the count to the user-
provided number.
SET FIELD VALUE

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.

For the reset input, this means:

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 CounterComponent’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.

FAKE INPUT EVENT

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. Such events are also
called synthetic events.

In newer browsers, we create a fake input event with new


Event('input'). To dispatch the event, we use the
dispatchEvent method of the target element.

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

resetInputEl.value = '123';

resetInputEl.dispatchEvent(new Event('input'));

If you need to run your tests in legacy 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.

HELPER FUNCTIONS
The helper function setFieldValue takes a Component fixture,
a test id and a string value. It finds the corresponding element
using findEl. Using another helper, setFieldElementValue,
it sets the value and dispatches an input event.

export function setFieldValue<T>(

fixture: ComponentFixture<T>,

testId: string,

value: string,

): void {

setFieldElementValue(

findEl(fixture, testId).nativeElement,

value

);

You can find the full source code of the involved helper functions
in element.spec-helper.ts.

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);

});

While the reset feature is simple, this is how to test most form
logic. Later, we will learn how to test complex forms.

INVALID INPUT

The CounterComponent checks the input value before it resets


the count. If the value is not a number, clicking the reset button
does nothing.

We need to cover this behavior with another spec:

it('does not reset if the value is not a number', () => {

const value = 'not a number';

// Act

setFieldValue(fixture, 'reset-input', value);

click(fixture, 'reset-button');

fixture.detectChanges();

// Assert

expectText(fixture, 'count', startCount);

});

The small difference in this spec is that we set the field value to
“not a number”, a string that cannot be parsed as a number, and
expect the count to remain unchanged.

This is it! We have tested the reset form with both valid and
invalid input.
🔗 CounterComponent: test code
🔗 Element spec helpers: full code

Testing Inputs
CounterComponent has an Input startCount that sets the
initial count. We need to test that the counter handles the Input
properly.

For example, if we set startCount to 123, the rendered count


needs to be 123 as well. If the Input is empty, the rendered
count needs to be 0, the default value.

SET INPUT VALUE

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.

INPUT VS. COMPONENT STATE

That is why CounterComponent 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 method:

public ngOnChanges(): void {

this.count = this.startCount;

ngOnChanges is called whenever a “data-bound property”


changes, including Inputs.

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

/* Incomplete! */

beforeEach(async () => {

/* … */

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

CounterComponent > shows the start count

Expected '0' to be '123'.

NGONCHANGES

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 corrected example:

describe('CounterComponent', () => {

let component: CounterComponent;

let fixture: ComponentFixture<CounterComponent>;

const startCount = 123;

beforeEach(async () => {

await TestBed.configureTestingModule({

declarations: [CounterComponent],

}).compileComponents();

fixture = TestBed.createComponent(CounterComponent);

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 CounterComponent expects a number Input and renders it


into the DOM. When reading text from the DOM, we always deal
with strings. That is why we pass in a number 123 but expect to
find the string '123'.

🔗 CounterComponent: test code

Testing Outputs
While Inputs pass data from parent to child, Outputs 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, Outputs are Component
instance properties. A unit test must inspect the Outputs
thoroughly to proof that the Component plays well with other
Components.

The CounterComponent has an output named countChange.


Whenever the count changes, the countChange Output emits
the new value.

export class CounterComponent implements OnChanges {

/* … */

@Output()

public countChange = new EventEmitter<number>();

/* … */

SUBSCRIBE TO OBSERVABLE

EventEmitter is a subclass of RxJS Subject, which 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 on increment', () => {

/* … */

});

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');

});

CHANGE VARIABLE VALUE

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');

// Assert

expect(actualCount).toBe(1);

});

EXPECT CHANGED VALUE

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');

});

ALWAYS RUN EXPECTATION


This works as well. But 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 (see Configuring Karma and
Jasmine). 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 on decrement and reset. We can achieve that by
adding two more specs that copy the existing spec:

it('emits countChange events on decrement', () => {

// Arrange

let actualCount: number | undefined;

component.countChange.subscribe((count: number) => {

actualCount = count;

});

// Act

click(fixture, 'decrement-button');

// 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);

});

🔗 CounterComponent: test code

Repetitive Component specs


Testing the countChange Output with three specs works fine,
but the code is highly repetitive. A testing helper can reduce the
repetition. Experts disagree on whether repetitive testing code is
a problem at all.

On the one hand, it is hard to grasp the essence of repetitive


specs. Testing helpers form a custom language for expressing
testing instructions clearly and briefly. For example, if your specs
find DOM elements via test ids, a testing helper establishes the
convention and hides the implementation details.
On the other hand, abstractions like helper functions make tests
more complex and therefore harder to understand. A developer
reading the specs needs to get familiar with the testing helpers
first. After all, tests should be more readable than the
implementation code.

DUPLICATION VS. ABSTRACTION

There is a controversial debate in software development


regarding repetition and the value of abstractions. As 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


a proper abstraction for all these diverse, sometimes mutually
exclusive cases is often futile.

CAREFULLY REDUCE REPETITION

Your mileage may vary on this question. For completeness, let us


discuss how to reduce the repetition in the countChange Output
specs.
An Output is an EventEmitter, that 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');

click(fixture, 'decrement-button');

setFieldValue(fixture, 'reset-input', String(newCount));

click(fixture, 'reset-button');

// 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 an optional way to merge
three specs into one.

🔗 CounterComponent: test code

Black vs. white box Component


testing
Component tests are most meaningful if they closely mimic how
the user interacts with the Component. The tests we have
written apply this principle. We have worked directly with the
DOM to read text, click on buttons and fill out form fields
because this 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.
INTERNAL YET PUBLIC

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 CounterComponent 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 { /* … */ }

PUBLIC FOR THE TEMPLATE


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.

In our CounterComponent black box test, we increment the


count by clicking on the “+” button. In contrast, many Angular
testing tutorials conduct Component white box tests. They call
the increment method directly:

/* Not recommended! */

describe('CounterComponent', () => {

/* … */

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 sometimes valuable, but
most of the time it is misused.

INPUTS, OUTPUTS, DOM

As we have learned, a Component test is meaningful if it


interacts with the Component via Inputs, Outputs and the
rendered DOM. If the Component test calls internal methods or
accesses internal properties instead, it often misses important
template logic and event handling.

The white box spec above 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.

START WITH BLACK BOX TESTS

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
Component. Then imitate the usage in your test.

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

That being said, white box testing is a viable advanced


technique. Experienced testers can write efficient white box
specs that still test out all Component 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.

RECOMMENDATION

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

No access
and methods
Testing Components with
children
LEARNING OBJECTIVES

Rendering a Component with or without its children


Checking that the parent and its children are wired up correctly
Replacing child Components with fakes
Using the ng-mocks library to fake dependencies

PRESENTATIONAL COMPONENTS

So far, we have tested an independent Component that renders


plain HTML elements, but no child Components. Such low-level
Components are the workhorses of an Angular application.

They directly render what the user sees and interacts with.

They are often highly generic and reusable.

They are controlled through Inputs and report back using


Outputs.

They have little to none dependencies.

They are easy to reason about and therefore easy to test.

The preferred way of testing them is a unit test.


These Components are called presentational Components
since they directly present a part of the user interface using
HTML and CSS. Presentational Components need to be
combined and wired to form a working user interface.

CONTAINER COMPONENTS

This is the duty of container Components. These high-level


Components bring multiple low-level Components together.
They pull data from different sources, like Services and state
managers, and distribute it to their children.

Container Components have several types of dependencies.


They depend on the nested child Components, but also
Injectables. These are classes, functions, objects, etc. provided
via dependency injection, like Services. These dependencies
make testing container Components complicated.

SHALLOW VS. DEEP RENDERING

There are two fundamental ways to test Components with


children:

A unit test using shallow rendering. The child Components


are not rendered.

An integration test using deep rendering. The child


Components are rendered.
Again, both are valid approaches we are going to discuss.

Shallow vs. deep rendering


In the counter example application, the HomeComponent
contains CounterComponents, ServiceCounterComponents
and NgRxCounterComponents.

From the template:

<app-counter

[startCount]="5"

(countChange)="handleCountChange($event)"

></app-counter>

<!-- … -->

<app-service-counter></app-service-counter>

<!-- … -->

<app-ngrx-counter></app-ngrx-counter>

These custom app-* elements end up in the DOM tree. They


become the host elements of the child Components.

CHECK WIRING ONLY

A unit test of HomeComponent does not render these children.


The host elements are rendered, but they remain empty. You
might wonder, what is the point of such a test? What does it do
after all?
From HomeComponent’s perspective, the inner workings of its
children are not relevant. We need to test that the template
contains the children. Also, we need to check that
HomeComponent and its children are wired up correctly using
Inputs and Outputs.

In particular, the HomeComponent unit test checks that an app-


counter element is present, that the startCount Input is
passed correctly and that HomeComponent handles the
countChange event. The same is done for the other children,
app-service-counter and app-ngrx-counter.

RENDER CHILDREN

An integration test of HomeComponent renders the child


Components. The host elements are filled with the output of
CounterComponent, ServiceCounterComponent and
NgRxCounterComponent, respectively. This integration test is
actually testing all four Components.

TEST COOPERATION

We need to decide the level of detail for testing the nested


Components. If separate unit tests for them exist, we do not
need to click on each respective increment button. After all, the
integration test needs to prove that the four Component work
together, without going into the child Component details.
🔗 HomeComponent: implementation and test code

Unit test
Let us write a unit test for HomeComponent first. The setup looks
familiar to the CounterComponent test suite. We are using
TestBed to configure a testing Module and to render the
Component under test.

describe('HomeComponent', () => {

let fixture: ComponentFixture<HomeComponent>;

let component: HomeComponent;

beforeEach(async () => {

await TestBed.configureTestingModule({

declarations: [HomeComponent],

}).compileComponents();

fixture = TestBed.createComponent(HomeComponent);

component = fixture.componentInstance;

fixture.detectChanges();

});

it('renders without errors', () => {

expect(component).toBeTruthy();

});

});

SMOKE TEST
This suite has one spec that acts as a smoke test. It checks the
presence of a Component instance. It does not assert anything
specific about the Component behavior yet. It merely proves that
the Component renders without errors.

If the smoke test fails, you know that something is wrong with
the testing setup.

UNKNOWN CUSTOM ELEMENTS

From Angular 9 on, the spec passes but produces a bunch of


warnings on the shell:

'app-counter' is not a known element:

1. If 'app-counter' is an Angular component, then


verify that it is part of this module.

2. If 'app-counter' is a Web Component then add


'CUSTOM_ELEMENTS_SCHEMA' to the
'@NgModule.schemas' of this component to suppress
this message.

We get the same warning regarding app-service-counter


and app-ngrx-counter. Another warning reads:

Can't bind to 'startCount' since it isn't a known


property of 'app-counter'.
What do these warnings mean? Angular does not recognize the
custom elements app-counter, app-service-counter and
app-ngrx-counter because we have not declared Components
that match these selectors. The warning points at two solutions:

1. Either declare the child Components in the testing Module.


This turns the test into an integration test.

2. Or tell Angular to ignore the unknown elements. This turns


the test into a unit test.

IGNORE CHILD ELEMENTS

Since we plan to write a unit test, we opt for the second.

When configuring the testing Module, we can specify schemas


to tell Angular how to deal with elements that are not handled by
Directives or Components.

The warning suggests CUSTOM_ELEMENTS_SCHEMA, but the


elements in question are not Web Components. We want
Angular to simply ignore the elements. Therefore we use the
NO_ERRORS_SCHEMA, “a schema that allows any property on any
element”.

await TestBed.configureTestingModule({

declarations: [HomeComponent],

schemas: [NO_ERRORS_SCHEMA],

}).compileComponents();

With this addition, our smoke test passes.

Now let us write a more meaningful spec! We start with the


nested app-counter. This is the code we need to cover:

<app-counter

[startCount]="5"

(countChange)="handleCountChange($event)"

></app-counter>

CHILD PRESENCE

First of all, we need to test the presence of app-counter, the


independent counter. We create a new spec for that purpose:

it('renders an independent counter', () => {

/* … */

});

To verify that an app-counter element exists in the DOM, we


use the familiar query method of the topmost DebugElement.

const { debugElement } = fixture;

const counter = debugElement.query(By.css('app-counter'));

This code uses the app-counter type selector to find the


element. You might wonder, why not use a test id and the
findEl helper?

FIND BY ELEMENT TYPE


In this rare occasion, we need to enforce the element app-
counter because this is CounterComponent’s selector.

Using a test id makes the element type arbitrary. This makes


tests more robust in other case. When testing the existence of
child Components though, it is the element type that invokes the
child.

Our spec still lacks an expectation. The query method returns a


DebugElement or null. We simply expect the return value to be
truthy:

it('renders an independent counter', () => {

const { debugElement } = fixture;

const counter = debugElement.query(By.css('app-counter'));

expect(counter).toBeTruthy();

});

Finding a child Component is a common task. Such repeating


patterns are good candidates for testing helpers. Not because it
is much code, but because the code has a specific meaning we
would like to convey.

debugElement.query(By.css('app-counter')) is not
particularly descriptive. The reader has to think for a moment to
realize that the code tries to find a nested Component.

FINDCOMPONENT

So let us introduce a helper function named findComponent.


export function findComponent<T>(

fixture: ComponentFixture<T>,

selector: string,

): DebugElement {

return fixture.debugElement.query(By.css(selector));

Our spec now looks like this:

it('renders an independent counter', () => {

const counter = findComponent(fixture, 'app-counter');

expect(counter).toBeTruthy();

});

CHECK INPUTS

The next feature we need to test is the startCount Input. In


particular, the property binding [startCount]="5" in
HomeComponent’s template. Let us create a new spec:

it('passes a start count', () => {

const counter = findComponent(fixture, 'app-counter');

/* … */

});

PROPERTIES

How do we read the Input value? Each DebugElement has a


properties object that contains DOM properties together with
its values. In addition, it contains certain property bindings. (The
type is { [key: string]: any }).
In a unit test with shallow rendering, properties contains the
Inputs of a child Component. First, we find app-counter to
obtain the corresponding DebugElement. Then we check the
Input value, properties.startCount.

it('passes a start count', () => {

const counter = findComponent(fixture, 'app-counter');

expect(counter.properties.startCount).toBe(5);

});

That was quite easy! Last but not least, we need to test the
Output.

OUTPUT EVENT

From HomeComponent’s perspective, reacting to the Output is


like handling an event on the app-counter element. The
template uses the familiar (event)="handler($event)"
syntax:

<app-counter

[startCount]="5"

(countChange)="handleCountChange($event)"

></app-counter>

The handleCountChange method is defined in the Component


class. It simply calls console.log to prove that the child-parent
communication worked:

export class HomeComponent {

public handleCountChange(count: number): void {

console.log('countChange event from CounterComponent', count);

Let us add a new spec for testing the Output:

it('listens for count changes', () => {

/* … */

});

The spec needs to do two things:

1. Act: Find the child Component and let the countChange


Output emit a value.

2. Assert: Check that console.log has been called.

From the parent’s viewpoint, countChange is simply an event.


Shallow rendering means there is no CounterComponent
instance and no EventEmitter named countChange. Angular
only sees an element, app-counter, with an event handler,
(countChange)="handleCountChange($event)".

SIMULATE OUTPUT

In this setup, we can simulate the Output using the known


triggerEventHandler method.

it('listens for count changes', () => {

/* … */

const counter = findComponent(fixture, 'app-counter');

const count = 5;

counter.triggerEventHandler('countChange', 5);

/* … */

});

The spec finds the app-counter element and triggers the


countChange event handler.

The second triggerEventHandler parameter, 5, is not an


event object as we know from DOM events like click. It is a
value that the Output would emit. The countChange Output has
the type EventEmitter<number>, so we use the fixed number 5
for testing purposes.

OUTPUT EFFECT

Under the hood, triggerEventHandler runs


handleCountChange($event) with $event being 5.
handleCountChange calls console.log. This is the observable
effect we need to test.

How do we verify that console.log has been called? We can


spy on existing methods with Jasmine’s spyOn.

spyOn(console, 'log');

This overwrites console.log with a spy for the duration of the


test run. We need to set up the spy in the Arrange phase, at the
beginning of our spec.
it('listens for count changes', () => {

spyOn(console, 'log');

const counter = findComponent(fixture, 'app-counter');

const count = 5;

counter.triggerEventHandler('countChange', count);

/* … */

});

In the Assert phase, we expect that the spy has been called with a
certain text and the number the Output has emitted.

it('listens for count changes', () => {

spyOn(console, 'log');

const counter = findComponent(fixture, 'app-counter');

const count = 5;

counter.triggerEventHandler('countChange', count);

expect(console.log).toHaveBeenCalledWith(

'countChange event from CounterComponent',

count,

);

});

So much for testing the CounterComponent child. The


HomeComponent also renders a ServiceCounterComponent
and an NgRxCounterComponent like this:

<app-service-counter></app-service-counter>

<!-- … -->

<app-ngrx-counter></app-ngrx-counter>

CHILD PRESENCE
Since they do not have Inputs or Outputs, we merely need to test
whether they are mentioned in the template. We add two
additional specs that check the presence of these app-
service-counter and app-ngrx-counter elements,
respectively.

it('renders a service counter', () => {

const serviceCounter = findComponent(fixture, 'app-service-


counter');

expect(serviceCounter).toBeTruthy();

});

it('renders a NgRx counter', () => {

const ngrxCounter = findComponent(fixture, 'app-ngrx-counter');

expect(ngrxCounter).toBeTruthy();

});

This is it! We have written a unit test with shallow rendering that
proves that HomeComponent correctly embeds several child
Components.

Note that this is one possible testing method. As always, it has


pros and cons. Compared with a full integration test, there is
little setup. The specs can use Angular’s DebugElement
abstraction to test presence as well as Inputs and Outputs.

UNIT TEST CONFIDENCE

However, the unit test gives little confidence that


HomeComponent works in production. We have instructed
Angular to ignore the elements app-counter, app-service-
counter and app-ngrx-counter.

What if HomeComponent uses a wrong element name and the


test copies that error? The test would pass incorrectly. We need
to render the involved Components together to spot the error.

🔗 HomeComponent: implementation and test code


🔗 Element spec helpers: full code

Faking a child Component


There is a middle ground between a naive unit test and an
integration test. Instead of working with empty custom
elements, we can render fake child Components.

A fake Component has the same selector, Inputs and Outputs,


but has no dependencies and does not have to render anything.
When testing a Component with children, we substitute the
children for fake Components.

Let us reduce the CounterComponent to an empty shell that


offers the same public API:

@Component({

selector: 'app-counter',

template: '',

})

class FakeCounterComponent implements Partial<CounterComponent> {

@Input()

public startCount = 0;

@Output()

public countChange = new EventEmitter<number>();

This fake Component lacks a template and any logic, but has the
same selector, Input and Output.

SAME PUBLIC API

Remember the rules for faking dependencies? We need to make


sure the fake resembles the original. FakeCounterComponent
implements Partial<CounterComponent> requires the class
to implement a subset of CounterComponent. TypeScript
enforces that the given properties and methods have the same
types as in the original class.

DECLARE FAKE COMPONENT

In our test suite, we place the FakeCounterComponent before


the describe block. The next step is to add the Component to
the testing Module:

TestBed.configureTestingModule({

declarations: [HomeComponent, FakeCounterComponent],

schemas: [NO_ERRORS_SCHEMA],

}).compileComponents();

When Angular encounters an app-counter element, it


instantiates and mounts a FakeCounterComponent. The
element stays empty since the fake template is empty as well.
The startCount Input property is set and the parent
HomeComponent subscribes to the countChange Output.

We need to adapt the test suite now that child Component are
rendered. Instead of searching for an app-counter element
and inspecting its properties, we explicitly search for a
FakeCounterComponent instance.

So far, we have used DebugElement’s query method to find


nested elements. For example:

const element = fixture.debugElement.query(By.css('…'));

Our helpers findEl and findComponent are using this pattern


as well.

FIND BY DIRECTIVE

Now we want to find a nested Component. We can use query


together with the By.directive predicate function:

const counterEl = fixture.debugElement.query(

By.directive(FakeCounterComponent)

);

By.directive finds all kinds of Directives. A Component is a


kind of Directive.
query returns a DebugElement or null in case no match was
found. As we have learned, a DebugElement always wraps a
native DOM element. When we query for
FakeCounterComponent, we get a DebugElement that wraps
the app-counter element – just as By.css('app-counter')
would return.

CHILD COMPONENT INSTANCE

The difference is that we can now access the rendered


FakeCounterComponent via the componentInstance property:

const counterEl = fixture.debugElement.query(

By.directive(FakeCounterComponent)

);

const counter: CounterComponent = counterEl.componentInstance;

Angular does not know the type of the Component,


componentInstance has the type any. So we add an explicit
type annotation.

CHILD PRESENCE

Having access to the child Component instance, we can make


expectations against it. First of all, we verify the presence.

it('renders an independent counter', () => {

const counterEl = fixture.debugElement.query(

By.directive(FakeCounterComponent)

);

const counter: CounterComponent = counterEl.componentInstance;

expect(counter).toBeTruthy();

});

This is a smoke test that fails early if no instance of


FakeCounterComponent was found. query would return null
and counterEl.componentInstance would fail with a
TypeError: counterEl is null.

CHECK INPUTS

The second spec checks the Input. An Input is a property of the


Component instance, so counter.startCount gives us the
value of the startCount Input.

it('passes a start count', () => {

const counterEl = fixture.debugElement.query(

By.directive(FakeCounterComponent)

);

const counter: CounterComponent = counterEl.componentInstance;

expect(counter.startCount).toBe(5);

});

The third spec checks the Output handling: If the counter emits a
value, the HomeComponent passes it to console.log.

EMIT OUTPUT

As mentioned earlier, an Output is an EventEmitter property


on the Component instance. Previously, we have simulated an
Output event using the triggerEventHandler abstraction.
Now we can access the Output directly and call its emit method,
just like the code in the child Component does.

it('listens for count changes', () => {

const counterEl = fixture.debugElement.query(

By.directive(FakeCounterComponent)

);

const counter: CounterComponent = counterEl.componentInstance;

spyOn(console, 'log');

const count = 5;

counter.countChange.emit(5);

expect(console.log).toHaveBeenCalledWith(

'countChange event from CounterComponent',

count,

);

});

We are done! Here is the HomeComponent test suite that vets the
CounterComponent child. To minimize repetition and noise, we
move the query part into the beforeEach block.

@Component({

selector: 'app-counter',

template: '',

})

class FakeCounterComponent implements Partial<CounterComponent> {

@Input()

public startCount = 0;

@Output()

public countChange = new EventEmitter<number>();

describe('HomeComponent (faking a child Component)', () => {

let fixture: ComponentFixture<HomeComponent>;

let component: HomeComponent;

let counter: FakeCounterComponent;

beforeEach(async () => {

await TestBed.configureTestingModule({

declarations: [HomeComponent, FakeCounterComponent],

schemas: [NO_ERRORS_SCHEMA],

}).compileComponents();

fixture = TestBed.createComponent(HomeComponent);

component = fixture.componentInstance;

fixture.detectChanges();

const counterEl = fixture.debugElement.query(

By.directive(FakeCounterComponent)

);

counter = counterEl.componentInstance;

});

it('renders an independent counter', () => {

expect(counter).toBeTruthy();

});

it('passes a start count', () => {

expect(counter.startCount).toBe(5);

});

it('listens for count changes', () => {

spyOn(console, 'log');

const count = 5;

counter.countChange.emit(count);

expect(console.log).toHaveBeenCalledWith(

'countChange event from CounterComponent',

count,

);

});

});

Let us recap what we have gained with this type of testing the
HomeComponent.

We have replaced a Component dependency with a fake that


behaves the same, as far as HomeComponent is concerned. The
fake child is rendered, but the template may be empty.

The original child Component, CounterComponent, is imported


only to create the derived fake Component. Our test remains a
fast and short unit test.

ADVANTAGES

Instead of searching for an element named app-counter, we


search for a Component instance. This is more robust. The
presence of the host element is a good indicator, but it is more
relevant that a Component has been rendered into this element.

Working with the Component instance is more intuitive than


working with the DebugElement abstraction. We can read
Component properties to learn about Inputs and Outputs. Basic
JavaScript and Angular knowledge suffices to write specs against
such an instance.

MANUAL FAKING DRAWBACKS

Our simple approach to faking a child Component has its flaws.


We have created the fake manually. This is tedious and time-
consuming, but also risky. The fake is only partly tied to the
original.

For example, if the original changes its selector app-counter,


the test should fail and remind us to adapt the template.
Instead, it passes incorrectly since we did not inherit the
Component metadata, { selector: 'app-counter', … },
but duplicated it in the test.

We are going to address these shortcomings in the next chapter.

🔗 HomeComponent spec that fakes a child Component

Faking a child Component with ng-


mocks
We have manually created a Component fake. This is an
important exercise to understand how faking Components
works, but it does not produce a robust, versatile fake. In this
guide, we cannot discuss all necessary bits and pieces of
creating airtight fake Components.

Instead, we will use a mature solution: ng-mocks is a feature-rich


library for
testing Components with fake dependencies.
(Remember, this guide uses the umbrella term “fake” while other
articles and tools use terms like “mock” or “stub”.)

CREATE FAKE FROM ORIGINAL

Among other things, ng-mocks helps creating fake Components


to substitute children. The MockComponent function expects the
original Component and returns a fake that resembles the
original.

Instead of creating a FakeCounterComponent, we call


MockComponent(CounterComponent) and add the fake to the
testing Module.

import { MockComponent } from 'ng-mocks';

beforeEach(async () => {

await TestBed.configureTestingModule({

declarations: [HomeComponent, MockComponent(CounterComponent)],

schemas: [NO_ERRORS_SCHEMA],

}).compileComponents();

});

We can then query the rendered DOM for an instance of


CounterComponent. The found instance is in fact a fake created
by ng-mocks.
Still, we can declare the type CounterComponent.

describe('HomeComponent with ng-mocks', () => {

let fixture: ComponentFixture<HomeComponent>;

let component: HomeComponent;

// Original type!

let counter: CounterComponent;

beforeEach(async () => {

await TestBed.configureTestingModule({

declarations: [HomeComponent,
MockComponent(CounterComponent)],

schemas: [NO_ERRORS_SCHEMA],

}).compileComponents();

fixture = TestBed.createComponent(HomeComponent);

component = fixture.componentInstance;

fixture.detectChanges();

const counterEl = fixture.debugElement.query(

// Original class!

By.directive(CounterComponent)

);

counter = counterEl.componentInstance;

});

/* … */

});

From a TypeScript viewpoint, the fake conforms to the


CounterComponent type. TypeScript uses a structural type
system that checks if all type requirements are met.
TYPE EQUIVALENCE

Every proposition that holds true for a CounterComponent


holds true for the fake as well. The fake has all properties and
methods that the original has. That is why we can safely replace
the original with the fake and treat the fake the same in our test.

The full code:

describe('HomeComponent with ng-mocks', () => {

let fixture: ComponentFixture<HomeComponent>;

let component: HomeComponent;

let counter: CounterComponent;

beforeEach(async () => {

await TestBed.configureTestingModule({

declarations: [HomeComponent, Mock(CounterComponent)],

schemas: [NO_ERRORS_SCHEMA],

}).compileComponents();

fixture = TestBed.createComponent(HomeComponent);

component = fixture.componentInstance;

fixture.detectChanges();

const counterEl = fixture.debugElement.query(

By.directive(CounterComponent)

);

counter = counterEl.componentInstance;

});

it('renders an independent counter', () => {

expect(counter).toBeTruthy();

});

it('passes a start count', () => {

expect(counter.startCount).toBe(5);

});

it('listens for count changes', () => {

spyOn(console, 'log');

const count = 5;

counter.countChange.emit(count);

expect(console.log).toHaveBeenCalledWith(

'countChange event from CounterComponent',

count,

);

});

});

We have eliminated the manual FakeCounterComponent. We


are using MockComponent(CounterComponent) to create the
fake and the original class CounterComponent. The specs itself
did not change.

This was only a glimpse of ng-mocks. The library not only helps
with nested Components, but provides high-level helpers for
setting up the Angular test environment. ng-mocks replaces the
conventional setup with TestBed.configureTestingModule
and helps faking Modules, Components, Directives, Pipes and
Services.

🔗 HomeComponent spec with ng-mocks


🔗 ng-mocks
Testing Components depending
on Services
LEARNING OBJECTIVES

Choosing between a unit or an integration test for Components that talk to


Services
Creating fake Services to test the Component in isolation
Verifying that the Component correctly interacts with the Service
Understanding different approaches for faking a Service dependency

We have successfully tested the independent


CounterComponent as well as the container HomeComponent.
The next Component on our list is the
ServiceCounterComponent.

As the name suggests, this Component depends on the


CounterService. The counter state is not stored in the
Component itself, but in the central Service.

SHARED CENTRAL STATE

Angular’s dependency injection maintains only one app-wide


instance of the Service, a so-called singleton. Therefore, multiple
instances of ServiceCounterComponent share the same
counter state. If the user increments the count with one
instance, the count also changes in the other instance.
Again, there are two fundamental ways to test the Component:

A unit test that replaces the CounterService dependency


with a fake.

An integration test that includes a real CounterService.

This guide will demonstrate both. For your Components, you


need to make a decision on an individual basis. These questions
may guide you: Which type of test is more beneficial, more
meaningful? Which test is easier to set up and maintain in the
long run?

Service dependency integration test


For the ServiceCounterComponent, the integration test is
much easier to set up than the unit test. The trivial
CounterService has little logic and no further dependencies. It
does not have side effects we need to suppress in the testing
environment, like HTTP requests. It only changes its internal
state.

The integration test looks almost identical to the


CounterComponent test we have already written.

describe('ServiceCounterComponent: integration test', () => {

let component: ServiceCounterComponent;

let fixture: ComponentFixture<ServiceCounterComponent>;

beforeEach(async () => {

await TestBed.configureTestingModule({

declarations: [ServiceCounterComponent],

providers: [CounterService],

}).compileComponents();

fixture = TestBed.createComponent(ServiceCounterComponent);

component = fixture.componentInstance;

fixture.detectChanges();

});

it('shows the start count', () => {

expectText(fixture, 'count', '0');

});

it('increments the count', () => {

click(fixture, 'increment-button');

fixture.detectChanges();

expectText(fixture, 'count', '1');

});

it('decrements the count', () => {

click(fixture, 'decrement-button');

fixture.detectChanges();

expectText(fixture, 'count', '-1');

});

it('resets the count', () => {

const newCount = 456;

setFieldValue(fixture, 'reset-input', String(newCount));

click(fixture, 'reset-button');

fixture.detectChanges();

expectText(fixture, 'count', String(newCount));

});

});

Compared to the CounterComponent test, there is nothing new


here except for one line:

providers: [CounterService],

PROVIDE SERVICE

This line adds the CounterService to the testing Module.


Angular creates an instance of the Service and injects it into the
Component under test. The test is shorter because the
ServiceCounterComponent does not have Inputs or Outputs
to test.

As the CounterService always starts with the count 0, the test


needs to take that for granted. Neither the Component nor the
Service allow a different start count.

INTERACTION WITH SERVICE

The integration test does not examine the Component’s inner


workings. It only provides the Service but does not check how
the Component and the Service interact. The Component might
not talk to the Service at all.

If we want an integration test to verify that the Component


stores the count in the Service, we would need a test with two
ServiceCounterComponents: When increasing the count using
one Component, the displayed count in the other should change
accordingly.

🔗 ServiceCounterComponent: integration test

Faking Service dependencies


Let us move on to the unit test for the
ServiceCounterComponent. To tackle this challenge, we need
to learn the art of faking Service dependencies.

There are several practical approaches with pros and cons. We


have discussed two main requirements on fake dependencies:

1. Equivalence of fake and original: The fake must have a type


derived from the original.

2. Effective faking: the original stays untouched.

RECOMMENDED FAKING APPROACH

This guide will present one solution that implements these


requirements. Note that other solutions might meet these
requirements as well.

The dependency we need to fake, CounterService, is a simple


class annotated with @Injectable(). 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 { /* … */ }

We need to build a fake that meets the mentioned needs.

FAKE INSTANCE

The simplest way to create a fake is an object literal {…} with


methods:

const currentCount = 123;

const fakeCounterService = {

getCount() {

return of(currentCount);

},

increment() {},

decrement() {},

reset() {},

};

getCount returns a fixed value from a constant named


currentCount. We will use the constant later to check whether
the Component uses the value correctly.
This fake 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.

TYPE EQUIVALENCE

The fake implementation above happens to have the same


shape as the original. As discussed, it is of utter importance that
the fake remains up to date with the original.

The equivalence is not yet enforced by TypeScript. We want


TypeScript to check whether the fake properly replicates the
original. The first attempt would be to add a type declaration:

ERRONEOUS CODE

// Error!

const fakeCounterService: CounterService = {

getCount() {

return of(currentCount);

},

increment() {},

decrement() {},

reset() {},

};

Unfortunately, this does not work. TypeScript complains that


private methods and properties are missing:

Type '{ getCount(): Observable<number>;


increment(): void; decrement(): void; reset():
void; }' is missing the following properties from
type 'CounterService': count, subject, notify

That is correct. But we cannot add private members to an object


literal, nor should we.

PICK PUBLIC MEMBERS

Luckily, we can use a TypeScript trick to fix this problem. Using


Pick and keyof, we create a derived type that only contains the
public members:

const fakeCounterService:

Pick<CounterService, keyof CounterService> = {

getCount() {

return of(currentCount);

},

increment() {},

decrement() {},

reset() {},

};

KEEP FAKE IN SYNC

When the CounterService changes its public API, the


dependent ServiceCounterComponent needs to be adapted.
Likewise, the fakeCounterService needs to reflect the change.
The type declaration reminds you to update the fake. It prevents
the fake to get out of sync with the original.

FAKE WHAT IS NECESSARY


ServiceCounterComponent calls all existing public
CounterService methods, so we have added them to the fake.

If the code under test does not use the full API, the fake does not
need to replicate the full API either. Only declare those methods
and properties the code under test actually uses.

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(currentCount);

},

};

Pick and other mapped types help to bind the fake to the
original type in a way that TypeScript can check the equivalence.

SPY ON METHODS

A plain object with methods is an easy way to create a fake


instance. The spec needs to verify that the methods have been
called with the right parameters.

Jasmine spies are suitable for this job. A first approach fills the
fake with standalone spies:
const fakeCounterService:

Pick<CounterService, keyof CounterService> = {

getCount:

jasmine.createSpy('getCount').and.returnValue(of(currentCount)),

increment: jasmine.createSpy('increment'),

decrement: jasmine.createSpy('decrement'),

reset: jasmine.createSpy('reset'),

};

CREATESPYOBJ

This is fine, but overly verbose. Jasmine provides a handy helper


function for creating an object with multiple spy methods,
createSpyObj. It expects a descriptive name and an object with
method names and return values:

const fakeCounterService = jasmine.createSpyObj<CounterService>(

'CounterService',

getCount: of(currentCount),

increment: undefined,

decrement: undefined,

reset: undefined,

);

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

TYPE EQUIVALENCE
createSpyObj accepts a TypeScript type variable to declare the
type of the created object. We pass CounterService between
angle brackets so TypeScript checks that the fake matches the
original.

Let us put our fake to work. In the Arrange phase, the fake is
created and injected into the testing Module.

describe('ServiceCounterComponent: unit test', () => {

const currentCount = 123;

let component: ServiceCounterComponent;

let fixture: ComponentFixture<ServiceCounterComponent>;

// Declare shared variable

let fakeCounterService: CounterService;

beforeEach(async () => {

// Create fake

fakeCounterService = jasmine.createSpyObj<CounterService>(

'CounterService',

getCount: of(currentCount),

increment: undefined,

decrement: undefined,

reset: undefined,

);

await TestBed.configureTestingModule({

declarations: [ServiceCounterComponent],

// Use fake instead of original

providers: [

{ provide: CounterService, useValue: fakeCounterService }

],

}).compileComponents();

fixture = TestBed.createComponent(ServiceCounterComponent);

component = fixture.componentInstance;

fixture.detectChanges();

});

/* … */

});

There is a new pattern in the providers sections of the testing


Module:

providers: [

{ provide: CounterService, useValue: fakeCounterService }

PROVIDE FAKE INSTEAD

This is the crucial moment where we tell Angular: For the


CounterService dependency, use the value
fakeCounterService instead. This is how we replace the
original with a fake.

Normally, Angular instantiates and injects a CounterService


instance whenever a Component, Service, etc. asks for the
CounterService. By using { provide: …, useValue: … },
we skip the instantiation and directly provide the value to inject.

The Arrange phase is complete now, let us write the actual specs.
The Act phase is the same as in the other counter Component
tests: We click on buttons and fill out form fields.

VERIFY SPIES

In the Assert phase, we need to verify that the Service methods


have been called. Thanks to jasmine.createSpyObj, all
methods of fakeCounterService are spies. We use expect
together with an appropriate matcher like toHaveBeenCalled,
toHaveBeenCalledWith, etc.

expect(fakeCounterService.getCount).toHaveBeenCalled();

Applied to all specs, the test suite looks like this:

describe('ServiceCounterComponent: unit test', () => {

const currentCount = 123;

let component: ServiceCounterComponent;

let fixture: ComponentFixture<ServiceCounterComponent>;

// Declare shared variable

let fakeCounterService: CounterService;

beforeEach(async () => {

// Create fake

fakeCounterService = jasmine.createSpyObj<CounterService>(

'CounterService',

getCount: of(currentCount),

increment: undefined,

decrement: undefined,

reset: undefined,

);

await TestBed.configureTestingModule({

declarations: [ServiceCounterComponent],

// Use fake instead of original

providers: [

{ provide: CounterService, useValue: fakeCounterService }

],

}).compileComponents();

fixture = TestBed.createComponent(ServiceCounterComponent);

component = fixture.componentInstance;

fixture.detectChanges();

});

it('shows the count', () => {

expectText(fixture, 'count', String(currentCount));

expect(fakeCounterService.getCount).toHaveBeenCalled();

});

it('increments the count', () => {

click(fixture, 'increment-button');

expect(fakeCounterService.increment).toHaveBeenCalled();

});

it('decrements the count', () => {

click(fixture, 'decrement-button');

expect(fakeCounterService.decrement).toHaveBeenCalled();

});

it('resets the count', () => {

const newCount = 456;

setFieldValue(fixture, 'reset-input', String(newCount));

click(fixture, 'reset-button');

expect(fakeCounterService.reset).toHaveBeenCalledWith(newCount);

});

});

🔗 ServiceCounterComponent: implementation and test code


🔗 Angular guide: Dependency providers

Fake Service with minimal logic


The specs above check whether user interaction calls the Service
methods. They do not check whether the Component re-renders
the new count after having called the Service.

ServiceCounter’s getCount method returns an


Observable<number> and pushes a new value through the
Observable whenever the count changes. The spec it('shows
the count', …) has proven that the Component obtained the
count from the Service and renders it.

In addition, we will check that the Component updates when


new values are pushed. This is not strictly necessary in our
simple ServiceCounterComponent and CounterService
example. But it is important in more complex interactions
between a Component and a Service.

COMPONENT UPDATE
The fake getCount method returns of(currentCount), an
Observable with the fixed value 123. The Observable completes
immediately and never pushes another value. We need to
change that behavior in order to demonstrate the Component
update.

The fake CounterService, devoid of logic so far, needs to gain


some logic. getCount needs to return an Observable that emits
new values when increment, decrement and reset are called.

BEHAVIORSUBJECT

Instead of a fixed Observable, we use a BehaviorSubject, just


like in the original CounterService implementation. The
BehaviorSubject has a next method for pushing new values.

We declare a variable fakeCount$ in the scope of the test suite


and assign a BehaviorSubject in the first beforeEach block:

describe('ServiceCounterComponent: unit test with minimal Service


logic', () => {

/* … */

let fakeCount$: BehaviorSubject<number>;

beforeEach(async () => {

fakeCount$ = new BehaviorSubject(0);

/* … */

});

/* … */

});

Then we change the fakeCounterService so the methods


push new values through fakeCount$.

const newCount = 123;

/* … */

fakeCounterService = {

getCount(): Observable<number> {

return fakeCount$;

},

increment(): void {

fakeCount$.next(1);

},

decrement(): void {

fakeCount$.next(-1);

},

reset(): void {

fakeCount$.next(Number(newCount));

},

};

The fake above is an object with plain methods. We are not using
createSpyObj any longer because it does not allow fake
method implementations.

SPY ON METHODS

We have lost the Jasmine spies and need to bring them back.
There are several ways to wrap the methods in spies. For
simplicity, we install spies on all methods using spyOn:

spyOn(fakeCounterService, 'getCount').and.callThrough();

spyOn(fakeCounterService, 'increment').and.callThrough();

spyOn(fakeCounterService, 'decrement').and.callThrough();

spyOn(fakeCounterService, 'reset').and.callThrough();

Remember to add .and.callThrough() so the underlying fake


methods are called.

Now our fake Service sends new counts to the Component. We


can reintroduce the checks for the Component output:

fixture.detectChanges();

expectText(fixture, 'count', '…');

Assembling all parts, the full ServiceCounterComponent unit


test:

describe('ServiceCounterComponent: unit test with minimal Service


logic', () => {

const newCount = 456;

let component: ServiceCounterComponent;

let fixture: ComponentFixture<ServiceCounterComponent>;

let fakeCount$: BehaviorSubject<number>;

let fakeCounterService: Pick<CounterService, keyof


CounterService>;

beforeEach(async () => {

fakeCount$ = new BehaviorSubject(0);

fakeCounterService = {

getCount(): Observable<number> {

return fakeCount$;

},

increment(): void {

fakeCount$.next(1);

},

decrement(): void {

fakeCount$.next(-1);

},

reset(): void {

fakeCount$.next(Number(newCount));

},

};

spyOn(fakeCounterService, 'getCount').and.callThrough();

spyOn(fakeCounterService, 'increment').and.callThrough();

spyOn(fakeCounterService, 'decrement').and.callThrough();

spyOn(fakeCounterService, 'reset').and.callThrough();

await TestBed.configureTestingModule({

declarations: [ServiceCounterComponent],

providers: [

{ provide: CounterService, useValue: fakeCounterService }

],

}).compileComponents();

fixture = TestBed.createComponent(ServiceCounterComponent);

component = fixture.componentInstance;

fixture.detectChanges();

});

it('shows the start count', () => {

expectText(fixture, 'count', '0');

expect(fakeCounterService.getCount).toHaveBeenCalled();

});

it('increments the count', () => {

click(fixture, 'increment-button');

fakeCount$.next(1);

fixture.detectChanges();

expectText(fixture, 'count', '1');

expect(fakeCounterService.increment).toHaveBeenCalled();

});

it('decrements the count', () => {

click(fixture, 'decrement-button');

fakeCount$.next(-1);

fixture.detectChanges();

expectText(fixture, 'count', '-1');

expect(fakeCounterService.decrement).toHaveBeenCalled();

});

it('resets the count', () => {

setFieldValue(fixture, 'reset-input', newCount);

click(fixture, 'reset-button');

fixture.detectChanges();

expectText(fixture, 'count', newCount);

expect(fakeCounterService.reset).toHaveBeenCalledWith(newCount);

});

});

Again, this example is intentionally verbose. The fake re-


implements a large part of the original logic. This is because the
original CounterService has little logic itself.

In reality, Services are more complex and Components process


the data they receive from the Services. Then, the effort of faking
essential logic is worthwhile.

🔗 ServiceCounterComponent: unit test

Faking Services: Summary


Creating fake Service dependencies and verifying their usage is
one of the most challenging problems when testing Angular
applications. This guide can only catch a glimpse on the subject.

TESTABLE SERVICES

Faking Services requires effort and steady practice. The more


unit tests you write, the more experience you gain. More
importantly, the practice teaches you to write simple Services that
are easy to fake: Services with a clear API and an obvious
purpose.

Unfortunately, there are no best practices when it comes to


faking Services. You will find plenty of approaches online that
have their strengths and weaknesses. The associated unit tests
have different degrees of accuracy and completeness.

Arguing about the “right” way of faking a Service is pointless. You


need to decide on a faking method that suits the Service on a
case-by-case basis.
GUIDELINES

There are two guidelines that may help you:

1. Is the test valuable? Does it cover the important interaction


between Component and Service? Decide whether to test the
interaction superficially or in-depth.

2. Whichever approach you choose, make sure to meet the


basic requirements:

1. Equivalence of fake and original: The fake must have a


type derived from the original.

2. Effective faking: the original stays untouched.


Testing complex forms
LEARNING OBJECTIVES

Filling out and submitting forms in a Component test


Testing synchronous and asynchronous field validation and error messages
Testing dynamic form logic
Using tools to ensure that forms are accessible to everyone

Forms are the powerhouses of large web applications. Especially


enterprise applications revolve around entering and editing data
via forms. Therefore, implementing complex forms is a vital
feature of the Angular framework.

We have already learned how to fill out form fields when testing
the counter Component. In doing so, we developed the
setFieldValue testing helper.

The simple forms we have dealt with served the purpose of


entering one value. We have tested them by filling out the field
and submitting the form. Now we will look at a more complex
example.

SIGN-UP FORM

We are introducing and testing a sign-up form for a fictional


online service.

🔗 Sign-up form: Source code


🔗 Sign-up form: Run the app
The sign-up form features are:

Different types of input fields: text, radio buttons,


checkboxes, select boxes

Field validation with synchronous and asynchronous


validators

Accessible form structure, field labels and error messages

Dynamic relations between fields

The form consists of four sections:

1. The plan selection: “Personal”, “Business” or “Education &


Non-profit”

2. The login credentials: username, email and password

3. The billing address

4. Terms of Services and submit button

IMPRACTICAL

Please note that this form is for demonstration purposes only.


While it follows best practices regarding validation and
accessibility, it is not practical from a design and user experience
perspective. Among other things, it is way too complex to get
new users onboard.
CLIENT & SERVER

In contrast to the other example repositories, this one is split


into a client and a server directory:

The client directory contains a standard Angular app


created with Angular CLI.

The server directory contains a simple Node.js service that


simulates the user management and account creation.

Again, the Node.js service is for demonstration purposes only.


The service holds the created user accounts in memory and
discards them when stopped. Please do not use it in production.

With 12 form controls, the sign-up form is not particularly large.


But there are subtle details we are going to explore.

Sign-up form Component


The form logic lies in the SignupFormComponent. The
Component depends on the SignupService for communicating
with the back-end service.

You might remember that there are two fundamental


approaches to forms in Angular: Template-driven Forms and
Reactive Forms.
While both approaches look quite different in practice, they are
based on the same underlying concepts: Form groups
(FormGroup objects) and form controls (FormControl objects).

REACTIVE FORM

The SignupFormComponent is a Reactive Form that explictly


creates the groups and controls in the Component class. This
way, it is easier to specify custom validators and to set up
dynamic field relations.

As with other Angular core concepts, this guide assumes you


have a basic understanding of Reactive Forms. Please refer to
the official guide on Reactive Forms to brush up your knowledge.

The important bits of the SignupFormComponent class are:

@Component({

selector: 'app-signup-form',

templateUrl: './signup-form.component.html',

styleUrls: ['./signup-form.component.scss'],

})

export class SignupFormComponent {

/* … */

public form = this.formBuilder.group({

plan: ['personal', required],

username: [

null,

[required, pattern('[a-zA-Z0-9.]+'), maxLength(50)],

(control: AbstractControl) =>

this.validateUsername(control.value),

],

email: [

null,

[required, email, maxLength(100)],

(control: AbstractControl) =>

this.validateEmail(control.value),

],

password: [

null,

required,

() => this.validatePassword()

],

tos: [null, requiredTrue],

address: this.formBuilder.group({

name: [null, required],

addressLine1: [null],

addressLine2: [null, required],

city: [null, required],

postcode: [null, required],

region: [null],

country: [null, required],

}),

});

/* … */

constructor(

private signupService: SignupService,

private formBuilder: FormBuilder) {

/* … */

/* … */

FORM GROUPS AND CONTROLS


Using Angular’s FormBuilder, we create the form property, the
topmost form group. Inside, there is another form group for the
address-related fields.

The form controls are declared with their initial values and their
validators. For example, the password control:

password: [

// The initial value (null means empty)

null,

// The synchronous validator

required,

// The asynchronous validator

() => this.validatePassword()

],

The SignupFormComponent template uses the formGroup,


formGroupName and formControlName directives to associate
elements with a form group or control, respectively.

The stripped-down form structure with only one control looks


like this:

<form [formGroup]="form">

<fieldset formGroupName="address">

<label>

Full name

<input type="text" formControlName="name" />

</label>

</fieldset>

</form>

FORM SUBMISSION

When the form is filled out correctly and all validations pass, the
user is able to submit to the form. It produces an object
described by the SignupData interface:

export interface SignupData {

plan: Plan;

username: string;

email: string;

password: string;

tos: true;

address: {

name: string;

addressLine1?: string;

addressLine2: string;

city: string;

postcode: string;

region?: string;

country: string;

};

Plan is a union of strings:

export type Plan = 'personal' | 'business' | 'non-profit';

The SignupService’s signup method takes the SignupData


and sends it to the server. For security reasons, the server
validates the data again. But we will focus on the front-end in
this guide.

🔗 SignupFormComponent: full code


🔗 Angular documentation: Reactive forms

Form validation and errors


SYNC VALIDATORS

Several form controls have synchronous validators. required,


email, maxLength, pattern etc. are built-in, synchronous
validators provided by Angular:

import { Validators } from '@angular/forms';

const {

email, maxLength, pattern, required, requiredTrue

} = Validators;

These validators take the control value, a string most of the time,
and return a ValidationErrors object with potential error
messages. The validation happens synchronously on the client.

ASYNC VALIDATORS

For the username, the email and the password, there are custom
asynchronous validators. They check whether the username and
email are available and whether the password is strong enough.

The asynchronous validators use the SignupService to talk to


the back-end service. These HTTP requests turn the validation
asynchronous.
ERROR RENDERING

When a validator returns any errors, corresponding messages


are shown below the form control. This repetitive task is
outsourced to another Component.

INVALID && (TOUCHED || DIRTY)

The ControlErrorsComponent displays the errors when the


form control is invalid and either touched or dirty.

Touched means the user has focussed the control but it has
lost the focus again (the blur event fired).

Dirty means the user has changed the value.

For example, for the name control, the interaction between the
input element and the ControlErrorsComponent looks like
this:

<label>

Full name

<input

type="text"

formControlName="name"

aria-required="true"

appErrorMessage="name-errors"

/>

</label>

<!-- … -->

<app-control-errors controlName="name" id="name-errors">

<ng-template let-errors>

<ng-container *ngIf="errors.required">

Name must be given.

</ng-container>

</ng-template>

</app-control-errors>

ARIA ATTRIBUTES

The appErrorMessage attribute activates the


ErrorMessageDirective. When the form control is invalid and
either touched or dirty, the Directive adds aria-invalid and
aria-errormessage attributes.

aria-invalid marks the control as invalid for assistive


technologies like screen readers. aria-errormessage points to
another element that contains the error messages.

CONNECT CONTROL WITH ERRORS

In case of an error, the Directive sets aria-errormessage to


the id of the corresponding app-control-errors element. In
the example above, the id is name-errors. This way, a screen
reader user finds the associated error messages quickly.

The control-specific error messages are still located in signup-


form.component.html. They are passed to
ControlErrorsComponent as an ng-template. The
ControlErrorsComponent renders the template dynamically,
passing the errors object as a variable:
<ng-template let-errors>

<ng-container *ngIf="errors.required">

Name must be given.

</ng-container>

</ng-template>

You do not have to understand the details of this particular


implementation. The solution in the sign-up form is just one
possibility to display errors, avoid repetition and set ARIA
attributes for accessibility.

From the user perspective and also from a testing perspective, it


does not matter how you implement the rendering of error
messages – as long as they are present and accessible.

IMPLEMENTATION DETAILS

We are going to test the SignupFormComponent in conjunction


with ControlErrorsComponent and ErrorMessageDirective
in a black-box integration test. For this test, the latter two will
be irrelevant implementation details.

🔗 Angular guide: Validating form input


🔗 MDN: Introduction to ARIA
🔗 MDN: aria-invalid
🔗 ARIA specification: aria-errormessage
🔗 ErrorMessageDirective: full code
🔗 ControlErrorsComponent: full code
Test plan
What are the important parts of the sign-up form that need to be
tested?

1. Form submission

Successful submission

Do not submit the invalid form

Submission failure

2. Required fields are marked as such and display error


messages

3. Asynchronous validation of username, email and password

4. Dynamic field relations

5. Password type toggle

6. Accessibility of the form structure, field labels and error


messages

Test setup
Before writing the individual specs, we need to set up the suite
in signup-form.component.spec.ts. Let us start with the
testing Module configuration.

await TestBed.configureTestingModule({

imports: [ReactiveFormsModule],

declarations: [

SignupFormComponent,

ControlErrorsComponent,

ErrorMessageDirective

],

providers: [

{ provide: SignupService, useValue: signupService }

],

}).compileComponents();

The Component under test contains a Reactive Form. That is why


we import the ReactiveFormsModule:

imports: [ReactiveFormsModule],

DEEP RENDERING

As described, we are writing an integration test, so we declare


the Component and its child components:

declarations: [

SignupFormComponent,

ControlErrorsComponent,

ErrorMessageDirective

],

FAKE SERVICE
The SignupFormComponent depends on the SignupService.
We do not want HTTP requests to the back-end when the tests
run, so we replace the Service with a fake instance.

providers: [

{ provide: SignupService, useValue: signupService }

],

A possible SignupService fake looks like this:

const signupService:

Pick<SignupService, keyof SignupService> = {

isUsernameTaken() {

return of(false);

},

isEmailTaken() {

return of(false);

},

getPasswordStrength() {

return of(strongPassword);

},

signup() {

return of({ success: true });

},

};

This fake implements the success case: the username and email
are available, the password is strong enough and the form
submission was successful.

Since we are going to test several error cases as well, we need to


create SignupService fakes dynamically. Also we need Jasmine
spies to verify that the Service methods are called correctly.

CREATESPYOBJ

This is a job for Jasmine’s createSpyObj (see Faking Service


dependencies).

const signupService = jasmine.createSpyObj<SignupService>(

'SignupService',

// Successful responses per default

isUsernameTaken: of(false),

isEmailTaken: of(false),

getPasswordStrength: of(strongPassword),

signup: of({ success: true }),

);

SETUP FUNCTION

Together with the testing Module configuration, we put this code


into a setup function. To adjust the SignupService fake
behavior, we allow passing method return values.

describe('SignupFormComponent', () => {

let fixture: ComponentFixture<SignupFormComponent>;

let signupService: jasmine.SpyObj<SignupService>;

const setup = async (

signupServiceReturnValues?:

jasmine.SpyObjMethodNames<SignupService>,

) => {

signupService = jasmine.createSpyObj<SignupService>(

'SignupService',

// Successful responses per default

isUsernameTaken: of(false),

isEmailTaken: of(false),

getPasswordStrength: of(strongPassword),

signup: of({ success: true }),

// Overwrite with given return values

...signupServiceReturnValues,

);

await TestBed.configureTestingModule({

imports: [ReactiveFormsModule],

declarations: [

SignupFormComponent,

ControlErrorsComponent,

ErrorMessageDirective

],

providers: [

{ provide: SignupService, useValue: signupService }

],

}).compileComponents();

fixture = TestBed.createComponent(SignupFormComponent);

fixture.detectChanges();

};

/* … */

});

In all following specs, we are going to call setup first. If we


simply write async setup(), the SignupService fake returns
successful responses.

We can pass an object with different return values to simulate


failure. For example, when testing that the username is taken:

await setup({

// Let the API return that the username is taken

isUsernameTaken: of(true),

});

Such a setup function is just one way to create fakes and avoid
repetition. You might come up with a different solution that
serves the same purpose.

🔗 SignupFormComponent: test code

Successful form submission


The first case we need to test is the successful form submission.
If the user fills out all required fields and the validations pass, we
expect the Component to call SignupService’s signup method
with the entered form data.

TEST DATA

The first step is to define valid test data we can fill into the form.
We put this in a separate file, signup-data.spec-helper.ts:
export const username = 'quickBrownFox';

export const password = 'dog lazy the over jumps fox brown quick
the';

export const email = '[email protected]';

export const name = 'Mr. Fox';

export const addressLine1 = '';

export const addressLine2 = 'Under the Tree 1';

export const city = 'Farmtown';

export const postcode = '123456';

export const region = 'Upper South';

export const country = 'Luggnagg';

export const signupData: SignupData = {

plan: 'personal',

username,

email,

password,

address: {

name, addressLine1, addressLine2,

city, postcode, region, country

},

tos: true,

};

In the signup-form.component.html template, all field elements


need to be marked with test ids so we can find them and enter
the values programmatically.

For example, the username input gets the test id username, the
email input gets email and so on.
Back in signup-form.component.spec.ts, we create a new
spec that calls the setup function.

it('submits the form successfully', async () => {

await setup();

/* … */

});

FILL OUT FORM

Next, we fill out all required fields with valid values. Since we
need to do that in several upcoming specs, let us create a
reusable function.

const fillForm = () => {

setFieldValue(fixture, 'username', username);

setFieldValue(fixture, 'email', email);

setFieldValue(fixture, 'password', password);

setFieldValue(fixture, 'name', name);

setFieldValue(fixture, 'addressLine1', addressLine1);

setFieldValue(fixture, 'addressLine2', addressLine2);

setFieldValue(fixture, 'city', city);

setFieldValue(fixture, 'postcode', postcode);

setFieldValue(fixture, 'region', region);

setFieldValue(fixture, 'country', country);

checkField(fixture, 'tos', true);

};

The fillForm function lies in the scope of describe so it may


access the fixture variable. It uses the setFieldValue and
checkField element testing helpers.
In the spec, we call fillForm:

it('submits the form successfully', async () => {

await setup();

fillForm();

/* … */

});

Let us try to submit the form immediately after. The form under
test listens for an ngSubmit event at the form element. This
boils down to a native submit event.

SUBMIT FORM

We find the form element by its test id and simulate a submit


event (see Triggering event handlers).

Then we expect the signup spy to have been called with the
entered data.

it('submits the form successfully', async () => {

await setup();

fillForm();

findEl(fixture, 'form').triggerEventHandler('submit', {});

expect(signupService.signup).toHaveBeenCalledWith(signupData);

});

If we run this spec, we find that is fails:

Expected spy SignupService.signup to have been called with:

[ Object({ plan: 'personal', … }) ]

but it was never called.

The spec fails because the form is still in the invalid state even
though we have filled out all fields correctly.

ASYNC VALIDATORS

The cause are the asynchronous validators for username,


email and password. When the user stops typing into these
fields, they wait for one second before sending a request to the
server.

In production, the HTTP request takes additional time, but our


fake SignupService returns the response instantly.

ONE SECOND DEBOUNCE

This technique to reduce the amount of requests is called


debouncing. For example, typing the username “fox” should send
one request with “fox”, not three subsequent requests with “f”,
“fo”, “fox”.

The spec above submits the form immediately after filling out
the fields. At this point in time, the asynchronous validators have
been called but have not returned a value yet. They are still
waiting for the debounce period to pass.
In consequence, the test needs to wait one second for the
asynchronous validators. An easy way would be to write an
asynchronous test that uses setTimeout(() => { /* … */},
1000). But this would slow down our specs.

FAKEASYNC AND TICK

Instead, we are going to use Angular’s fakeAsync and tick


functions to simulate the passage of time. They are a powerful
couple to test asynchronous behavior.

fakeAsync freezes time. It hooks into the processing of


asynchronous tasks created by timers, intervals, Promises and
Observables. It prevents these tasks from being executed.

SIMULATE PASSAGE OF TIME

Inside the time warp created by fakeAsync, we use the tick


function to simulate the passage of time. The scheduled tasks
are executed and we can test their effect.

The specialty of fakeAsync and tick is that the passage of time


is only virtual. Even if one second passes in the simulation, the
spec still completes in a few milliseconds.

fakeAsync wraps the spec function which is also an async


function due to the setup call. After filling out the form, we
simulate the waiting with tick(1000).
it('submits the form successfully', fakeAsync(async () => {

await setup();

fillForm();

// Wait for async validators

tick(1000);

findEl(fixture, 'form').triggerEventHandler('submit', {});

expect(signupService.signup).toHaveBeenCalledWith(signupData);

}));

This spec passes! Now we should add some expectations to test


the details.

First, we expect the asynchronous validators to call the


SignupService methods with the user input. The methods are
isUsernameTaken, isEmailTaken and
getPasswordStrength.

it('submits the form successfully', fakeAsync(async () => {

await setup();

fillForm();

// Wait for async validators

tick(1000);

findEl(fixture, 'form').triggerEventHandler('submit', {});

expect(signupService.isUsernameTaken).toHaveBeenCalledWith(username
);

expect(signupService.isEmailTaken).toHaveBeenCalledWith(email);

expect(signupService.getPasswordStrength).toHaveBeenCalledWith(pass
word);

expect(signupService.signup).toHaveBeenCalledWith(signupData);

}));

SUBMIT BUTTON

Next, we make sure that the submit button is disabled initially.


After successful validation, the button is enabled. (The submit
button carries the test id submit.)

STATUS MESSAGE

Also, when the form has been submitted successfully, the status
message “Sign-up successful!” needs to appear. (The status
message carries the test id status.)

This brings us to the final spec:

it('submits the form successfully', fakeAsync(async () => {

await setup();

fillForm();

fixture.detectChanges();

expect(findEl(fixture, 'submit').properties.disabled).toBe(true);

// Wait for async validators

tick(1000);

fixture.detectChanges();

expect(findEl(fixture,
'submit').properties.disabled).toBe(false);

findEl(fixture, 'form').triggerEventHandler('submit', {});

fixture.detectChanges();

expectText(fixture, 'status', 'Sign-up successful!');

expect(signupService.isUsernameTaken).toHaveBeenCalledWith(username
);

expect(signupService.isEmailTaken).toHaveBeenCalledWith(email);

expect(signupService.getPasswordStrength).toHaveBeenCalledWith(pass
word);

expect(signupService.signup).toHaveBeenCalledWith(signupData);

}));

Because we are testing DOM changes, we have to call


detectChanges after each Act phase.

🔗 SignupFormComponent: test code


🔗 Angular API reference: fakeAsync
🔗 Angular API reference: tick

Invalid form
Now that we have tested the successful form submission, let us
check the handling of an invalid form. What happens if we do
not fill out any fields, but submit the form?

We create a new spec for this case:

it('does not submit an invalid form', fakeAsync(async () => {

await setup();

// Wait for async validators

tick(1000);

findEl(fixture, 'form').triggerEventHandler('submit', {});

expect(signupService.isUsernameTaken).not.toHaveBeenCalled();

expect(signupService.isEmailTaken).not.toHaveBeenCalled();

expect(signupService.getPasswordStrength).not.toHaveBeenCalled();

expect(signupService.signup).not.toHaveBeenCalled();

}));

This spec does less than the previous. We wait for a second and
submit the form without entering data. Finally, we expect that no
SignupService method has been called.

🔗 SignupFormComponent: test code

Form submission failure


We have already tested the successful form submission. Now let
us test the form submission failure.

REASONS FOR FAILURE


Despite correct input, the submission may fail for several
reasons:

The network is unavailable

The back-end processed the request but returned an error:

The server-side validation failed

The request structure is not as expected

The server code has bugs, has crashed or is frozen

OBSERVABLE

When the user submits the form, the Component under tests
calls the SignupService’s signup method.

In the success case, the signup method returns an


Observable that emits the value { success: true } and
completes. The form displays a status message “Sign-up
successful!”.

In the error case, the Observable fails with an error. The form
displays a status message “Sign-up error”.

Let us test the latter case in a new spec. The structure resembles
the spec for the successful submission. But we configure the
fake signup method to return an Observable that fails with an
error.

import { throwError } from 'rxjs';

it('handles signup failure', fakeAsync(async () => {

await setup({

// Let the API report a failure

signup: throwError(new Error('Validation failed')),

});

/* … */

});

We fill out the form, wait for the validators and submit the form.

fillForm();

// Wait for async validators

tick(1000);

findEl(fixture, 'form').triggerEventHandler('submit', {});

fixture.detectChanges();

STATUS MESSAGE

Finally, we expect the “Sign-up error” status message to appear.


Also, we verify that the relevant SignupService methods have
been called.

expectText(fixture, 'status', 'Sign-up error');

expect(signupService.isUsernameTaken).toHaveBeenCalledWith(username
);

expect(signupService.getPasswordStrength).toHaveBeenCalledWith(pass
word);

expect(signupService.signup).toHaveBeenCalledWith(signupData);

The full spec:

it('handles signup failure', fakeAsync(async () => {

await setup({

// Let the API report a failure

signup: throwError(new Error('Validation failed')),

});

fillForm();

// Wait for async validators

tick(1000);

findEl(fixture, 'form').triggerEventHandler('submit', {});

fixture.detectChanges();

expectText(fixture, 'status', 'Sign-up error');

expect(signupService.isUsernameTaken).toHaveBeenCalledWith(username
);

expect(signupService.getPasswordStrength).toHaveBeenCalledWith(pass
word);

expect(signupService.signup).toHaveBeenCalledWith(signupData);

}));

🔗 SignupFormComponent: test code


Required fields
A vital form logic is that certain fields are required and that the
user interface conveys the fact clearly. Let us write a spec that
checks whether required fields as marked as such.

REQUIREMENTS

The requirements are:

A required field has an aria-required attribute.

A required, invalid field has an aria-errormessage


attribute. It contains the id of another element.

This element contains an error message “… must be given”.


(The text for the Terms of Services checkbox reads “Please
accept the Terms and Services” instead.)

Our spec needs to verify all required fields, so we compile a list


of their respective test ids:

const requiredFields = [

'username',

'email',

'name',

'addressLine2',

'city',

'postcode',

'country',

'tos',

];

INVALID && (TOUCHED ||  DIRTY)

Before examining the fields, we need to trigger the display of


form errors. As described in Form validation and errors, error
messages are shown when the field is invalid and either touched
or dirty.

Luckily, the empty, but required fields are already invalid.


Entering text would make them dirty but also valid.

MARK AS TOUCHED

So we need to touch the fields. If a field is focussed and loses


focus again, Angular considers it as touched. Under the hood,
Angular listens for the blur event.

In our spec, we simulate a blur event using the


dispatchFakeEvent testing helper. Let us put the call in a
reusable function:

const markFieldAsTouched = (element: DebugElement) => {

dispatchFakeEvent(element.nativeElement, 'blur');

};

We can now write the Arrange and Act phases of the spec:

it('marks fields as required', async () => {

await setup();

// Mark required fields as touched

requiredFields.forEach((testId) => {

markFieldAsTouched(findEl(fixture, testId));

});

fixture.detectChanges();

/* … */

});

A forEach loop walks through the required field test ids, finds
the element and marks the field as touched. We call
detectChanges afterwards so the error messages appear.

ARIA-REQUIRED

Next, the Assert phase. Again we walk through the required fields
to examine each one of them. Let us start with the aria-
required attribute.

requiredFields.forEach((testId) => {

const el = findEl(fixture, testId);

// Check aria-required attribute

expect(el.attributes['aria-required']).toBe(

'true',

`${testId} must be marked as aria-required`,

);

/* … */

});

findEl returns a DebugElement with an attributes property.


This object contains all attributes set by the template. We expect
the attribute aria-required="true" to be present.

ARIA-ERRORMESSAGE

The next part tests the error message with three steps:

1. Read the aria-errormessage attribute. Expect that it is set.

2. Find the element that aria-errormessage refers to. Expect


that it exists.

3. Read the text content. Expect an error message.

Step 1 looks like this:

// Check aria-errormessage attribute

const errormessageId = el.attributes['aria-errormessage'];

if (!errormessageId) {

throw new Error(`Error message id for ${testId} not present`);

Normally, we would use a Jasmine expectation like


expect(errormessageId).toBeDefined(). But
errormessageId has the type string | null whereas we
need a string in the upcoming commands.

TYPE ASSERTIONS
We need a TypeScript type assertion that rules out the null case
and narrows down the type to string. If the attribute is absent
or empty, we throw an exception. This fails the test with the
given error and ensures that errormessageId is a string for the
rest of the spec.

Step 2 finds the error message element:

// Check element with error message

const errormessageEl = document.getElementById(errormessageId);

if (!errormessageEl) {

throw new Error(`Error message element for ${testId} not found`);

We use the native DOM method document.getElementById to


find the element. errormessageEl has the type HTMLElement
| null, so we rule out the null case to work with
errormessageEl.

ERROR MESSAGE

Finally, we ensure that the element contains an error message,


with a special treatment of the Terms and Services message.

if (errormessageId === 'tos-errors') {

expect(errormessageEl.textContent).toContain(

'Please accept the Terms and Services',

);

} else {

expect(errormessageEl.textContent).toContain('must be given');

The full spec looks like this:

it('marks fields as required', async () => {

await setup();

// Mark required fields as touched

requiredFields.forEach((testId) => {

markFieldAsTouched(findEl(fixture, testId));

});

fixture.detectChanges();

requiredFields.forEach((testId) => {

const el = findEl(fixture, testId);

// Check aria-required attribute

expect(el.attributes['aria-required']).toBe(

'true',

`${testId} must be marked as aria-required`,

);

// Check aria-errormessage attribute

const errormessageId = el.attributes['aria-errormessage'];

if (!errormessageId) {

throw new Error(

`Error message id for ${testId} not present`

);

// Check element with error message

const errormessageEl = document.getElementById(errormessageId);

if (!errormessageEl) {

throw new Error(

`Error message element for ${testId} not found`

);

if (errormessageId === 'tos-errors') {

expect(errormessageEl.textContent).toContain(

'Please accept the Terms and Services',

);

} else {

expect(errormessageEl.textContent).toContain('must be
given');

});

});

🔗 SignupFormComponent: test code

Asynchronous validators
The sign-up form features asynchronous validators for
username, email and password. They are asynchronous because
they wait for a second and make an HTTP request. Under the
hood, they are implemented using RxJS Observables.

ASYNC VALIDATION FAILURE

We have already covered the “happy path” in which the entered


username and email are available and the password is strong
enough. We need to write three specs for the error cases:
Username or email are taken and the password is too weak.

The validators call SignupService methods. Per default, the


SignupService fake returns successful responses.
const setup = async (

signupServiceReturnValues?:

jasmine.SpyObjMethodNames<SignupService>,

) => {

signupService = jasmine.createSpyObj<SignupService>(

'SignupService',

// Successful responses per default

isUsernameTaken: of(false),

isEmailTaken: of(false),

getPasswordStrength: of(strongPassword),

signup: of({ success: true }),

// Overwrite with given return values

...signupServiceReturnValues,

);

/* … */

};

The setup function allows us to overwrite the fake behavior. It


accepts an object with SignupService method return values.

We add three specs that configure the fake accordingly:

it('fails if the username is taken', fakeAsync(async () => {

await setup({

// Let the API return that the username is taken

isUsernameTaken: of(true),

});

/* … */

}));

it('fails if the email is taken', fakeAsync(async () => {

await setup({

// Let the API return that the email is taken

isEmailTaken: of(true),

});

/* … */

}));

it('fails if the password is too weak', fakeAsync(async () => {

await setup({

// Let the API return that the password is weak

getPasswordStrength: of(weakPassword),

});

/* … */

}));

The rest is the same for all three specs. Here is the first spec:

it('fails if the username is taken', fakeAsync(async () => {

await setup({

// Let the API return that the username is taken

isUsernameTaken: of(true),

});

fillForm();

// Wait for async validators

tick(1000);

fixture.detectChanges();

expect(findEl(fixture, 'submit').properties.disabled).toBe(true);

findEl(fixture, 'form').triggerEventHandler('submit', {});

expect(signupService.isUsernameTaken).toHaveBeenCalledWith(username
);

expect(signupService.isEmailTaken).toHaveBeenCalledWith(email);

expect(signupService.getPasswordStrength).toHaveBeenCalledWith(pass
word);

expect(signupService.signup).not.toHaveBeenCalled();

}));

We fill out the form, wait for the async validators and try to
submit the form.

We expect that the three async validators call the respective


SignupService methods.

The username validation fails, so we expect that the Component


prevents the form submission. The signup method must not be
called.

As stated above, the two other specs it('fails if the


email is taken', /* … */) and it('fails if the
password is too weak', /* … */) look the same apart
from the fake setup.

🔗 SignupFormComponent: test code


🔗 Angular documentation: Creating asynchronous validators

Dynamic field relations


The sign-up form has a fixed set of fields. But the addressLine1
field depends on the value of the plan field:

If the selected plan is “Personal”, the field is optional and the


label reads “Address line 1”.

If the selected plan is “Business”, the field is required and the


label reads “Company”.

If the selected plan is “Education & Non-profit”, the field is


required and the label reads “Organization”.

The implementation in the Component class looks like this:

this.plan.valueChanges.subscribe((plan: Plan) => {

if (plan !== this.PERSONAL) {

this.addressLine1.setValidators(required);

} else {

this.addressLine1.setValidators(null);

this.addressLine1.updateValueAndValidity();

});

We listen for value changes of the plan control. Depending on


the selection, we either add the required validator to the
addressLine1 control or remove all validators.

Finally, we need to tell Angular to revalidate the field value, now


that the validators have changed.
Let us write a spec to ensure that addressLine1 is required for
certain plans.

it('requires address line 1 for business and non-profit plans',


async () => {

await setup();

/* … */

});

First, we need inspect the initial state: The “Personal” plan is


selected and addressLine1 is optional.

We do so by looking at attributes of the addressLine1 field


element: The ng-invalid class and the aria-required
attribute must be absent.

// Initial state (personal plan)

const addressLine1El = findEl(fixture, 'addressLine1');

expect('ng-invalid' in addressLine1El.classes).toBe(false);

expect('aria-required' in addressLine1El.attributes).toBe(false);

From this baseline, let us change the plan from “Personal” to


“Business”. We use the checkField spec helper to activate the
corresponding radio button.

// Change plan to business

checkField(fixture, 'plan-business', true);

fixture.detectChanges();

To see the effect of the change, we need to tell Angular to


update the DOM. Then we expect the ng-invalid class and the
aria-required to be present.

expect(addressLine1El.attributes['aria-required']).toBe('true');

expect(addressLine1El.classes['ng-invalid']).toBe(true);

We perform the same check for the “Education & Non-profit”


plan.

// Change plan to non-profit

checkField(fixture, 'plan-non-profit', true);

fixture.detectChanges();

expect(addressLine1El.attributes['aria-required']).toBe('true');

expect(addressLine1El.classes['ng-invalid']).toBe(true);

This is it! Here is the full spec:

it('requires address line 1 for business and non-profit plans',


async () => {

await setup();

// Initial state (personal plan)

const addressLine1El = findEl(fixture, 'addressLine1');

expect('ng-invalid' in addressLine1El.classes).toBe(false);

expect('aria-required' in addressLine1El.attributes).toBe(false);

// Change plan to business

checkField(fixture, 'plan-business', true);

fixture.detectChanges();

expect(addressLine1El.attributes['aria-required']).toBe('true');

expect(addressLine1El.classes['ng-invalid']).toBe(true);

// Change plan to non-profit

checkField(fixture, 'plan-non-profit', true);

fixture.detectChanges();

expect(addressLine1El.attributes['aria-required']).toBe('true');

expect(addressLine1El.classes['ng-invalid']).toBe(true);

});

We have already checked the presence of aria-required


attributes when testing the required fields. For consistency, we
check for aria-required in this spec as well.

As a second indicator, we check for the ng-invalid class. This


class is set by Angular itself on invalid form fields without us
having to add it via the template. Note that the mere presence of
the class does not imply that the invalid state is conveyed
visually.

Alternatively, we could check for the presence of an error


message, like we did in the required fields spec.

🔗 SignupFormComponent: test code

Password type toggle


Another small feature of the sign-up form is the password type
switcher. This button toggles the visibility of the entered
password. Under the hood, it changes the input type from
password to text and vice versa.

The Component class stores the visibility in a boolean property:

public showPassword = false;

In the template, the input type depends on the property


(shortened code):

<input [type]="showPassword ? 'text' : 'password'" />

Finally, the button toggles the boolean value (shortened code):

<button

type="button"

(click)="showPassword = !showPassword"

>

{{ showPassword ? '🔒 Hide password' : '👁️ Show password' }}

</button>

To test this feature, we create a new spec:

it('toggles the password display', async () => {

await setup();

/* … */

});

Initially, the field has the password type so the entered text is
obfuscated. Let us test this baseline.

First, we enter a password into the field. This is not strictly


necessary but makes the test more realistic and debugging
easier. (The password field has the test id password.)

setFieldValue(fixture, 'password', 'top secret');

We find the input element by its test id again to check the type
attribute.

const passwordEl = findEl(fixture, 'password');

expect(passwordEl.attributes.type).toBe('password');

Now we click on the toggle button for the first time. We let
Angular update the DOM and check the input type again.

click(fixture, 'show-password');

fixture.detectChanges();

expect(passwordEl.attributes.type).toBe('text');

We expect that the type has changed from password to text.


The password is now visible.

With a second click on the toggle button, the type switches back
to password.

click(fixture, 'show-password');

fixture.detectChanges();

expect(passwordEl.attributes.type).toBe('password');

This is the whole spec:

it('toggles the password display', async () => {

await setup();

setFieldValue(fixture, 'password', 'top secret');

const passwordEl = findEl(fixture, 'password');

expect(passwordEl.attributes.type).toBe('password');

click(fixture, 'show-password');

fixture.detectChanges();

expect(passwordEl.attributes.type).toBe('text');

click(fixture, 'show-password');

fixture.detectChanges();

expect(passwordEl.attributes.type).toBe('password');

});

🔗 SignupFormComponent: test code

Testing form accessibility


Web accessibility means that all people can use a web site,
regardless of their physical or mental abilities or web access
technologies. It is is part of a greater effort called Inclusive
Design, the process of creating information systems that
account for people with diverse abilities and needs.

Designing web forms is a usability and accessibility challenge.


Web forms often pose a barrier for users with disabilities and
users of assistive technologies.

ACCESSIBLE FORM

The sign-up form has several accessibility features, among


others:

The form is well-structured with heading, fieldset and


legend elements.

All fields have proper labels, e.g. “Username (required)”.

Some fields have additional descriptions. The descriptions are


linked with aria-describedby attributes.

Required fields are marked with aria-required="true".

Invalid fields are marked with aria-invalid="true". The


error messages are linked with aria-errormessage
attributes.

When the form is submitted, the result is communicated


using a status message with role="status".
The structure and styling clearly conveys the current focus as
well as the validity state.

AUTOMATED ACCESSIBILITY TESTING

There are many more accessibility requirements and best


practices that we have not mentioned. Since this guide is not
about creating accessible forms primarily, let us explore how to
test accessibility in an automated way.

We have tested some of the features above in the


SignupFormComponent’s integration test. Instead of writing
more specs for accessibility requirements by hand, let us test the
accessibility with a proper tool.

🔗 Inclusive Design Principles

pa11y
In this guide, we will look at pa11y, a Node.js program that
checks the accessibility of a web page.

TESTS IN CHROME

pa11y starts and remotely-controls a Chrome or Chromium


browser. The browser navigates to the page under test. pa11y
then injects axe-core and/or HTML CodeSniffer, two accessibility
testing engines.
These engines check compliance with the Web Content
Accessibility Guidelines (WCAG), the authoritative technical
standard for web accessibility.

CLI VS. CI

pa11y has two modes of operation: The command line interface


(CLI) for checking one web page and the continuous integration
(CI) mode for checking multiple web pages.

For quickly testing a single page of your Angular application, use


the command line interface. When testing the whole application
on a regular basis, use the continuous integration mode.

To use the command line interface, install pa11y as a global npm


module:

npm install -g pa11y

TEST SINGLE PAGE

This installs the global command pa11y. To test a page on the


local Angular development server, run:

pa11y http://localhost:4200/

For the sign-up form, pa11y does not report any errors:

Welcome to Pa11y

> Running Pa11y on URL http://localhost:4200/

No issues found!

ERROR REPORT

If one of the form fields did not have a proper label, pa11y would
complain:

• Error: This textinput element does not have a name available to

an accessibility API. Valid names are: label element,

title undefined, aria-label undefined, aria-labelledby


undefined.

├── WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.InputText.Name

├── html > body > app-root > main > app-signup-form > form >
fieldset:nth-child(2) > div:nth-child(2) > p > span > input

└── <input _ngcontent-srr-c42="" type="text"


formcontrolname="username" …

• Error: This form field should be labelled in some way.

Use the label element (either with a "for" attribute or

wrapped around the form field), or "title", "aria-label"

or "aria-labelledby" attributes as appropriate.

├── WCAG2AA.Principle1.Guideline1_3.1_3_1.F68

├── html > body > app-root > main > app-signup-form > form >
fieldset:nth-child(2) > div:nth-child(2) > p > span > input

└── <input _ngcontent-srr-c42="" type="text"


formcontrolname="username" …

Each error message contains the violated WCAG rule, the DOM
path to the violating element and its HTML code.

🔗 pa11y: Accessibility testing tools


🔗 axe-core: Accessibility engine for automated Web UI testing
🔗 HTML CodeSniffer: Accessibility auditor
🔗 Web Content Accessibility Guidelines (WCAG) 2.1

pa11y-ci
For comprehensive test runs both during development and on a
build server, we will set up pa11y in the continuous integration
mode.

In your Angular project directory, install the pa11y-ci package:

npm install pa11y-ci

pa11y-ci expects a configuration file named .pa11yci in the


project directory. Create the file and paste this JSON:

"defaults": {

"runner": [

"axe",

"htmlcs"

},

"urls": [

"http://localhost:4200"

TEST MULTIPLE URLS

This configuration tells pa11y to check the URL


http://localhost:4200 and to use both available testing engines,
axe and htmlcs. You can add many URLs to the urls array.

We can now run pa11y-ci with:

npx pa11y-ci

For the sign-up form, we get this output:

Running Pa11y on 1 URLs:

> http://localhost:4200 - 0 errors

✔ 1/1 URLs passed

🔗 pa11y-ci: CI-centric accessibility test runner

Start server and run pa11y-ci


The configuration above expects that the development server is
already running at http://localhost:4200. Both in development
and on a build server, it is useful to start the Angular server, run
the accessibility tests and then stop the server again.

We can achieve this with another handy Node.js package,


start-server-and-test.

npm install start-server-and-test

start-server-and-test first runs an npm script that is


supposed to start an HTTP server. Then it waits for the server to
boot up. Once a given URL is available, it runs another npm
script.

In our case, the first script is start, an alias for ng serve. We


need to create the second script to run pa11y-ci.

NPM SCRIPTS

We edit package.json, and add two scripts:

"scripts": {

"a11y": "start-server-and-test start http-get://localhost:4200/


pa11y-ci",

"pa11y-ci": "pa11y-ci"

},

Now, npm run a11y starts the Angular development server,


then runs pa11y-ci, finally stops the server. The audit result is
written to the standard output.

🔗 start-server-and-test: Starts server, waits for URL, then runs test command

Form accessibility: Summary


pa11y is a powerful set of tools with many options. We have
barely touched on its features.
Automated accessibility testing is a valuable addition to unit,
integration and end-to-end tests. You should run an accessibility
tester like pa11y against the pages of your Angular application.
It is especially helpful to ensure the accessibility of complex
forms.

Bear in mind that automated testing only points out certain


accessibility barriers that can be detected programmatically.

The Web Content Accessibility Guidelines (WCAG) establish –


 from abstract to specific – principles, guidelines and success
criteria. The latter are the practical rules some of which can be
checked automatically.

The WCAG success criteria are accompanied by techniques for


HTML, CSS, JavaScript etc. For JavaScript web applications,
techniques like ARIA are especially relevant.

In summary, you need to learn about accessibility and Inclusive


Design first, apply the rules while designing and implementing
the application. Then check the compliance manually and
automatically.

🔗 Web Content Accessibility Guidelines (WCAG) 2.1


🔗 Quick Reference: How to Meet WCAG
🔗 WebAIM: Introduction to ARIA - Accessible Rich Internet Applications
🔗 Web Accessibility Tutorial: Forms
Testing Components with
Spectator
LEARNING OBJECTIVES

Simplifying Component tests with the Spectator library


Using the unified Spectator interface
Interacting with the Component and the rendered DOM
Dispatching synthetic DOM events to simulate user input
Using Spectator and ng-mocks to fake child Components and Services

We have used Angular’s testing tools to set up modules, render


Components, query the DOM and more. These tools are
TestBed, ComponentFixture and DebugElement, also
HttpClientTestingModule and RouterTestingModule.

STRUCTURAL WEAKNESSES

The built-in tools are fairly low-level and unopinionated. They


have several drawbacks:

TestBed requires a large amount of boilerplate code to set


up a common Component or Service test.

DebugElement lacks essential features and is a “leaky”


abstraction. You are forced to work with the wrapped native
DOM element for common tasks.
There are no default solutions for faking Components and
Service dependencies safely.

The tests itself get verbose and repetitive. You have to


establish testing conventions and write helpers yourself.

We have already used small element testing helpers. They solve


isolated problems in order to write more consistent and compact
specs.

If you write hundreds or thousands of specs, you will find that


these helper functions do not suffice. They do not address the
above-mentioned structural problems.

UNIFIED TESTING API

Spectator is an opinionated library for testing Angular


applications. Technically, it sits on top of TestBed,
ComponentFixture and DebugElement. But the main idea is to
unify all these APIs in one consistent, powerful and user-friendly
interface – the Spectator object.

Spectator simplifies testing Components, Services, Directives,


Pipes, routing and HTTP communication. Spectator’s strength
are Component tests with Inputs, Outputs, children, event
handling, Service dependencies and more.
For faking child Components, Spectator resorts to the ng-mocks
library just like we did.

This guide cannot introduce all Spectator features, but we will


discuss the basics of Component testing using Spectator.

Both example applications are tested with our element helpers


and also with Spectator. The former specs use the suffix
.spec.ts, while the latter use the suffix .spectator.spec.ts.
This way, you can compare the tests side-by-side.

In this chapter, we will discuss testing the Flickr search with


Spectator.

Component with an Input


Let us start with the FullPhotoComponent because it is a
presentational Component, a leaf in the Component tree. It
expects a Photo object as Input and renders an image as well as
the photo metadata. No Outputs, no children, no Service
dependencies.

The FullPhotoComponent suite with our helpers looks like this:

describe('FullPhotoComponent', () => {

let component: FullPhotoComponent;

let fixture: ComponentFixture<FullPhotoComponent>;

beforeEach(async () => {

await TestBed.configureTestingModule({

declarations: [FullPhotoComponent],

schemas: [NO_ERRORS_SCHEMA],

}).compileComponents();

fixture = TestBed.createComponent(FullPhotoComponent);

component = fixture.componentInstance;

component.photo = photo1;

fixture.detectChanges();

});

it('renders the photo information', () => {

expectText(fixture, 'full-photo-title', photo1.title);

const img = findEl(fixture, 'full-photo-image');

expect(img.properties.src).toBe(photo1.url_m);

expect(img.properties.alt).toBe(photo1.title);

expectText(fixture, 'full-photo-ownername', photo1.ownername);

expectText(fixture, 'full-photo-datetaken', photo1.datetaken);

expectText(fixture, 'full-photo-tags', photo1.tags);

const link = findEl(fixture, 'full-photo-link');

expect(link.properties.href).toBe(photo1Link);

expect(link.nativeElement.textContent.trim()).toBe(photo1Link);

});

});

This suite already benefits from expectText and findEl, but it


is still using the leaky DebugElement abstraction.

COMPONENT FACTORY
When using Spectator, the Module configuration and the
Component creation looks different. In the scope of the test
suite, we create a Component factory:

import { createComponentFactory } from '@ngneat/spectator';

describe('FullPhotoComponent with spectator', () => {

/* … */

const createComponent = createComponentFactory({

component: FullPhotoComponent,

shallow: true,

});

/* … */

});

createComponentFactory expects a configuration object.


component: FullPhotoComponent specifies the Component
under test. shallow: true means we want shallow, not deep
rendering. It does not make a difference for
FullPhotoComponent though since it has no children.

The configuration object may include more options for the


testing Module, as we will see later.

Internally, createComponentFactory creates a beforeEach


block that calls TestBed.configureTestingModule and
TestBed.compileComponents, just like we did manually.
createComponentFactory returns a factory function for
creating a FullPhotoComponent. We save that function in the
createComponent constant.

CREATE COMPONENT

The next step is to add a beforeEach block that creates the


Component instance. createComponent again takes an options
object. To set the photo Input property, we pass props: {
photo: photo1 }.

import { createComponentFactory, Spectator } from


'@ngneat/spectator';

describe('FullPhotoComponent with spectator', () => {

let spectator: Spectator<FullPhotoComponent>;

const createComponent = createComponentFactory({

component: FullPhotoComponent,

shallow: true,

});

beforeEach(() => {

spectator = createComponent({ props: { photo: photo1 } });

});

/* … */

});

SPECTATOR
createComponent returns a Spectator object. This is the
powerful interface we are going to use in the specs.

The spec it('renders the photo information', /* …


*/) repeats three essential tasks several times:

1. Find an element by test id

2. Check its text content

3. Check its attribute value

First, the spec finds the element with the test id full-photo-
title and expects it to contain the photo’s title.

With Spectator, it reads:

expect(

spectator.query(byTestId('full-photo-title'))

).toHaveText(photo1.title);

SPECTATOR.QUERY

The central spectator.query method finds an element in the


DOM. This guide recommends to find elements by test ids
(data-testid attributes).

Spectator supports test ids out of the box, so we write:

spectator.query(byTestId('full-photo-title'))

spectator.query returns a native DOM element or null in


case no match was found. Note that it does not return a
DebugElement.

When using Spectator, you work directly with DOM element


objects. What seems cumbersome at first glance, in fact lifts the
burden of the leaky DebugElement abstraction.

JASMINE MATCHERS

Spectator makes it easy to work with plain DOM elements.


Several matchers are added to Jasmine to create expectations on
an element.

For checking an element’s text content, Spectator provides the


toHaveText matcher. This leads us to the following expectation:

expect(

spectator.query(byTestId('full-photo-title'))

).toHaveText(photo1.title);

This code is equivalent to our expectText helper, but more


idiomatic and fluent to read.

Next, we need to verify that the Component renders the full


photo using an img element.

const img = spectator.query(byTestId('full-photo-image'));

expect(img).toHaveAttribute('src', photo1.url_m);

expect(img).toHaveAttribute('alt', photo1.title);

Here, we find the element with the test id full-photo-image to


check its src and alt attributes. We use Spectator’s matcher
toHaveAttribute for this purpose.

The rest of the spec finds more elements to inspect their


contents and attributes.

The full test suite using Spectator (only imports from Spectator
are shown):

import {

byTestId, createComponentFactory, Spectator

} from '@ngneat/spectator';

describe('FullPhotoComponent with spectator', () => {

let spectator: Spectator<FullPhotoComponent>;

const createComponent = createComponentFactory({

component: FullPhotoComponent,

shallow: true,

});

beforeEach(() => {

spectator = createComponent({ props: { photo: photo1 } });

});

it('renders the photo information', () => {

expect(

spectator.query(byTestId('full-photo-title'))

).toHaveText(photo1.title);

const img = spectator.query(byTestId('full-photo-image'));

expect(img).toHaveAttribute('src', photo1.url_m);

expect(img).toHaveAttribute('alt', photo1.title);

expect(

spectator.query(byTestId('full-photo-ownername'))

).toHaveText(photo1.ownername);

expect(

spectator.query(byTestId('full-photo-datetaken'))

).toHaveText(photo1.datetaken);

expect(

spectator.query(byTestId('full-photo-tags'))

).toHaveText(photo1.tags);

const link = spectator.query(byTestId('full-photo-link'));

expect(link).toHaveAttribute('href', photo1Link);

expect(link).toHaveText(photo1Link);

});

});

Compared to the version with custom testing helpers, the


Spectator version is not necessarily shorter. But it works on a
consistent abstraction level.

Instead of a wild mix of TestBed, ComponentFixture,


DebugElement plus helper functions, there is the
createComponentFactory function and one Spectator
instance.

Spectator avoids wrapping DOM elements, but offers convenient


Jasmine matchers for common DOM expectations.

🔗 FullPhotoComponent: implementation code and the two tests


🔗 Spectator: Queries
🔗 Spectator: Custom matchers

Component with children and


Service dependency
Spectator really shines when testing container Components.
These are Components with children and Service dependencies.

In the Flickr search, the topmost FlickrSearchComponent calls


the FlickrService and holds the state. It orchestrates three
other Components, passes down the state and listens for
Outputs.

The FlickrSearchComponent template:

<app-search-form (search)="handleSearch($event)"></app-search-form>

<div class="photo-list-and-full-photo">

<app-photo-list

[title]="searchTerm"

[photos]="photos"

(focusPhoto)="handleFocusPhoto($event)"

class="photo-list"

></app-photo-list>

<app-full-photo

*ngIf="currentPhoto"

[photo]="currentPhoto"

class="full-photo"

data-testid="full-photo"

></app-full-photo>

</div>

The FlickrSearchComponent class:

@Component({

selector: 'app-flickr-search',

templateUrl: './flickr-search.component.html',

styleUrls: ['./flickr-search.component.css'],

})

export class FlickrSearchComponent {

public searchTerm = '';

public photos: Photo[] = [];

public currentPhoto: Photo | null = null;

constructor(private flickrService: FlickrService) {}

public handleSearch(searchTerm: string): void {

this.flickrService.searchPublicPhotos(searchTerm).subscribe(

(photos) => {

this.searchTerm = searchTerm;

this.photos = photos;

this.currentPhoto = null;

);

public handleFocusPhoto(photo: Photo): void {

this.currentPhoto = photo;

CHILD COMPONENTS

Since this is the Component where all things come together,


there is much to test.

1. Initially, the SearchFormComponent and the


PhotoListComponent are rendered, not the
FullPhotoComponent. The photo list is empty.

2. When the SearchFormComponent emits the search Output,


the FlickrService is called with the search term.

3. The search term and the photo list are passed down to the
PhotoListComponent via Inputs.

4. When the PhotoListComponent emits the focusPhoto


Output, the FullPhotoComponent is rendered. The selected
photo is passed down via Input.

WITHOUT SPECTATOR

The FlickrSearchComponent test suite with our helpers looks


like this:

describe('FlickrSearchComponent', () => {

let fixture: ComponentFixture<FlickrSearchComponent>;

let component: FlickrSearchComponent;

let fakeFlickrService: Pick<FlickrService, keyof FlickrService>;

let searchForm: DebugElement;

let photoList: DebugElement;

beforeEach(async () => {

fakeFlickrService = {

searchPublicPhotos: jasmine

.createSpy('searchPublicPhotos')

.and.returnValue(of(photos)),

};

await TestBed.configureTestingModule({

imports: [HttpClientTestingModule],

declarations: [FlickrSearchComponent],

providers: [

{ provide: FlickrService, useValue: fakeFlickrService }

],

schemas: [NO_ERRORS_SCHEMA],

}).compileComponents();

});

beforeEach(() => {

fixture = TestBed.createComponent(FlickrSearchComponent);

component = fixture.debugElement.componentInstance;

fixture.detectChanges();

searchForm = findComponent(fixture, 'app-search-form');

photoList = findComponent(fixture, 'app-photo-list');

});

it('renders the search form and the photo list, not the full
photo', () => {

expect(searchForm).toBeTruthy();

expect(photoList).toBeTruthy();

expect(photoList.properties.title).toBe('');

expect(photoList.properties.photos).toEqual([]);

expect(() => {

findComponent(fixture, 'app-full-photo');

}).toThrow();

});

it('searches and passes the resulting photos to the photo list',


() => {

const searchTerm = 'beautiful flowers';

searchForm.triggerEventHandler('search', searchTerm);

fixture.detectChanges();

expect(fakeFlickrService.searchPublicPhotos).toHaveBeenCalledWith(s
earchTerm);

expect(photoList.properties.title).toBe(searchTerm);

expect(photoList.properties.photos).toBe(photos);

});

it('renders the full photo when a photo is focussed', () => {

expect(() => {

findComponent(fixture, 'app-full-photo');

}).toThrow();

photoList.triggerEventHandler('focusPhoto', photo1);

fixture.detectChanges();

const fullPhoto = findComponent(fixture, 'app-full-photo');

expect(fullPhoto.properties.photo).toBe(photo1);

});

});

Without going too much into detail, a few notes:


We use shallow rendering. The child Components are not
declared and only empty shell elements are rendered (app-
search-form, app-photo-list and app-full-photo). This
lets us check their presence, their Inputs and Outputs.

We use our findComponent testing helper to find the child


elements.

To check the Input values, we use the properties of


DebugElements.

To simulate that an Output emits, we use


triggerEventListener on DebugElements.

We provide our own fake FlickrService. It contains one


Jasmine spy that returns a Observable with a fixed list of
photos.

fakeFlickrService = {

searchPublicPhotos: jasmine

.createSpy('searchPublicPhotos')

.and.returnValue(of(photos)),

};

WITH SPECTATOR

Rewriting this suite with Spectator brings two major changes:

1. We replace the child Components with fakes created by ng-


mocks. The fake Components mimic the originals regarding
their Inputs and Outputs, but they do not render anything.
We will work with these Component instances instead of
operating on DebugElements.

2. We use Spectator to create the fake FlickrService.

The test suite setup:

import {

createComponentFactory, mockProvider, Spectator

} from '@ngneat/spectator';

describe('FlickrSearchComponent with spectator', () => {

/* … */

const createComponent = createComponentFactory({

component: FlickrSearchComponent,

shallow: true,

declarations: [

MockComponents(

SearchFormComponent, PhotoListComponent, FullPhotoComponent

),

],

providers: [mockProvider(FlickrService)],

});

/* … */

});

Again, we use Spectator’s createComponentFactory. This time,


we replace the child Components with fakes created by ng-
mocks’ MockComponents function.
MOCKPROVIDER

Then we use Spectator’s mockProvider function to create a fake


FlickrService. Under the hood, this works roughly the same
as our manual fakeFlickrService. It creates an object that
resembles the original, but the methods are replaced with
Jasmine spies.

In a beforeEach block, the Component is created.

import {

createComponentFactory, mockProvider, Spectator

} from '@ngneat/spectator';

describe('FlickrSearchComponent with spectator', () => {

let spectator: Spectator<FlickrSearchComponent>;

let searchForm: SearchFormComponent | null;

let photoList: PhotoListComponent | null;

let fullPhoto: FullPhotoComponent | null;

const createComponent = createComponentFactory(/* … */);

beforeEach(() => {

spectator = createComponent();

spectator.inject(FlickrService).searchPublicPhotos.and.returnValue(
of(photos));

searchForm = spectator.query(SearchFormComponent);

photoList = spectator.query(PhotoListComponent);

fullPhoto = spectator.query(FullPhotoComponent);

});

/* … */

});

spectator.inject is the equivalent of TestBed.inject. We


get hold of the FlickrService fake instance and configure the
searchPublicPhotos spy to return fixed data.

FIND CHILDREN

spectator.query not only finds elements in the DOM, but also


child Components and other nested Directives. We find the three
child Components and save them in variables since they will be
used in all specs.

Note that searchForm, photoList and fullPhoto are typed as


Component instances, not DebugElements. This is accurate
because the fakes have the same public interfaces, the same
Inputs and Output.

Due to the equivalence of fake and original, we can access


Inputs with the pattern componentInstance.input. And we let
an Output emit with the pattern
componentInstance.output.emit(…).

The first spec checks the initial state:

it('renders the search form and the photo list, not the full
photo', () => {

if (!(searchForm && photoList)) {

throw new Error('searchForm or photoList not found');

expect(photoList.title).toBe('');

expect(photoList.photos).toEqual([]);

expect(fullPhoto).not.toExist();

});

spectator.query(PhotoListComponent) either returns the


Component instance or null if there is no such nested
Component. Hence, the photoList variable is typed as
PhotoListComponent | null.

MANUAL TYPE GUARD

Unfortunately, expect is not a TypeScript type guard. Jasmine


expectations cannot narrow down the type from
PhotoListComponent | null to PhotoListComponent.

We cannot call expect(photoList).not.toBe(null) and


continue with expect(photoList.title).toBe(''). The first
expectation throws an error in the null case, but TypeScript
does not know this. TypeScript still assumes the type
PhotoListComponent | null, so it would complain about
photoList.title.

This is why we manually throw an error when photoList is


null. TypeScript infers that the type must be
PhotoListComponent in the rest of the spec.
In contrast, our findComponent helper function throws an
exception directly if no match was found, failing the test early. To
verify that a child Component is absent, we had to expect this
exception:

expect(() => {

findComponent(fixture, 'app-full-photo');

}).toThrow();`.

The Spectator spec goes on and uses


expect(fullPhoto).not.toExist(), which is equivalent to
expect(fullPhoto).toBe(null). The Jasmine matcher
toExist comes from Spectator.

TEST SEARCH

The second spec covers the search:

it('searches and passes the resulting photos to the photo list', ()


=> {

if (!(searchForm && photoList)) {

throw new Error('searchForm or photoList not found');

const searchTerm = 'beautiful flowers';

searchForm.search.emit(searchTerm);

spectator.detectChanges();

const flickrService = spectator.inject(FlickrService);

expect(flickrService.searchPublicPhotos).toHaveBeenCalledWith(searc
hTerm);

expect(photoList.title).toBe(searchTerm);

expect(photoList.photos).toBe(photos);

});

When the SearchFormComponent emits a search term, we


expect that the FlickrService has been called. In addition, we
expect that the search term and the photo list from Service are
passed to the PhotoListComponent.

spectator.detectChanges() is just Spectator’s shortcut to


fixture.detectChanges().

TEST FOCUS PHOTO

The last spec focusses a photo:

it('renders the full photo when a photo is focussed', () => {

expect(fullPhoto).not.toExist();

if (!photoList) {

throw new Error('photoList not found');

photoList.focusPhoto.emit(photo1);

spectator.detectChanges();

fullPhoto = spectator.query(FullPhotoComponent);

if (!fullPhoto) {

throw new Error('fullPhoto not found');

expect(fullPhoto.photo).toBe(photo1);

});

Again, the main difference is that we directly work with Inputs


and Outputs.

🔗 FlickrSearchComponent: implementation code and the two tests


🔗 ng-mocks: How to mock a component
🔗 Spectator: Mocking providers

Event handling with Spectator


Most Components handle input events like mouse clicks,
keypresses or form field changes. To simulate them, we have
used the triggerEventHandler method on DebugElements.
This method does not actually simulate DOM events, it merely
calls the event handlers registered by
(click)="handler($event)" and the like.

triggerEventHandler requires you to create an event object


that becomes $event in the template. For this reason, we have
introduced the click and makeClickEvent helpers.

SYNTHETIC EVENTS

Spectator takes a different approach: It dispatches synthetic


DOM events. This makes the test more realistic. Synthetic events
can bubble up in the DOM tree like real events. Spectator creates
the event objects for you while you can configure the details.
SPECTATOR.CLICK

To perform a simple click, we use spectator.click and pass


the target element or a byTestId selector. An example from the
PhotoItemComponent test:

describe('PhotoItemComponent with spectator', () => {

/* … */

it('focusses a photo on click', () => {

let photo: Photo | undefined;

spectator.component.focusPhoto.subscribe((otherPhoto: Photo) =>


{

photo = otherPhoto;

});

spectator.click(byTestId('photo-item-link'));

expect(photo).toBe(photo1);

});

/* … */

});

Another common task is to simulate form field input. So far, we


have used the setFieldValue helper for this purpose.

SPECTATOR.​
T YPEINELEMENT

Spectator has an equivalent method named


spectator.typeInElement. It is used by the
SearchFormComponent test:
describe('SearchFormComponent with spectator', () => {

/* … */

it('starts a search', () => {

let actualSearchTerm: string | undefined;

spectator.component.search.subscribe((otherSearchTerm: string)
=> {

actualSearchTerm = otherSearchTerm;

});

spectator.typeInElement(searchTerm, byTestId('search-term-
input'));

spectator.dispatchFakeEvent(byTestId('form'), 'submit');

expect(actualSearchTerm).toBe(searchTerm);

});

});

DISPATCH NGSUBMIT

The spec simulates typing the search term into the search field.
Then it simulates an ngSubmit event at the form element. We
use the generic method spectator.dispatchFakeEvent for
this end.

Spectator offers many more convenient shortcuts for triggering


events. The Flickr search Spectator tests just use the most
common ones.

🔗 PhotoItemComponent: implementation code and the two tests


🔗 SearchFormComponent: implementation code and the two tests
🔗 Spectator: Events API

Spectator: Summary
Spectator is a mature library that addresses the practical needs
of Angular developers. It offers solutions for common Angular
testing problems. The examples above presented only a few of
Spectator’s features.

Test code should be both concise and easy to understand.


Spectator provides an expressive, high-level language for writing
Angular tests. Spectator makes simple tasks simple without
losing any power.

Spectator’s success underlines that the standard Angular testing


tools are cumbersome and inconsistent. Alternative concepts are
both necessary and beneficial.

Once you are familiar with the standard tools, you should try out
alternatives like Spectator and ng-mocks. Then decide whether
you stick with isolated testing helpers or switch to more
comprehensive testing libraries.

🔗 Spectator project site


🔗 ng-mocks project site
Testing Services
LEARNING OBJECTIVES

Writing tests for Services with internal state


Testing Observables returned by Services
Verifying HTTP requests and payload processing
Covering HTTP success and error cases

In an Angular application, Services are responsible for fetching,


storing and processing data. Services are singletons, meaning
there is only one instance of a Service during runtime. They are
fit for central data storage, HTTP and WebSocket communication
as well as data validation.

SINGLETON

The single Service instance is shared among Components and


other application parts. Therefore, a Service is used when
Components that are not parent and child need to communicate
with each other and exchange data.

INJECTABLE

“Service” is an umbrella term for any object that serves a specific


purpose and is injected as a dependency. Technically, Services
have little in common. There are no rules regarding the structure
or behavior of a Service.
Typically, Services are classes, but not necessarily. While
Modules, Components and Directives are marked with respective
decorators – @Module, @Component, @Directive –, Services are
marked with the generic @Injectable.

RESPONSIBILITIES

So what does a Service do and how do we test it? Services are


diverse, but some patterns are widespread.

Services have public methods that return values.

In the test, we check whether a method returns correct data.

Services store data. They hold an internal state. We can get or


set the state.

In the test, we check whether the state is changed correctly.


Since the state should be held in private properties, we
cannot access the state directly. We test the state change by
calling public methods. We should not peek into the black
box.

Services interact with dependencies. These are often other


Services. For example, a Service might send HTTP requests
via Angular’s HttpClient.

In the unit test, we replace the dependency with a fake that


returns canned responses.
Testing a Service with internal state
Let us start with testing the CounterService. By now, you
should be familiar with the Service. As a reminder, here is the
shape including private members:

class CounterService {

private count: number;

private subject: BehaviorSubject<number>;

public getCount(): Observable<number> { /* … */ }

public increment(): void { /* … */ }

public decrement(): void { /* … */ }

public reset(newCount: number): void { /* … */ }

private notify(): void { /* … */ }

We need to identify what the Service does, what we need test


and how we test it.

WHAT IT DOES

The Service holds an internal state, namely in the private


count and subject properties. We cannot and should not
access these properties in the test.

For reading the state, the Service has the getCount method.
It does not return a synchronous value, but an RxJS
Observable. We will use getCount to get the current count
and also to subscribe to changes.
For changing the state, the Service provides the methods
increment, decrement and reset. We will call them and
check whether the state has changed accordingly.

Let us write the test code! We create a file called


counter.service.spec.ts and fill it with test suite boilerplate
code:

describe('CounterService', () => {

/* … */

});

We already know what the Service does and what needs to be


tested. So we add specs for all features:

describe('CounterService', () => {

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

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

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

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

});

INSTANTIATE WITHOUT TESTBED

In the Arrange phase, each spec needs to create an instance of


CounterService. The simplest way to do that is:

const counterService = new CounterService();

This is fine for simple Services without dependencies. For testing


Services with dependencies, we will use the TestBed later.
We create the fresh instance in a beforeEach block since every
spec needs it:

describe('CounterService', () => {

let counterService: CounterService;

beforeEach(() => {

counterService = new CounterService();

});

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

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

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

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

});

Let us start with writing the spec it('returns the count',


/* … */). It tests the getCount method that returns an
Observable.

CHANGE VARIABLE VALUE

For testing the Observable, we use the same pattern that we


have used for testing a Component Output:

1. We declare a variable actualCount that is initially undefined.

2. We subscribe to the Observable. We assign the emitted value


to the actualCount variable.
3. Finally, outside of the subscriber function, we compare the
actual to the expected value.

it('returns the count', () => {

let actualCount: number | undefined;

counterService.getCount().subscribe((count) => {

actualCount = count;

});

expect(actualCount).toBe(0);

});

This works because the Observable is backed by a


BehaviorSubject that stores the latest value and sends it to
new subscribers immediately.

STATE CHANGE

The next spec tests the increment method. We call the method
and verify that the count state has changed.

As mentioned before, we cannot access the private properties


for this purpose. Just like in the spec above, we need to use the
public getCount method to read the count.

it('increments the count', () => {

counterService.increment();

let actualCount: number | undefined;

counterService.getCount().subscribe((count) => {

actualCount = count;

});

expect(actualCount).toBe(1);

});

EXPECT CHANGED VALUE

The order here is important: First, we call increment, then we


subscribe to the Observable to read and verify the changed
value. Again, the BehaviorSubject emits the current value to
new subscribers synchronously.

The two remaining specs work almost the same. We just call the
respective methods.

it('decrements the count', () => {

counterService.decrement();

let actualCount: number | undefined;

counterService.getCount().subscribe((count) => {

actualCount = count;

});

expect(actualCount).toBe(-1);

});

it('resets the count', () => {

const newCount = 123;

counterService.reset(newCount);

let actualCount: number | undefined;

counterService.getCount().subscribe((count) => {

actualCount = count;

});

expect(actualCount).toBe(newCount);

});

REPEATING PATTERNS

We quickly notice that the specs are highly repetitive and noisy.
In every spec’s Assert phase, we are using this pattern to inspect
the Service state:

let actualCount: number | undefined;

counterService.getCount().subscribe((count) => {

actualCount = count;

});

expect(actualCount).toBe(/* … */);

This is a good candidate for a helper function. Let us call it


expectCount.

function expectCount(count: number): void {

let actualCount: number | undefined;

counterService.getCount().subscribe((actualCount2) => {

actualCount = actualCount2;

});

expect(actualCount).toBe(count);

The pattern has one variable bit, the expected count. That is why
the helper function has one parameter.

UNSUBSCRIBE

Now that we have pulled out the code into a central helper
function, there is one optimization we should add. The First Rule
of RxJS Observables states: “Anyone who subscribes, must
unsubscribe as well”.
In expectCount, we need to get the current count only once.
We do not want to create a long-lasting subscription. We are not
interested in future changes.

If we call expectCount only once per spec, this is not a huge


problem. If we wrote a more complex spec with several
expectCount calls, we would create pointless subscriptions.
This is likely to cause confusion when debugging the subscriber
function.

In short, we want to fetch the count and then unsubscribe to


reduce unwanted subscriptions.

UNSUBSCRIBE MANUALLY

One possible solution is unsubscribing immediately after


subscribing. The subscribe method returns a Subscription
with the useful unsubscribe method.

function expectCount(count: number): void {

let actualCount: number | undefined;

counterService

.getCount()

.subscribe((actualCount2) => {

actualCount = actualCount2;

})

.unsubscribe();

expect(actualCount).toBe(count);

RXJS OPERATOR
A more idiomatic way is to use an RxJS operator that completes
the Observable after the first value: first.

import { first } from 'rxjs/operators';

function expectCount(count: number): void {

let actualCount: number | undefined;

counterService

.getCount()

.pipe(first())

.subscribe((actualCount2) => {

actualCount = actualCount2;

});

expect(actualCount).toBe(count);

If you are not familiar with this arcane RxJS magic, do not worry.
In the simple CounterService test, unsubscribing is not strictly
necessary. But it is a good practice that avoids weird errors when
testing more complex Services that make use of Observables.

The complete test suite now looks like this:

describe('CounterService', () => {

let counterService: CounterService;

function expectCount(count: number): void {

let actualCount: number | undefined;

counterService

.getCount()

.pipe(first())

.subscribe((actualCount2) => {

actualCount = actualCount2;

});

expect(actualCount).toBe(count);

beforeEach(() => {

counterService = new CounterService();

});

it('returns the count', () => {

expectCount(0);

});

it('increments the count', () => {

counterService.increment();

expectCount(1);

});

it('decrements the count', () => {

counterService.decrement();

expectCount(-1);

});

it('resets the count', () => {

const newCount = 123;

counterService.reset(newCount);

expectCount(newCount);

});

});

🔗 CounterService: test code


Testing a Service that sends HTTP
requests
Services without dependencies, like CounterService, are
relatively easy to test. Let us examine a more complex Service
with a dependency.

In the Flickr search, the FlickrService is responsible for searching


photos via the Flickr API. It makes an HTTP GET request to
www.flickr.com. The server responds with JSON. Here is the full
code:

@Injectable()

export class FlickrService {

constructor(private http: HttpClient) {}

public searchPublicPhotos(searchTerm: string):


Observable<Photo[]> {

return this.http

.get<FlickrAPIResponse>(

'https://www.flickr.com/services/rest/',

params: {

tags: searchTerm,

method: 'flickr.photos.search',

format: 'json',

nojsoncallback: '1',

tag_mode: 'all',

media: 'photos',

per_page: '15',

extras: 'tags,date_taken,owner_name,url_q,url_m',

api_key: 'XYZ',

},

.pipe(map((response) => response.photos.photo));

The Service is marked with @Injectable() so it takes part in


Angular’s Dependency Injection. It depends on Angular’s
standard HTTP library, HttpClient from the
@angular/common/http package. Most Angular applications
use HttpClient to communicate with HTTP APIs.

There are two ways to test the FlickrService: an integration


test or a unit test.

REQUESTS AGAINST PRODUCTION

An integration test provides the real HttpClient. This leads to


HTTP requests to the Flickr API when the running the tests. This
makes the whole test unreliable.

The network or the web service might be slow or unavailable.


Also the Flickr API endpoint returns a different response for each
request. It is hard to expect a certain FlickrService behavior if
the input is unknown.
Requests to third-party production APIs make little sense in a
testing environment. If you want to write an integration test for
a Service that makes HTTP request, better use a dedicated
testing API that returns fixed data. This API can run on the same
machine or in the local network.

INTERCEPT REQUESTS

In the case of FlickrService, we better write a unit test.


Angular has a powerful helper for testing code that depends on
HttpClient: the HttpClientTestingModule.

For testing a Service with dependencies, it is tedious to


instantiate the Service with new. Instead, we use the TestBed to
set up a testing Module.

In place of the HttpClient, we import the


HttpClientTestingModule.

TestBed.configureTestingModule({

imports: [HttpClientTestingModule],

providers: [FlickrService],

});

The HttpClientTestingModule provides a fake


implementation of HttpClient. It does not actually send out
HTTP requests. It merely intercepts them and records them
internally.
In the test, we inspect that record of HTTP requests. We respond
to pending requests manually with fake data.

FIND, RESPOND, VERIFY

Our test will perform the following steps:

1. Call the method under test that sends HTTP requests

2. Find pending requests

3. Respond to these requests with fake data

4. Check the result of the method call

5. Verify that all requests have been answered

🔗 Angular guide: Communicating with backend services using HTTP


🔗 Angular API reference: HttpClient
🔗 Angular guide: Testing HTTP requests
🔗 Angular API reference: HttpClientTestingModule

Call the method under test


In the first step, we call the method under test,
searchPublicPhotos. The search term is simply a fixed string.

const searchTerm = 'dragonfly';

describe('FlickrService', () => {

let flickrService: FlickrService;

beforeEach(() => {

TestBed.configureTestingModule({

imports: [HttpClientTestingModule],

providers: [FlickrService],

});

flickrService = TestBed.inject(FlickrService);

});

it('searches for public photos', () => {

flickrService.searchPublicPhotos(searchTerm).subscribe(

(actualPhotos) => {

/* … */

);

/* … */

});

});

We subscribe to the Observable returned by


searchPublicPhotos so the (fake) HTTP request is sent. We will
investigate the response, actualPhotos, later in step four.

Find pending requests


In the second step, we find the pending request using the
HttpTestingController. This class is part of the
HttpClientTestingModule. We get hold of the instance by
calling TestBed.inject(HttpTestingController).
EXPECTONE

The controller has methods to find requests by different criteria.


The simplest is expectOne. It finds a request matching the given
criteria and expects that there is exactly one match.

In our case, we search for a request with a given URL of the Flickr
API.

const searchTerm = 'dragonfly';

const expectedUrl = `https://www.flickr.com/services/rest/?


tags=${searchTerm}&method=flickr.photos.search&format=json&nojsonca
llback=1&tag_mode=all&media=photos&per_page=15&extras=tags,date_tak
en,owner_name,url_q,url_m&api_key=XYZ`;

describe('FlickrService', () => {

let flickrService: FlickrService;

let controller: HttpTestingController;

beforeEach(() => {

TestBed.configureTestingModule({

imports: [HttpClientTestingModule],

providers: [FlickrService],

});

flickrService = TestBed.inject(FlickrService);

controller = TestBed.inject(HttpTestingController);

});

it('searches for public photos', () => {

flickrService.searchPublicPhotos(searchTerm).subscribe(

(actualPhotos) => {

/* … */

);

const request = controller.expectOne(expectedUrl);

/* … */

});

});

expectOne returns the found request, that is an instance of


TestRequest. If there is no pending request that matches the
URL, expectOne throws an exception, failing the spec.

🔗 Angular API reference: HttpTestingController


🔗 Angular API reference: TestRequest

Respond with fake data


Now that we have the pending request at hand, we respond to it
with an object that mimics the original API response. The Flickr
API returns a complex object with an array of photos objects
deep within. In the FlickrService test, we only care about the
payload, the photos array.

The Flickr search repository contains fake photo objects that are
used throughout the tests. For the FlickrService test, we
import the photos array with two fake photo objects.

We use the request’s flush method to respond with fake data.


This simulates a successful “200 OK” server response.
request.flush({ photos: { photo: photos } });

Check the result of the method call


The spec has proven that searchPublicPhotos makes a
request to the expected URL. It still needs to prove that the
method passes through the desired part of the API response. In
particular, it needs to prove that the Observable emits the
photos array.

We have already subscribed to the Observable:

flickrService.searchPublicPhotos(searchTerm).subscribe(

(actualPhotos) => {

/* … */

);

We expect that the Observable emits a photos array that equals


to the one from the API response:

flickrService.searchPublicPhotos(searchTerm).subscribe(

(actualPhotos) => {

expect(actualPhotos).toEqual(photos);

);

This leads to a problem that is known from testing Outputs: If


the code under test is broken, the Observable never emits. The
next callback with expect will not be called. Despite the defect,
Jasmine thinks that all is fine.

EXPECT CHANGED VALUE

There are several ways to solve this problem. We have opted for
a variable that is undefined initially and is assigned a value.

let actualPhotos: Photo[] | undefined;

flickrService.searchPublicPhotos(searchTerm).subscribe(

(otherPhotos) => {

actualPhotos = otherPhotos;

);

const request = controller.expectOne(expectedUrl);

// Answer the request so the Observable emits a value.

request.flush({ photos: { photo: photos } });

// Now verify emitted valued.

expect(actualPhotos).toEqual(photos);

The expect call is located outside of the next callback function


to ensure it is definitely called. If the Observable emits no value
or a wrong value, the spec fails.

Verify that all requests have been


answered
In the last step, we ensure that there are no pending requests
left. We expect the method under test to make one request to a
specific URL. We have found the request with expectOne and
have answered it with flush.

Finally, we call:

controller.verify();

This fails the test if there are any outstanding requests.

verify guarantees that the code under test is not making


excess requests. But it also guarantees that your spec checks all
requests, for example by inspecting their URLs.

Putting the parts together, the full test suite:

const searchTerm = 'dragonfly';

const expectedUrl = `https://www.flickr.com/services/rest/?


tags=${searchTerm}&method=flickr.photos.search&format=json&nojsonca
llback=1&tag_mode=all&media=photos&per_page=15&extras=tags,date_tak
en,owner_name,url_q,url_m&api_key=XYZ`;

describe('FlickrService', () => {

let flickrService: FlickrService;

let controller: HttpTestingController;

beforeEach(() => {

TestBed.configureTestingModule({

imports: [HttpClientTestingModule],

providers: [FlickrService],

});

flickrService = TestBed.inject(FlickrService);

controller = TestBed.inject(HttpTestingController);

});

it('searches for public photos', () => {

let actualPhotos: Photo[] | undefined;

flickrService.searchPublicPhotos(searchTerm).subscribe(

(otherPhotos) => {

actualPhotos = otherPhotos;

);

const request = controller.expectOne(expectedUrl);

request.flush({ photos: { photo: photos } });

controller.verify();

expect(actualPhotos).toEqual(photos);

});

});

🔗 FlickrService: test code


🔗 Photo spec helper

Testing the error case


Are we done with testing searchPublicPhotos? We have
tested the success case in which the server returns a 200 OK.
But we have not tested the error case yet!

UNHAPPY PATH
searchPublicPhotos passes through the error from
HttpClient. If the Observable returned by this.http.get
fails with an error, the Observable returned by
searchPublicPhotos fails with the same error.

Whether there is custom error handling in the Service or not, the


unhappy path should be tested.

Let us simulate a “500 Internal Server Error”. Instead of


responding to the request with flush, we let it fail by calling
error.

const status = 500;

const statusText = 'Internal Server Error';

const errorEvent = new ErrorEvent('API error');

/* … */

const request = controller.expectOne(expectedUrl);

request.error(

errorEvent,

{ status, statusText }

);

The TestRequest’s error method expects an ErrorEvent, and


an optional options object.

ErrorEvent is a special type of Error. For testing purposes, we


create an instance using new ErrorEvent('…'). The
constructor parameter is a string message that describes the
error case.
The second parameter, the options object, allows us to set the
HTTP status (like 500), the statusText (like 'Internal
Server Error') and response headers. In the example above,
we set status and statusText.

EXPECT OBSERVABLE ERROR

Now we check that the returned Observable behaves correctly. It


must not emit a next value and must not complete. It must fail
with an error.

We achieve that by subscribing to next, error and complete


events:

flickrService.searchPublicPhotos(searchTerm).subscribe(

() => {

/* next handler must not be called! */

},

(error) => {

/*

error handler must be called!

Also, we need to inspect the error.

*/

},

() => {

/* complete handler must not be called! */

},

);

FAIL
When the next or complete handlers are called, the spec must
fail immediately. There is a handy global Jasmine function for
this purpose: fail.

For inspecting the error, we use the same pattern as above,


saving the error in a variable in the outer scope.

let actualError: HttpErrorResponse | undefined;

flickrService.searchPublicPhotos(searchTerm).subscribe(

() => {

fail('next handler must not be called');

},

(error) => {

actualError = error;

},

() => {

fail('complete handler must not be called');

},

);

After answering the request with a server error, we check that


the error is passed through. The error handler receives an
HttpErrorResponse object that reflects the ErrorEvent as
well as the status information.

if (!actualError) {

throw new Error('Error needs to be defined');

expect(actualError.error).toBe(errorEvent);

expect(actualError.status).toBe(status);

expect(actualError.statusText).toBe(statusText);

TYPE GUARD

Since actualError is defined as HttpErrorResponse |


undefined, we need to rule out the undefined case first before
accessing the properties.

expect(actualError).toBeDefined() would accomplish


that. But the TypeScript compiler does not know that this rules
out the undefined case. So we need to throw an exception
manually.

This is the full spec for the error case:

it('passes through search errors', () => {

const status = 500;

const statusText = 'Server error';

const errorEvent = new ErrorEvent('API error');

let actualError: HttpErrorResponse | undefined;

flickrService.searchPublicPhotos(searchTerm).subscribe(

() => {

fail('next handler must not be called');

},

(error) => {

actualError = error;

},

() => {

fail('complete handler must not be called');

},

);

controller.expectOne(expectedUrl).error(

errorEvent,

{ status, statusText }

);

if (!actualError) {

throw new Error('Error needs to be defined');

expect(actualError.error).toBe(errorEvent);

expect(actualError.status).toBe(status);

expect(actualError.statusText).toBe(statusText);

});

This example is deliberately verbose. It shows you how to test all


details. It fails fast and provides helpful error messages.

This approach is recommended for Service methods that have a


dedicated error handling. For example, a Service might
distinguish between successful responses (like “200 OK”), client
errors (like “404 Not Found”) and server errors (like “500 Server
error”).

🔗 FlickrService: implementation and test


🔗 Angular API reference: HttpErrorResponse
🔗 MDN reference: ErrorEvent

Alternatives for finding pending requests


We have used controller.expectOne to find a request that
matches the expected URL. Sometimes it is necessary to specify
more criteria, like the method (GET, POST, etc.), headers or the
request body.

expectOne has several signatures. We have used the simplest, a


string that is interpreted as URL:

controller.expectOne('https://www.example.org')

To search for a request with a given method and url, pass an


object with these properties:

controller.expectOne({

method: 'GET',

url: 'https://www.example.org'

})

If you need to find one request by looking at its details, you can
pass a function:

controller.expectOne(

(requestCandidate) =>

requestCandidate.method === 'GET' &&

requestCandidate.url === 'https://www.example.org' &&

requestCandidate.headers.get('Accept') === 'application/json',

);

This predicate function is called for each request, decides


whether the candidate matches and returns a boolean.
This lets you sift through all requests programmatically and
check all criteria. The candidate is an HttpRequest instance with
properties like method, url, headers, body, params, etc.

There are two possible approaches: Either you use expectOne


with many criteria, like in the predicate example. If some request
detail does not match, expectOne throws an exception and fails
the test.

Or you use expectOne with few criteria, passing { method:


'…', url: '…' }. To check the request details, you can still
use Jasmine expectations.

expectOne returns a TestRequest instance. This object only


has methods to answer the request, but no direct information
about the request. Use the request property to access the
underlying HttpRequest.

// Get the TestRequest.

const request = controller.expectOne({

method: 'GET',

url: 'https://www.example.org'

});

// Get the underlying HttpRequest. Yes, this is confusing.

const httpRequest = request.request;

expect(httpRequest.headers.get('Accept')).toBe('application/json');

request.flush({ success: true });

This is equivalent to the predicate example above, but gives a


more specific error message if the header is incorrect.

MATCH

In addition to expectOne, there is the match method for finding


multiple requests that match certain criteria. It returns an array
of requests. If there are no matches, the array is empty, but the
spec does not fail. Hence, you need to add Jasmine expectations
to check the array and the requests therein.

Assume there is a CommentService with a method


postTwoComments. The code under test makes two requests to
the same URL, but with a different body.

@Injectable()

class CommentService() {

constructor(private http: HttpClient) {}

public postTwoComments(firstComment: string, secondComment:


string) {

return combineLatest([

this.http.post('/comments/new', { comment: firstComment }),

this.http.post('/comments/new', { comment: secondComment }),

]);

The spec could contain:

const firstComment = 'First comment!';

const secondComment = 'Second comment!';

commentService

.postTwoComments(firstComment, secondComment)

.subscribe();

const requests = controller.match({

method: 'POST',

url: '/comments/new',

});

expect(requests.length).toBe(2);

expect(requests[0].request.body).toEqual({ comment: firstComment


});

expect(requests[1].request.body).toEqual({ comment: secondComment


});

requests[0].flush({ success: true });

requests[1].flush({ success: true });

We verify the number of requests and also the body of each


request. If these checks pass, we answer each request.

🔗 Angular API reference: HttpRequest


🔗 Angular API reference: TestRequest

Testing Services: Summary


All in all, testing Services is easier than testing other Angular
application parts. Most Services have a clear purpose and a well-
defined public API.

If the Service under test depends on another Service, a unit test


needs to the fake the dependency. This is probably the hardest
part, but takes the same effort as faking Services that are
Component dependencies.

PREDEFINED TESTING MODULES

Angular ships with crucial Services that are commonly used in


your own Services. Since Angular intends to be testable, Angular
also offers tools to replace them with fakes.

We have used the HttpClientTestingModule for testing a


Service that depends on HttpClient. To name another
example, there is the RouterTestingModule for testing
Services that depend on Router and Location.
Testing Pipes
LEARNING OBJECTIVES

Verifying the output of synchronous, pure Pipes


Testing asynchronous, impure Pipes that load data from a Service

An Angular Pipe is a special function that is called from a


Component template. Its purpose is to transform a value: You
pass a value to the Pipe, the Pipe computes a new value and
returns it.

The name Pipe originates from the vertical bar “|” that sits
between the value and the Pipe’s name. The concept as well as
the “|” syntax originate from Unix pipes and Unix shells.

In this example, the value from user.birthday is transformed


by the date Pipe:

{{ user.birthday | date }}

FORMATTING

Pipes are often used for internationalization, including


translation of labels and messages, formatting of dates, times
and various numbers. In these cases, the Pipe input value should
not be shown to the user. The output value is user-readable.
Examples for built-in Pipes are DatePipe, CurrencyPipe and
DecimalPipe. They format dates, amounts of money and
numbers, respectively, according to the localization settings.
Another well-known Pipe is the AsyncPipe which unwraps an
Observable or Promise.

PURE PIPES

Most Pipes are pure, meaning they merely take a value and
compute a new value. They do not have side effects: They do not
change the input value and they do not change the state of
other application parts. Like pure functions, pure Pipes are
relatively easy to test.

GreetPipe
Let us study the structure of a Pipe first to find ways to test it. In
essence, a Pipe is class with a public transform method. Here is
a simple Pipe that expects a name and greets the user.

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({ name: 'greet' })

export class GreetPipe implements PipeTransform {

transform(name: string): string {

return `Hello, ${name}!`;

In a Component template, we transform a value using the Pipe:

{{ 'Julie' | greet }}

The GreetPipe take the string 'Julie' and computes a new


string, 'Hello, Julie!'.

SIMPLE VS. COMPLEX SETUP

There are two important ways to test a Pipe:

1. Create an instance of the Pipe class manually. Then call the


transform method.

This way is fast and straight-forward. It requires minimal


setup.

2. Set up a TestBed. Render a host Component that uses the


Pipe. Then check the text content in the DOM.

This way closely mimics how the Pipe is used in practice. It


also tests the name of the Pipe, as declared in the @Pipe()
decorator.

Both ways allow to test Pipes that depend on Services. Either we


provide the original dependencies, writing an integration test. Or
we provide fake dependencies, writing a unit test.
GreetPipe test
The GreetPipe does not have any dependencies. We opt for the
first way and write a unit test that examines the single instance.

First, we create a Jasmine test suite. In a beforeEach block, we


create an instance of GreetPipe. In the specs, we scrutinize the
transform method.

describe('GreetPipe', () => {

let greetPipe: GreetPipe;

beforeEach(() => {

greetPipe = new GreetPipe();

});

it('says Hello', () => {

expect(greetPipe.transform('Julie')).toBe('Hello, Julie!');

});

});

We call the transform method with the string 'Julie' and


expect the output 'Hello, Julie!'.

This is everything that needs to be tested in the GreetPipe


example. If the transform method contains more logic that
needs to be tested, we add more specs that call the method with
different input.
Testing Pipes with dependencies
Many Pipes depend on local settings, including the user
interface language, date and number formatting rules, as well as
the selected country, region or currency.

We are introducing and testing the TranslatePipe, a complex


Pipe with a Service dependency.

🔗 TranslatePipe: Source code


🔗 TranslatePipe: Run the app
The example application lets you change the user interface
language during runtime. A popular solution for this task is the
ngx-translate library. For the purpose of this guide, we will adopt
ngx-translate’s proven approach but implement and test the
code ourselves.

TranslateService
The current language is stored in the TranslateService. This
Service also loads and holds the translations for the current
language.

The translations are stored in a map of keys and translation


strings. For example, the key greeting translates to “Hello!” if
the current language is English.

The TranslateService looks like this:

import { HttpClient } from '@angular/common/http';

import { EventEmitter, Injectable } from '@angular/core';

import { Observable, of } from 'rxjs';

import { map, take } from 'rxjs/operators';

export interface Translations {

[key: string]: string;

@Injectable()

export class TranslateService {

/** The current language */

private currentLang = 'en';

/** Translations for the current language */

private translations: Translations | null = null;

/** Emits when the language change */

public onTranslationChange = new EventEmitter<Translations>();

constructor(private http: HttpClient) {

this.loadTranslations(this.currentLang);

/** Changes the language */

public use(language: string): void {

this.currentLang = language;

this.loadTranslations(language);

/** Translates a key asynchronously */

public get(key: string): Observable<string> {

if (this.translations) {

return of(this.translations[key]);

return this.onTranslationChange.pipe(

take(1),

map((translations) => translations[key])

);

/** Loads the translations for the given language */

private loadTranslations(language: string): void {

this.translations = null;

this.http

.get<Translations>(`assets/${language}.json`)

.subscribe((translations) => {

this.translations = translations;

this.onTranslationChange.emit(translations);

});

This is what the Service provides:

1. use method: Set the current language and load the


translations as JSON via HTTP.

2. get method: Get the translation for a key.

3. onTranslationChange EventEmitter: Observing changes


on the translations as a result of use.
In the example project, the AppComponent depends on the
TranslateService. On creation, the Service loads the English
translations. The AppComponent renders a select field allowing
the user to change the language.

🔗 TranslateService: implementation code


🔗 TranslateService: test code

TranslatePipe
To show a translated label, a Component could call the Service’s
get method manually for each translation key. Instead, we
introduce the TranslatePipe to do the heavy lifting. It lets us
write:

{{ 'greeting' | translate }}

This translates the key 'greeting'.

Here is the code:

import {

ChangeDetectorRef,

OnDestroy,

Pipe,

PipeTransform,

} from '@angular/core';

import { Subscription } from 'rxjs';

import { TranslateService } from './translate.service';

@Pipe({

name: 'translate',

pure: false,

})

export class TranslatePipe implements PipeTransform, OnDestroy {

private lastKey: string | null = null;

private translation: string | null = null;

private onTranslationChangeSubscription: Subscription;

private getSubscription: Subscription | null = null;

constructor(

private changeDetectorRef: ChangeDetectorRef,

private translateService: TranslateService

) {

this.onTranslationChangeSubscription =

this.translateService.onTranslationChange.subscribe(

() => {

if (this.lastKey) {

this.getTranslation(this.lastKey);

);

public transform(key: string): string | null {

if (key !== this.lastKey) {

this.lastKey = key;

this.getTranslation(key);

return this.translation;

private getTranslation(key: string): void {

this.getSubscription?.unsubscribe();

this.getSubscription = this.translateService

.get(key)

.subscribe((translation) => {

this.translation = translation;

this.changeDetectorRef.markForCheck();

this.getSubscription = null;

});

public ngOnDestroy(): void {

this.onTranslationChangeSubscription.unsubscribe();

this.getSubscription?.unsubscribe();

ASYNC TRANSLATION

The TranslatePipe is impure because the translations are


loaded asynchronously. When called the first time, the
transform method cannot return the correct translation
synchronously. It calls the TranslateService’s get method
which returns an Observable.

TRIGGER CHANGE DETECTION

Once the translation is loaded, the TranslatePipe saves it and


notifies the Angular change detector. In particular, it marks the
corresponding view as changed by calling
ChangeDetectorRef’s markForCheck method.
In turn, Angular re-evaluates every expression that uses the
Pipe, like 'greeting' | translate, and calls the transform
method again. Finally, transform returns the right translation
synchronously.

TRANSLATION CHANGES

The same process happens when the user changes the language
and new translations are loaded. The Pipe subscribes to
TranslateService’s onTranslationChange and calls the
TranslateService again to get the new translation.

🔗 TranslatePipe: implementation code


🔗 Angular API reference: ChangeDetectorRef

TranslatePipe test
Now let us test the TranslatePipe! We can either write a test
that integrates the TranslateService dependency. Or we
write a unit test that replaces the dependency with a fake.

TranslateService performs HTTP requests to load the


translations. We should avoid these side effects when testing
TranslatePipe. So let us fake the Service to write a unit test.

let translateService: Pick<

TranslateService, 'onTranslationChange' | 'get'

>;

/* … */

translateService = {

onTranslationChange: new EventEmitter<Translations>(),

get(key: string): Observable<string> {

return of(`Translation for ${key}`);

},

};

The fake is a partial implementation of the original. The


TranslatePipe under test only needs the
onTranslationChange property and the get method. The
latter returns a fake translation including the key so we can test
that the key was passed correctly.

HOST COMPONENT

Now we need to decide whether to test the Pipe directly or


within a host Component. Neither solution is significantly easier
or more robust. You will find both solutions in the example
project. In this guide, we will discuss the solution with TestBed
and host Component.

Let us start with the host Component:

const key1 = 'key1';

const key2 = 'key2';

@Component({

template: '{{ key | translate }}',

})

class HostComponent {

public key = key1;

This Component uses the TranslatePipe to translate its key


property. Per default, it is set to key1. There is also a second
constant key2 for testing the key change later.

Let us set up the test suite:

describe('TranslatePipe: with TestBed and HostComponent', () => {

let fixture: ComponentFixture<HostComponent>;

let translateService: Pick<

TranslateService, 'onTranslationChange' | 'get'

>;

beforeEach(async () => {

translateService = {

onTranslationChange: new EventEmitter<Translations>(),

get(key: string): Observable<string> {

return of(`Translation for ${key}`);

},

};

await TestBed.configureTestingModule({

declarations: [TranslatePipe, HostComponent],

providers: [

{ provide: TranslateService, useValue: translateService }

],

}).compileComponents();

translateService = TestBed.inject(TranslateService);

fixture = TestBed.createComponent(HostComponent);

});

/* … */

});

In the testing Module, we declare the Pipe under test and the
HostComponent. For the TranslateService, we provide a fake
object instead. Just like in a Component test, we create the
Component and examine the rendered DOM.

SYNC AND ASYNC TRANSLATION

What needs to be tested? We need to check that {{ key |


translate }} evaluates to Translation for key1. There are
two cases that need to be tested though:

1. The translations are already loaded. The Pipe’s transform


method returns the correct translation synchronously. The
Observable returned by TranslateService’s get emits the
translation and completes immediately.

2. The translations are pending. transform returns null (or an


outdated translation). The Observable completes at any time
later. Then, the change detection is triggered, transform is
called the second time and returns the correct translation.

In the test, we write specs for both scenarios:

it('translates the key, sync service response', /* … */);

it('translates the key, async service response', /* … */);

Let us start with the first case. The spec is straight-forward.

it('translates the key, sync service response', () => {

fixture.detectChanges();

expectContent(fixture, 'Translation for key1');

});

Remember, the TranslateService fake returns an Observable


created with of.

return of(`Translation for ${key}`);

This Observable emits one value and completes immediately.


This mimics the case in which the Service has already loaded the
translations.

We merely need to call detectChanges. Angular calls the Pipe’s


transform method, which calls TranslateService’s get. The
Observable emits the translation right away and transform
passes it through.

Finally, we use the expectContent Component helper to test


the DOM output.

SIMULATE DELAY

Testing the second case is trickier because the Observable needs


to emit asynchronously. There are numerous ways to achieve
this. We will use the RxJS delay operator for simplicity.
At the same time, we are writing an asynchronous spec. That is,
Jasmine needs to wait for the Observable and the expectations
before the spec is finished.

FAKEASYNC AND TICK

Again, there are several ways how to accomplish this. We are


going to use Angular’s fakeAsync and tick functions. We have
introduced them when testing a form with async validators.

A quick recap: fakeAsync freezes time and prevents


asynchronous tasks from being executed. The tick function
then simulates the passage of time, executing the scheduled
tasks.

fakeAsync wraps the function passed to it:

it('translates the key, async service response', fakeAsync(() => {

/* … */

});

Next, we need to change the TranslateService’s get method


to make it asynchronous.

it('translates the key, async service response', fakeAsync(() => {

translateService.get = (key) =>

of(`Async translation for ${key}`).pipe(delay(100));

/* … */

});

DELAY OBSERVABLE
We still use of, but we delay the output by 100 milliseconds. The
exact number does not matter as long as there is some delay
greater or equal 1.

Now, we can call detectChanges for the first time.

it('translates the key, async service response', fakeAsync(() => {

translateService.get = (key) =>

of(`Async translation for ${key}`).pipe(delay(100));

fixture.detectChanges();

/* … */

});

The Pipe’s transform method is called for the first time and
returns null since the Observable does not emit a value
immediately.

So we expect that the output is empty:

it('translates the key, async service response', fakeAsync(() => {

translateService.get = (key) =>

of(`Async translation for ${key}`).pipe(delay(100));

fixture.detectChanges();

expectContent(fixture, '');

/* … */

});

LET TIME PASS

Here comes the interesting part. We want the Observable to


emit a value now. We simulate the passage of 100 milliseconds
with tick(100).
it('translates the key, async service response', fakeAsync(() => {

translateService.get = (key) =>

of(`Async translation for ${key}`).pipe(delay(100));

fixture.detectChanges();

expectContent(fixture, '');

tick(100);

/* … */

});

This causes the Observable to emit the translation and complete.


The Pipe receives the translation and saves it.

To see a change in the DOM, we start a second change


detection. The Pipe’s transform method is called for the second
time and returns the correct translation.

it('translates the key, async service response', fakeAsync(() => {

translateService.get = (key) =>

of(`Async translation for ${key}`).pipe(delay(100));

fixture.detectChanges();

expectContent(fixture, '');

tick(100);

fixture.detectChanges();

expectContent(fixture, 'Async translation for key1');

}));

Testing these details may seem pedantic at first. But the logic in
TranslatePipe exists for a reason.

There are two specs left to write:


it('translates a changed key', /* … */);

it('updates on translation change', /* … */);

The TranslatePipe receives the translation asynchronously


and stores both the key and the translation. When Angular calls
transform with the same key again, the Pipe returns the
translation synchronously. Since the Pipe is marked as impure,
Angular does not cache the transform result.

DIFFERENT KEY

When translate is called with a different key, the Pipe needs to


fetch the new translation. We simulate this case by changing the
HostComponent’s key property from key1 to key2.

it('translates a changed key', () => {

fixture.detectChanges();

fixture.componentInstance.key = key2;

fixture.detectChanges();

expectContent(fixture, 'Translation for key2');

});

After a change detection, the DOM contains the updated


translation for key2.

TRANSLATION CHANGE

Last but no least, the Pipe needs to fetch a new translation from
the TranslateService when the user changes the language
and new translations have been loaded. For this purpose, the
Pipe subscribes to the Service’s onTranslationChange emitter.
Our TranslateService fake supports onTranslationChange
as well, hence we call the emit method to simulate a translation
change. Before, we let the Service return a different translation
in order to see a change in the DOM.

it('updates on translation change', () => {

fixture.detectChanges();

translateService.get = (key) =>

of(`New translation for ${key}`);

translateService.onTranslationChange.emit({});

fixture.detectChanges();

expectContent(fixture, 'New translation for key1');

});

We made it! Writing these specs is challenging without doubt.

TranslateService and TranslatePipe are non-trivial


examples with a proven API. The original classes from ngx-
translate are more powerful. If you look for a robust and flexible
solution, you should use the ngx-translate library directly.

🔗 TranslatePipe: test code


🔗 Angular API reference: fakeAsync
🔗 Angular API reference: tick
🔗 RxJS: delay operator
🔗 ngx-translate
Testing Directives
LEARNING OBJECTIVES

Testing the effect of an Attribute Directive


Testing complex Structural Directives with Inputs and templates
Providing a host Component for testing Attribute and Structural Directives

Angular beginners quickly encounter four core concepts:


Modules, Components, Services and Pipes. A lesser known core
concept are Directives. Without knowing, even beginners are
using Directives, because Directives are everywhere.

In Angular, there are three types of Directives:

1. A Component is a Directive with a template. A Component


typically uses an element type selector, like app-counter.
Angular then looks for app-counter elements and renders
the Component template into these host elements.

2. An Attribute Directive adds logic to an existing host element


in the DOM. Examples for built-in Attribute Directives are
NgClass and NgStyle.

3. A Structural Directive alters the structure of the DOM,


meaning it adds and removes elements programmatically.
Examples for built-in Structural Directives are NgIf, NgFor
and NgSwitch.
We have already tested Components. We have yet to test the two
other types of Directives.

Testing Attribute Directives


The name Attribute Directive comes from the attribute selector,
for example [ngModel]. An Attribute Directive does not have a
template and cannot alter the DOM structure.

We have already mentioned the built-in Attribute Directives


NgClass and NgStyle. In addition, both Template-driven and
Reactive Forms rely heavily on Attribute Directives: NgForm,
NgModel, FormGroupDirective, FormControlName, etc.

STYLING LOGIC

Attributes Directives are often used for changing the style of an


element, either directly with inline styles or indirectly with
classes.

Most styling logic can be implemented using CSS alone, no


JavaScript code is necessary. But sometimes JavaScript is
required to set inline styles or add classes programmatically.

ThresholdWarningDirective
None of our example applications contain an Attribute Directive,
so we are introducing and testing the
ThresholdWarningDirective.

This Directive applies to <input type="number"> elements. It


toggles a class if the picked number exceeds a given threshold. If
the number is higher than the threshold, the field should be
marked visually.

Note that numbers above the threshold are valid input. The
ThresholdWarningDirective does not add a form control
validator. We merely want to warn the user so they check the
input twice.

🔗 ThresholdWarningDirective: Source code


🔗 ThresholdWarningDirective: Run the app
Enter a number greater than 10 to see the effect.

This is the Directive’s code:

import {

Directive, ElementRef, HostBinding, HostListener, Input

} from '@angular/core';

@Directive({

selector: '[appThresholdWarning]',

})

export class ThresholdWarningDirective {

@Input()

public appThresholdWarning: number | null = null;

@HostBinding('class.overThreshold')

public overThreshold = false;

@HostListener('input')

public inputHandler(): void {

this.overThreshold =

this.appThresholdWarning !== null &&

this.elementRef.nativeElement.valueAsNumber >
this.appThresholdWarning;

constructor(private elementRef: ElementRef<HTMLInputElement>) {}

This is how we apply the Directive to an element:

<input type="number" [appThresholdWarning]="10" />

This means: If the user enters a number that is greater than 10,
mark the field with a visual warning.

One bit is missing: the styles for the visual warning.

input[type='number'].overThreshold {

background-color: #fe9;

Before we write the test for the Directive, let us walk through the
implementation parts.

INPUT OF THE SAME NAME


The ThresholdWarningDirective is applied with an attribute
binding [appThresholdWarning]="…". It receives the attribute
value as an Input of the same name. This is how the threshold is
configured.

@Input()

public appThresholdWarning: number | null = null;

INPUT EVENT

Using HostListener, the Directive listens for input event on


the host element. When the user changes the field value, the
inputHandler method is called.

The inputHandler gets the field value and checks whether it is


over the threshold. The result is stored in the overThreshold
boolean property.

@HostListener('input')

public inputHandler(): void {

this.overThreshold =

this.appThresholdWarning !== null &&

this.elementRef.nativeElement.valueAsNumber >
this.appThresholdWarning;

READ VALUE

To access the host element, we use the ElementRef


dependency. ElementRef is a wrapper around the host
element’s DOM node. this.elementRef.nativeElement
yields the input element’s DOM node. valueAsNumber contains
the input value as a number.

TOGGLE CLASS

Last but not least, the overThreshold property is bound to a


class of the same name using HostBinding. This is how the
class is toggled.

@HostBinding('class.overThreshold')

public overThreshold = false;

ThresholdWarningDirective test
Now that we understand what is going on, we need to replicate
the workflow in our test.

HOST COMPONENT

First of all, Attribute and Structural Directives need an existing


host element they are applied to. When testing these Directives,
we use a host Component that renders the host element. For
example, the ThresholdWarningDirective needs an <input
type="number"> host element.

@Component({

template: `

<input type="number"

[appThresholdWarning]="10" />

})

class HostComponent {}

We are going to render this Component. We need a standard


Component test setup using the TestBed.

describe('ThresholdWarningDirective', () => {

let fixture: ComponentFixture<HostComponent>;

beforeEach(async () => {

await TestBed.configureTestingModule({

declarations: [ThresholdWarningDirective, HostComponent],

}).compileComponents();

fixture = TestBed.createComponent(HostComponent);

fixture.detectChanges();

});

/* … */

});

When configuring the testing Module, we declare both the


Directive under test and the host Component. Just like in a
Component test, we render the Component and obtain a
ComponentFixture.

FIND INPUT ELEMENT

In the following specs, we need to access the input element. We


use the standard approach: a data-testid attribute and the
findEl testing helper.
For convenience, we pick the input element in the beforeEach
block. We save it in a shared variable named input.

@Component({

template: `

<input type="number"

[appThresholdWarning]="10"

data-testid="input" />

})

class HostComponent {}

describe('ThresholdWarningDirective', () => {

let fixture: ComponentFixture<HostComponent>;

let input: HTMLInputElement;

beforeEach(async () => {

await TestBed.configureTestingModule({

declarations: [ThresholdWarningDirective, HostComponent],

}).compileComponents();

fixture = TestBed.createComponent(HostComponent);

fixture.detectChanges();

input = findEl(fixture, 'input').nativeElement;

});

/* … */

});

CHECK CLASS
The first spec ensures that the Directive does nothing when the
user has not touched the input. Using the element’s classList, we
expect the class overThreshold to be absent.

it('does not set the class initially', () => {

expect(input.classList.contains('overThreshold')).toBe(false);

});

The next spec enters a number over the threshold. To simulate


the user input, we use our handy testing helper
setFieldValue. Then, the spec expects the class to be present.

it('adds the class if the number is over the threshold', () => {

setFieldValue(fixture, 'input', '11');

fixture.detectChanges();

expect(input.classList.contains('overThreshold')).toBe(true);

});

setFieldValue triggers a fake input event. This triggers the


Directive’s event handler. 11 is greater than the threshold 10, so
the class is added. We still need to call detectChanges so the
DOM is updated.

The last spec makes sure that the threshold is still considered as
a safe value. No warning should be shown.

it('removes the class if the number is at the threshold', () => {

setFieldValue(fixture, 'input', '10');

fixture.detectChanges();

expect(input.classList.contains('overThreshold')).toBe(false);

});

This is it! Testing the ThresholdWarningDirective is like


testing a Component. The difference is that the Component
serves as a host for the Directive.

The full spec for the ThresholdWarningDirective looks like


this:

import { Component } from '@angular/core';

import { async, ComponentFixture, TestBed } from


'@angular/core/testing';

import { findEl, setFieldValue } from './spec-helpers/element.spec-


helper';

import { ThresholdWarningDirective } from './threshold-


warning.directive';

@Component({

template: `

<input type="number"

[appThresholdWarning]="10"

data-testid="input" />

})

class HostComponent {}

describe('ThresholdWarningDirective', () => {

let fixture: ComponentFixture<HostComponent>;

let input: HTMLInputElement;

beforeEach(async () => {

await TestBed.configureTestingModule({

declarations: [ThresholdWarningDirective, HostComponent],

}).compileComponents();

fixture = TestBed.createComponent(HostComponent);

fixture.detectChanges();

input = findEl(fixture, 'input').nativeElement;

});

it('does not set the class initially', () => {

expect(input.classList.contains('overThreshold')).toBe(false);

});

it('adds the class if the number is over the threshold', () => {

setFieldValue(fixture, 'input', '11');

fixture.detectChanges();

expect(input.classList.contains('overThreshold')).toBe(true);

});

it('removes the class if the number is at the threshold', () => {

setFieldValue(fixture, 'input', '10');

fixture.detectChanges();

expect(input.classList.contains('overThreshold')).toBe(false);

});

});

🔗 ThresholdWarningDirective: implementation code


🔗 ThresholdWarningDirective: test code

Testing Structural Directives


A Structural Directive does not have a template like a
Component, but operates on an internal ng-template. The
Directive renders the template into the DOM programmatically,
passing context data to the template.

RENDER TEMPLATE PROGRAMMATICALLY

The prime examples emonstrate what Structural Directives are


capable of:

The NgIf Directive decides whether the template is rendered


or not.

The NgFor Directive walks over a list of items and renders the
template repeatedly for each item.

A Structural Directive uses an attribute selector, like [ngIf]. The


attribute is applied to a host element with the special asterisk
syntax, for example *ngIf. Internally, this is translated to <ng-
template [ngIf]="…"> … </ng-template>.

This guide assumes that you roughly understand how Structural


Directives work and how the microsyntax translates to Directive
Inputs. Please refer to the comprehensive official guide on
Structural Directives.

PaginateDirective
We are introducing and testing the PaginateDirective, a
complex Structural Directive.

NGFOR WITH PAGINATION

PaginateDirective works similar to NgFor, but does not


render all list items at once. It spreads the items over pages,
usually called pagination.

Per default, only ten items are rendered. The user can turn the
pages by clicking on “next” or “previous” buttons.

🔗 PaginateDirective: Source code


🔗 PaginateDirective: Run the app
Before writing the test, we need to understand the outer
structure of PaginateDirective first.

The simplest use of the Directive looks like this:

<ul>

<li *appPaginate="let item of items">

{{ item }}

</li>

</ul>

This is similar to the NgFor directive. Assuming that items is an


array of numbers ([1, 2, 3, …]), the example above renders
the first 10 numbers in the array.
The asterisk syntax *appPaginate and the so-called microsyntax
let item of items is syntactic sugar. This is a shorter and
nicer way to write something complex. Internally, Angular
translates the code to the following:

<ng-template appPaginate let-item [appPaginateOf]="items">

<li>

{{ item }}

</li>

</ng-template>

There is an ng-template with an attribute appPaginate and an


attribute binding appPaginateOf. Also there is a template input
variable called item.

RENDER TEMPLATE FOR EACH ITEM

As mentioned, a Structural Directive does not have its own


template, but operates on an ng-template and renders it
programmatically. Our PaginateDirective works with the ng-
template shown above. The Directive renders the template for
each item on the current page.

Now that we have seen Angular’s internal representation, we can


understand the structure of the PaginateDirective class:

@Directive({

selector: '[appPaginate]',

})

export class PaginateDirective<T> implements OnChanges {

@Input()

public appPaginateOf: T[] = [];

/* … */

The Directive uses the [appPaginate] attribute selector and


has an Input called appPaginateOf. By writing the microsyntax
*appPaginate="let item of items", we actually set the
appPaginateOf Input to the value items.

DIRECTIVE INPUTS

The PaginateDirective has a configuration option named


perPage. It specifies how many items are visible per page.

Per default, there are ten items on a page. To change it, we set
perPage: … in the microsyntax:

<ul>

<li *appPaginate="let item of items; perPage: 5">

{{ item }}

</li>

</ul>

This translates to:

<ng-template

appPaginate

let-item

[appPaginateOf]="items"

[appPaginatePerPage]="5">

<li>

{{ item }}

</li>

</ng-template>

perPage translates to an Input named appPaginatePerPage in


the Directive’s code:

@Directive({

selector: '[appPaginate]',

})

export class PaginateDirective<T> implements OnChanges {

@Input()

public appPaginateOf: T[] = [];

@Input()

public appPaginatePerPage = 10;

/* … */

This is how built-in Structural Directives like NgIf and NgFor


work as well.

Now it gets more complicated. Since we want to paginate the


items, we need user controls to turn the pages – in addition to
rendering the items.

Again, a Structural Directive lacks a template.


PaginateDirective cannot render the “next” and “previous”
buttons itself. And to remain flexible, it should not render
specific markup. The Component that uses the Directive should
decide how the controls look.

PASS ANOTHER TEMPLATE

We solve this by passing the controls as a template to the


Directive. In particular, we pass a reference to a separate ng-
template. This will be the second template the Directive
operates on.

This is how the controls template could look like:

<ng-template

#controls

let-previousPage="previousPage"

let-page="page"

let-pages="pages"

let-nextPage="nextPage"

>

<button (click)="previousPage()">

Previous page

</button>

{{ page }} / {{ pages }}

<button (click)="nextPage()">

Next page

</button>

</ng-template>

#controls sets a template reference variable. This means we


can further reference the template by the name controls.

CONTEXT OBJECT
The Directive renders the controls template with a context object
that implements the following TypeScript interface:

interface ControlsContext {

page: number;

pages: number;

previousPage(): void;

nextPage(): void;

page is the current page number. pages is the total number of


pages. previousPage and nextPage are functions for turning
the pages.

USE PROPERTIES FROM CONTEXT

The ng-template takes these properties from the context and


saves them in local variables of the same name:

let-previousPage="previousPage"

let-page="page"

let-pages="pages"

let-nextPage="nextPage"

This means: Take the context property previousPage and make


it available in the template under the name previousPage. And
so on.

The content of the template is rather simple. It renders two


buttons for the page turning, using the functions as click
handlers. It outputs the current page number and the number of
total pages.

<button (click)="previousPage()">

Previous page

</button>

{{ page }} / {{ pages }}

<button (click)="nextPage()">

Next page

</button>

Last but not least, we pass the template to the


PaginateDirective using the microsyntax:

<ul>

<li *appPaginate="let item of items; perPage: 5; controls:


controls">

{{ item }}

</li>

</ul>

This translates to:

<ng-template

appPaginate

let-item

[appPaginateOf]="items"

[appPaginatePerPage]="5"

[appPaginateControls]="controls">

<li>

{{ item }}

</li>

</ng-template>

controls: … in the microsyntax translates to an Input named


appPaginateControls. This concludes the Directive‘s outer
structure:

@Directive({

selector: '[appPaginate]',

})

export class PaginateDirective<T> implements OnChanges {

@Input()

public appPaginateOf: T[] = [];

@Input()

public appPaginatePerPage = 10;

@Input()

public appPaginateControls?: TemplateRef<ControlsContext>;

/* … */

The inner workings of the PaginateDirective are not relevant


for testing, so we will not discuss them in detail here. Please
refer to the Angular guide Write a structural directive for a
general explanation.

🔗 PaginateDirective: implementation code

PaginateDirective test
We have explored all features of PaginateDirective and are
now ready to test them!

HOST COMPONENT

First, we need a host Component that applies the Structural


Directive under test. We let it render a list of ten numbers, three
numbers on each page.

const items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

@Component({

template: `

<ul>

<li

*appPaginate="let item of items; perPage: 3"

data-testid="item"

>

{{ item }}

</li>

</ul>

`,

})

class HostComponent {

public items = items;

CONTROLS TEMPLATE

Since we also want to test the custom controls feature, we need


to pass a controls template. We will use the simple controls
discussed above.
const items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

@Component({

template: `

<ul>

<li

*appPaginate="let item of items; perPage: 3; controls:


controls"

data-testid="item"

>

{{ item }}

</li>

</ul>

<ng-template

#controls

let-previousPage="previousPage"

let-page="page"

let-pages="pages"

let-nextPage="nextPage"

>

<button

(click)="previousPage()"

data-testid="previousPage">

Previous page

</button>

<span data-testid="page">{{ page }}</span>

<span data-testid="pages">{{ pages }}</span>

<button

(click)="nextPage()"

data-testid="nextPage">

Next page

</button>

</ng-template>

`,

})

class HostComponent {

public items = items;

The template code already contains data-testid attributes.


This is how we find and examine the elements in the test (see
Querying the DOM with test ids).

This is quite a setup, but after all, we want to test the


PaginateDirective under realistic conditions.

The test suite configures a testing Module, declares both the


HostComponent and the PaginateDirective and renders the
HostComponent:

describe('PaginateDirective', () => {

let fixture: ComponentFixture<HostComponent>;

beforeEach(async () => {

await TestBed.configureTestingModule({

declarations: [PaginateDirective, HostComponent],

}).compileComponents();

fixture = TestBed.createComponent(HostComponent);

fixture.detectChanges();

});

/* … */

});

This is a standard Component test setup – nothing special yet.

The first spec verifies that the Directive renders the items on the
first page, in our case the numbers 1, 2 and 3.

We have marked the item element with data-testid="item".


We use the findEls test helper to find all elements with said
test id.

EXPECT RENDERED ITEMS

We expect to find three items. Then we examine the text content


of each item and expect that it matches the item in the number
list, respectively.

it('renders the items of the first page', () => {

const els = findEls(fixture, 'item');

expect(els.length).toBe(3);

expect(els[0].nativeElement.textContent.trim()).toBe('1');

expect(els[1].nativeElement.textContent.trim()).toBe('2');

expect(els[2].nativeElement.textContent.trim()).toBe('3');

});

Already, the expectations are repetitive and hard to read. So we


introduce a little helper function.

function expectItems(

elements: DebugElement[],

expectedItems: number[],

): void {

elements.forEach((element, index) => {

const actualText = element.nativeElement.textContent.trim();

expect(actualText).toBe(String(expectedItems[index]));

});

This lets us rewrite the spec so it is easier to grasp:

it('renders the items of the first page', () => {

const els = findEls(fixture, 'item');

expect(els.length).toBe(3);

expectItems(els, [1, 2, 3]);

});

CHECK CONTROLS

The next spec proves that the controls template is rendered


passing the current page and the total number of pages.

The elements have have a data-testid="page" and data-


testid="pages", respectively. We use the expectText testing
helper to check their text content.

it('renders the current page and total pages', () => {

expectText(fixture, 'page', '1');

expectText(fixture, 'pages', '4');

});

Three more specs deal with the controls for turning pages. Let
us start with the “next” button.

it('shows the next page', () => {

click(fixture, 'nextPage');

fixture.detectChanges();

const els = findEls(fixture, 'item');

expect(els.length).toBe(3);

expectItems(els, [4, 5, 6]);

});

TURN PAGES

We simulate a click on the “next” button using the click testing


helper. Then we start Angular’s change detection so the
Component together with the Directive are re-rendered.

Finally, we verify that the Directive has rendered the next three
items, the numbers 4, 5 and 6.

The spec for the “previous” button looks similar. First, we jump to
the second page, then back to the first page.

it('shows the previous page', () => {

click(fixture, 'nextPage');

click(fixture, 'previousPage');

fixture.detectChanges();

const els = findEls(fixture, 'item');

expect(els.length).toBe(3);

expectItems(els, [1, 2, 3]);

});

STRESS TEST

We have now covered the Directive’s important behavior. Time


for testing edge cases! Does the Directive behave correctly if we
click on the “previous” button on the first page and the “next”
button on the last page?

it('checks the pages bounds', () => {

click(fixture, 'nextPage'); // -> 2

click(fixture, 'nextPage'); // -> 3

click(fixture, 'nextPage'); // -> 4

click(fixture, 'nextPage'); // -> 4

click(fixture, 'previousPage'); // -> 3

click(fixture, 'previousPage'); // -> 2

click(fixture, 'previousPage'); // -> 1

click(fixture, 'previousPage'); // -> 1

fixture.detectChanges();

// Expect that the first page is visible again

const els = findEls(fixture, 'item');

expect(els.length).toBe(3);

expectItems(els, [1, 2, 3]);

});

By clicking on the buttons, we jump forward to the last page and


backward to the first page again.

This is it! Here is the full test code:

import { Component, DebugElement } from '@angular/core';

import { async, ComponentFixture, TestBed } from


'@angular/core/testing';

import {

findEls,

expectText,

click,

} from './spec-helpers/element.spec-helper';

import { PaginateDirective } from './paginate.directive';

const items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

@Component({

template: `

<ul>

<li

*appPaginate="let item of items; perPage: 3; controls:


controls"

data-testid="item"

>

{{ item }}

</li>

</ul>

<ng-template

#controls

let-previousPage="previousPage"

let-page="page"

let-pages="pages"

let-nextPage="nextPage"

>

<button (click)="previousPage()" data-testid="previousPage">

Previous page

</button>

<span data-testid="page">{{ page }}</span>

<span data-testid="pages">{{ pages }}</span>

<button (click)="nextPage()" data-testid="nextPage">

Next page

</button>

</ng-template>

`,

})

class HostComponent {

public items = items;

function expectItems(

elements: DebugElement[],

expectedItems: number[],

): void {

elements.forEach((element, index) => {

const actualText = element.nativeElement.textContent.trim();

expect(actualText).toBe(String(expectedItems[index]));

});

describe('PaginateDirective', () => {

let fixture: ComponentFixture<HostComponent>;

beforeEach(async () => {

await TestBed.configureTestingModule({

declarations: [PaginateDirective, HostComponent],

}).compileComponents();

fixture = TestBed.createComponent(HostComponent);

fixture.detectChanges();

});

it('renders the items of the first page', () => {

const els = findEls(fixture, 'item');

expect(els.length).toBe(3);

expectItems(els, [1, 2, 3]);

});

it('renders the current page and total pages', () => {

expectText(fixture, 'page', '1');

expectText(fixture, 'pages', '4');

});

it('shows the next page', () => {

click(fixture, 'nextPage');

fixture.detectChanges();

const els = findEls(fixture, 'item');

expect(els.length).toBe(3);

expectItems(els, [4, 5, 6]);

});

it('shows the previous page', () => {

click(fixture, 'nextPage');

click(fixture, 'previousPage');

fixture.detectChanges();

const els = findEls(fixture, 'item');

expect(els.length).toBe(3);

expectItems(els, [1, 2, 3]);

});

it('checks the pages bounds', () => {

click(fixture, 'nextPage'); // -> 2

click(fixture, 'nextPage'); // -> 3

click(fixture, 'nextPage'); // -> 4

click(fixture, 'previousPage'); // -> 3

click(fixture, 'previousPage'); // -> 2

click(fixture, 'previousPage'); // -> 1

fixture.detectChanges();

// Expect that the first page is visible again

const els = findEls(fixture, 'item');

expect(els.length).toBe(3);

expectItems(els, [1, 2, 3]);

});

});

PaginateDirective is a complex Structural Directive that


requires a complex test setup. Once we have created a suitable
host Component, we can test it using our familiar testing
helpers. The fact that the logic resides in the Directive is not
relevant for the specs.

🔗 PaginateDirective: implementation code


🔗 PaginateDirective: test code
Testing Modules
LEARNING OBJECTIVES

Deciding whether and how to test Angular Modules


Writing smoke tests to catch Module errors early

Modules are central parts of Angular applications. Often they


contain important setup code. Yet they are hard to test since
there is no typical logic, only sophisticated configuration.

ONLY METADATA

Angular Modules are classes, but most of the time, the class
itself is empty. The essence lies in the metadata set with
@NgModule({ … }).

We could sneak into the metadata and check whether certain


Services are provided, whether third-party Modules are imported
and whether Components are exported.

But such a test would simply mirror the implementation. Code


duplication does not give you more confidence, it only increases
the cost of change.

Should we write tests for Modules at all? If there is a reference


error in the Module, the compilation step (ng build) fails before
the automated tests scrutinize the build. “Failing fast” is good
from a software quality perspective.
SMOKE TEST

There are certain Module errors that only surface during


runtime. These can be caught with a smoke test. Given this
Module:

import { NgModule } from '@angular/core';

import { CommonModule } from '@angular/common';

import { ExampleComponent } from './example.component';

@NgModule({

declarations: [ExampleComponent],

imports: [CommonModule],

})

export class FeatureModule {}

We write this smoke test:

import { TestBed } from '@angular/core/testing';

import { FeatureModule } from './example.module';

describe('FeatureModule', () => {

beforeEach(() => {

TestBed.configureTestingModule({

imports: [FeatureModule],

});

});

it('initializes', () => {

const module = TestBed.inject(FeatureModule);

expect(module).toBeTruthy();

});

});

The integration test uses the TestBed to import the Module


under test. It verifies that no error occurs when importing the
Module.
Measuring code coverage
LEARNING OBJECTIVES

Understanding the code coverage metric


Generating and inspecting a code coverage report
Finding code that is not yet covered by automated tests
Enforcing code coverage thresholds and improving code coverage

Code coverage, also called test coverage, tells you which parts of
your code are executed by running the unit and integration
tests. Code coverage is typically expressed as percent values, for
example, 79% statements, 53% branches, 74% functions, 78%
lines.

Statements are, broadly speaking, control structures like if and


for as well as expressions separated by semicolon. Branches
refers to the two branches of if (…) {…} else {…} and … ?
… : … conditions. Functions and lines are self-explanatory.

Coverage report
In Angular’s Karma and Jasmine setup, Istanbul is used for
measuring test coverage. Istanbul rewrites the code under test
to record whether a statement, branch, function and line was
called. Then it produces a comprehensive test report.
To activate Istanbul when running the tests, add the --code-
coverage parameter:

ng test --code-coverage

After the tests have completed, Istanbul saves the report in the
coverage directory located in the Angular project directory.

The report is a bunch of HTML files you can open with a browser.
Start by opening coverage/index.html in the browser of your
choice.

The report for the Flickr search example looks like this:
Istanbul creates an HTML page for every directory and every file.
By following the links, you can descend to reports for the
individual files.

For example, the coverage report for photo-item.component.ts


of the Flickr search:

The report renders the source code annotated with the


information how many times a line was called. In the example
above, the code is fully covered except for an irrelevant else
branch, marked with an “E”.

The spec it('focusses a photo on click', () => {…})


clicks on the photo item to test whether the focusPhoto Output
emits. Let us disable the spec on purpose to see the impact.

You can tell from the coverage report above that the
handleClick method is never called. A key Component
behavior is untested.
How to use the coverage report
Now that we know how to generate the report, what should we
do with it?

In the chapter The right amount of testing, we have identified


code coverage as a useful, but flawed metric. As a quantitative
measure, code coverage cannot assess the quality of your tests.

Software testing is not a competition. We should not try to reach


a particular score just for the sake of it. For what purpose are we
measuring code coverage then?

FIND UNCOVERED CODE

The coverage report is a valuable tool you should use while


writing tests. It reveals code behavior that is not yet tested. The
report not only guides your testing, it also deepens your
understanding of how your tests work.

Whatever your current coverage score is, use the reporting to


monitor and improve your testing practice. As described in
Tailoring your testing approach, testing should be part of the
development routine. New features should include tests, bug
fixes should include a test as proof and to prevent regressions.

IMPROVE COVERAGE
Writing new code and changing existing code should not lower
the coverage score, but gradually increase it. This means if your
existing tests cover 75% lines of code, new code needs to be at
least 75% covered. Otherwise the score slowly deteriorates.

It is common practice to run the unit and integration tests in a


continuous integration environment and measure the code
coverage. To enforce a certain coverage score and to prevent
decline, you can configure thresholds in the Karma
configuration.

In karma.conf.js, you can add global thresholds for


statements, branches, functions and lines.

coverageReporter: {

/* … */

check: {

global: {

statements: 75,

branches: 75,

functions: 75,

lines: 75,

},

},

},

In the configuration above, all values are set to 75%. If the


coverage drops below that number, the test execution fails even
if all specs succeeded.
RAISE THE BAR

When new code is added to the project with a test coverage


better than average, you can raise the thresholds slowly but
steadily – for example, from 75 to 75.1, 75.2, 75.3 and so on.
Soon these small improvements add up.

Test coverage should not be a pointless competition that puts


developers under pressure and shames those that do not meet
an arbitrary mark. Measuring coverage is a tool you should use
for your benefit. Keep in mind that writing meaningful, spot-on
tests does not necessarily increase the coverage score.

For beginners and experts alike, the coverage report helps to set
up, debug and improve their tests. For experienced developers,
the score helps to keep up a steady testing practice.

🔗 karma-coverage Configuration
End-to-end testing
LEARNING OBJECTIVES

Writing valueable tests that cover all parts of your application


Understanding different approaches to end-to-end testing
Setting up Cypress for testing your Angular project
Orchestrating a web browser to load and inspect your application
Intercepting API calls to return fixed data

We have successfully written unit and integration tests using


Karma, Jasmine and Angular’s own testing tools. These precise
tests give confidence that a single application part – like a
Component or Service - or a group of connected parts work as
intended.

USER PERSPECTIVE

Karma and Jasmine tests take a technical perspective. They focus


on the front-end JavaScript code alone and run it in a controlled
and isolated test environment. What is really important though
is whether the whole application works for the user.

The most effective and reliable way to ensure a working


application is manual testing: A dedicated software tester walks
through the application feature by feature, case by case
according to a test plan.
Manual tests are slow, labor-intensive and cannot be repeated
often. They are unspecific from a developer perspective: If the
test fails, we cannot easily pin down which part of the application
is responsible or which code change causes the regression.

We need automated tests that take the user’s perspective. This is


what end-to-end (E2E) tests do.

Strengths of end-to-end tests


As discussed in distribution of testing efforts, all types of
automated tests have pros and cons. Unit and integration tests
are fast and reliable, but do not guarantee a working application.
End-to-end test are slow and often fail incorrectly, but they
assess the fitness of the application as a whole.

REAL CONDITIONS

When all parts of the application come together, a new type of


bug appears. Often these bugs have to do with timing and order
of events, like network latency and race conditions.

The unit and integration tests we wrote worked with a fake back-
end. We send fake HTTP requests and respond with fake data.
We made an effort to keep the originals and fakes on par.

FRONT-END AND BACK-END


It is much harder to keep the front-end code in sync with the
actual API endpoints and responses from the back-end. Even if
the front-end and the back-end share type information about the
transferred data, there will be mismatches.

It is the goal of end-to-end tests to catch these bugs that cannot


be caught by other automated tests.

Deployment for end-to-end tests


End-to-end tests require a testing environment that closely
resembles the production environment. You need to deploy the
full application, including the front-end and the relevant back-
end parts. For that purpose, back-end frameworks typically
support configurations for different environments, like
development, testing and production.

DETERMINISTIC ENVIRONMENT

The database needs to be filled with pre-fabricated fake data.


With each run of the end-to-end tests, you need to reset the
database to a defined initial state.

The back-end services need to answer requests with


deterministic responses. Third-party dependencies need to be
set up so they return realistic data but do not compromise
production data.

Since this guide is not about DevOps, we will not go into details
here and focus on writing end-to-end tests.

How end-to-end tests work


An end-to-end test mimics how a user interacts with the
application. Typically, the test engine launches an ordinary
browser and controls it remotely.

SIMULATE USER ACTIONS

Once the browser is started, the end-to-end test navigates to the


application’s URL, reads the page content and makes keyboard
and pointer input. For example, the test fills out a form and clicks
on the submit button.

Just like unit and integration tests, the end-to-end test then
makes expectations: Does the page include the right content?
Did the URL change? This way, whole features and user
interfaces are examined.

End-to-end testing frameworks


Frameworks for end-to-end tests allow navigating to URLs,
simulating user input and inspecting the page content. Apart
from that, they have little in common. The test syntax and the
way the tests are run differ widely.

There are two categories of end-to-end testing frameworks:


Those that use WebDriver and those that do not.

BROWSER AUTOMATION

The WebDriver protocol defines a way to control a browser


remotely with a set of commands. It originates from the
Selenium browser automation project and is now developed at
the World Wide Web Consortium (W3C).

All common browsers support the WebDriver protocol and can


be controlled remotely. The most important WebDriver
commands are:

Navigate to a given URL

Find one or more elements in the DOM

Get information about a found element:

Get an element attribute or property

Get the element’s text content


Click on an element

Send keyboard input to a form field

Execute arbitrary JavaScript code

WebDriver is a key technology most end-to-end testing


frameworks depend on. The main benefit of WebDriver is that
tests can be run in different browsers, even simultaneously.

WebDriver is a high-level, generic, HTTP-based protocol. It


connects the test running on one machine with a browser
possibly running on another machine. The level of control over
the browser is limited.

FLEXIBILITY VS. RELIABILITY

Not all frameworks build on WebDriver. Some frameworks


integrate more directly into the browser. This makes them more
reliable, less complex, but also less flexible since they only
support few browsers.

In this guide, we will learn about two frameworks, one of each


category:

Protractor, which is based on WebDriver,

Cypress, which does not use WebDriver.

🔗 WebDriver protocol
🔗 Protractor: Official web site
🔗 Cypress: Official web site

Introducing Protractor
Protractor is an end-to-end testing framework based on
WebDriver, made for Angular applications. Like the Angular
framework itself, Protractor originates from Google.

DEPRECATED

Up until Angular version 12, Protractor was the default end-to-


end testing framework in projects created with Angular CLI.
Since Angular 12, Protractor is deprecated. In new CLI projects,
there is no default end-to-end testing solution configured.

The main reason for Protractor’s deprecation is that it was not


maintained for years. During that time, new browser automation
standards and better end-to-end testing frameworks emerged.

Protractor has two outstanding features designed for testing


Angular applications. But as we will learn, you cannot benefit
from these optimizations any longer.

ANGULAR-SPECIFIC FEATURES

First, Protractor hooks into the Angular app under tests. After
sending a WebDriver command, Protractor waits for Angular to
update the page. Then it continues with the next command. By
synchronizing the test and the application, the test gets faster
and more reliable.

Second, Protractor has a feature called control flow. While


WebDriver commands are asynchronous by nature, the control
flow allows you to write tests as if they ran synchronously.

Protractor’s control flow implementation led to inconsistencies


and bugs. The underlying WebDriverJS library removed the
feature, so Protractor had to deprecate it as well. This means you
need to use async / await to explicitly wait for a command to
finish.

As a result, Protractor lost a useful feature. Protractor’s


contenders, namely Cypress and WebDriver.io, still allow to write
asynchronous test code in a synchronous manner.

Without the control flow, you practically need to disable the “wait
for Angular” feature as well. This means both key Protractor
features have lapsed.

NOT RECOMMENDED

In view of these events, this guide recommends against using


Protractor for new projects. Protractor is a solid project, but
today there is no compelling reason to choose Protractor over its
competitors.
TESTED WITH PROTRACTOR

If you are looking for Protractor examples, have a look at the


Protractor end-to-end tests for the Counter and Flickr search.
They are not explained in this guide though.

🔗 Counter: End-to-end tests with Protractor


🔗 Flickr search: End-to-end tests with Protractor

Introducing Cypress
Cypress is an end-to-end testing framework that is not based on
WebDriver. There are no Angular-specific features. Any web site
can be tested with Cypress.

REPRODUCIBLE TESTS

WebDriver-based testing solutions are flexible and powerful but


turned out to be slow and unreliable. Cypress aims to improve
both the developing experience and the reliability of end-to-end
tests.

Cypress employs a fundamentally different architecture. A


Node.js application starts the browser. The browser is not
controlled remotely, but the tests run directly in the browser,
supported by a browser plugin.
The test runner provides a powerful user interface for inspecting
and debugging tests right in the browser.

TEST RUNNER

Cypress is the product of one company, Cypress.io, Inc. The test


runner we are going to use is open source and free of charge.

The company generates revenue with an additional paid service:


The Cypress dashboard manages test runs recorded in a
continuous integration environment. You do not have to
subscribe to this service to write and run Cypress tests.

TRADE-OFFS

From our perspective, Cypress has a few drawbacks.

In place of Jasmine, Cypress uses the Mocha and Chai


libraries for writing tests. While both serve the same purpose,
you have to learn the subtle differences. If you use Jasmine
for unit and integration tests, there will be an inconsistency in
your tests.

Cypress only supports Firefox as well as Chromium-based


browsers like Google Chrome and Microsoft Edge. It does not
support Safari, legacy Edge or even Internet Explorer.

Cypress is not simply better than WebDriver-based frameworks.


It tries to solve their problems by narrowing the scope and by
making trade-offs.

RECOMMENDED

That being said, this guide recommends to use Cypress for


testing Angular applications. Cypress is well-maintained and
well-documented. With Cypress, you can write valuable end-to-
end tests with little effort.

In case you do need a WebDriver-based framework, have a look


at Webdriver.io instead.

🔗 Cypress: Trade-offs
🔗 Cypress: Key differences
🔗 Webdriver.io

Installing Cypress
An easy way to add Cypress to an existing Angular CLI project is
the Cypress Angular Schematic.

In your Angular project directory, run this shell command:

ng add @cypress/schematic

This command does four important things:

1. Add Cypress and auxiliary npm packages to package.json.


2. Add the Cypress configuration file cypress.config.ts.

3. Change the angular.json configuration file in order to add


ng run commands.

4. Create a sub-directory named cypress with a scaffold for


your tests.

The output looks like this:

ℹ Using package manager: npm

✔ Found compatible package version: @cypress/[email protected].

✔ Package information loaded.

The package @cypress/[email protected] will be installed and


executed.

Would you like to proceed? Yes

✔ Packages successfully installed.

? Would you like the default `ng e2e` command to use Cypress? [
Protractor to Cypress Migration Guide:

https://on.cypress.io/protractor-to-cypress?cli=true ] Yes

CREATE cypress.config.ts (127 bytes)

CREATE cypress/tsconfig.json (139 bytes)

CREATE cypress/e2e/spec.cy.ts (143 bytes)

CREATE cypress/fixtures/example.json (85 bytes)

CREATE cypress/support/commands.ts (1377 bytes)

CREATE cypress/support/e2e.ts (655 bytes)

UPDATE package.json (1187 bytes)

UPDATE angular.json (3757 bytes)

✔ Packages installed successfully.

The installer asks if you would like the ng e2e command to start
Cypress. If you are setting up a new project without end-to-end
tests yet, it is safe to answer “Yes”.

In Angular CLI prior to version 12, ng e2e used to start


Protractor. If you have any legacy Protractor tests in the project
and want to continue to run them using ng e2e, answer “No” to
the question.

Writing an end-to-end test with


Cypress
In the project directory, you will find a sub-directory called
cypress. It contains:

A tsconfig.json configuration for all TypeScript files


specifically in this directory,

an e2e directory for the end-to-end tests,

a support directory for custom commands and other testing


helpers,

a fixtures directory for test data.


The test files reside in the e2e directory. Each test is TypeScript
file with the extension .cy.ts.

The tests itself are structured with the test framework Mocha.
The assertions (also called expectations) are written using Chai.

Mocha and Chai is a popular combination. They roughly do the


same as Jasmine, but are much more flexible and rich in
features.

TEST SUITES

If you have written unit tests with Jasmine before, the Mocha
structure will be familiar to you. A test file contains one or more
suites declared with describe('…', () => { /* … */}).
Typically, one file contains one describe block, possible with
nested describe blocks.

Inside describe, the blocks beforeEach, afterEach,


beforeAll, afterAll and it can be used similar to Jasmine
tests.

This brings us to the following end-to-end test structure:

describe('… Feature description …', () => {

beforeEach(() => {

// Navigate to the page

});

it('… User interaction description …', () => {

// Interact with the page

// Assert something about the page content

});

});

Testing the counter Component


Step by step, we are going to write end-to-end tests for the
counter example application.

🔗 Counter Component: Source code


🔗 Counter Component: Run the app
As a start, let us write a minimal test that checks the document
title. In the project directory, we create a file called
cypress/e2e/counter.cy.ts. It looks like this:

describe('Counter', () => {

beforeEach(() => {

cy.visit('/');

});

it('has the correct title', () => {

cy.title().should('equal', 'Angular Workshop: Counters');

});

});

COMMANDS
Cypress commands are methods of the cy namespace object.
Here, we are using two commands, visit and title.

cy.visit orders the browser to visit the given URL. Above, we


use the path /. Cypress appends the path to the baseUrl. Per
default, the baseUrl is set to http://localhost:4200 in
Cypress’ configuration file, cypress.config.ts.

CHAINERS

cy.title returns the page title. To be specific, it returns a


Cypress Chainer. This is an asynchronous wrapper around an
arbitrary value. Most of the time, a Chainer wraps DOM
elements. In the case, cy.title wraps a string.

ASSERTIONS

The Chainer has a should method for creating an assertion.


Cypress relays the call to the Chai library to verify the assertion.

cy.title().should('equal', 'Angular Workshop: Counters');

We pass two parameters, 'equal' and the expected title string.


equal creates an assertion that the subject value (the page title)
equals to the given value ('Angular Workshop: Counters').
equal uses the familiar === comparison.

This should style of assertions differs from Jasmine


expectations, which use the expect(…).toBe(…) style. In fact,
Chai supports three different assertion styles: should, assert,
but also expect. In Cypress you will typically use should on
Chainers and expect on unwrapped values.

🔗 Cypress API reference: cy.visit


🔗 Cypress API reference: cy.title
🔗 Cypress documentation: Assertions
🔗 Chai API reference: should style assertions
🔗 Chai API reference: equal

Running the Cypress tests


Save the minimal test from the last chapter as
cypress/e2e/counter.cy.ts.

Cypress has two shell commands to run the end-to-end tests:

TEST RUNNER

npx cypress run – Non-interactive test runner. Runs the


tests in a “headless” browser. This means the browser window
is not visible.

The tests are run once, then the browser is closed and the
shell command finishes. You can see the test results in the
shell output.
This command is typically used in a continuous integration
environment.

npx cypress open – Interactive test runner. Opens a


window where you can select which browser to use and
which tests to run. The browser window is visible and it
remains visible after completion.

You can see the test results the browser window. If you make
changes on the test files, Cypress automatically re-runs the
tests.

This command is typically used in the development


environment.

SERVE AND RUN TESTS

The Cypress schematic that we have installed wraps these


commands so they integrate well with Angular.

ng run $project-name$:cypress-run – Starts the non-


interactive test runner.

ng run $project-name$:cypress-open – Starts the


interactive test runner.

$project-name$ is a placeholder. Insert the name of the


respective Angular project. This is typically the same as the
directory name. If not, it can be found in angular.json in the
projects object.

For example, the Counter example has the project name


angular-workshop. Hence, the commands read:

ng run angular-workshop:cypress-run

ng run angular-workshop:cypress-open

Whatever method of starting Cypress you use, you need to start


Angular’s development server with ng serve in a separate shell
first. Cypress tries to connect to the baseUrl
(http://localhost:4200) and will let you know if the server is
not reachable.

LAUNCH WINDOW

The cypress open command will open the test runner. First,
you need to choose the type of testing, which is “E2E testing” in
our case.
On the next screen, you need to choose the browser for running
the tests.
Cypress automatically lists all browsers it finds on your system.
In addition, you can run your tests in Electron. Cypress’ user
interface is an Electron application. Electron is based on
Chromium, the open source foundation of the Chrome browser.

Select a browser and click on the “Start E2E Testing” button. This
launches the browser and opens the test runner, Cypress’
primary user interface. (The screenshot shows Chrome.)
In the main window pane, all tests are listed. To run a single test,
click on it.

TEST RUNNER

Suppose you run the tests in Chrome and run the test
counter.cy.ts, the in-browser test runner looks like this:
In the “Specs” column, the tests of this test run are listed. For
each test, you can see the specs.

On the right side, the web page under test is seen. The web page
is scaled to fit into the window, but uses a default viewport width
of 1000 pixels.

SPEC LOG

By clicking on a spec name, you can see all commands and


assertions in the spec.
You can watch Cypress running the specs command by
command. This is especially useful when a spec fails. Let us
break the spec on purpose to see Cypress’ output.

cy.title().should('equal', 'Fluffy Golden Retrievers');

This change leads to a failing spec:


Cypress provides a helpful error message, pointing to the
assertion that failed. You can click on the file name with line and
column, cypress/e2e/counter.cy.ts:7:16 in the example,
to jump right to the assertion in your code editor.

TIME TRAVEL

A unique feature of the in-browser test runner is the ability to


see the state of the page at a certain point in time. Cypress
creates DOM snapshot whenever a command is run or an
assertion verified.

By hovering over a command or assertion, you can travel back in


time. The page on the right side then reflects the page when the
command or assertion was processed.

The time travel feature is invaluable when writing and


debugging end-to-end tests. Use it to understand how your test
interacts with the application and how the application reacts.
When a test fails, use it to reconstruct what circumstances lead
to the failure.

🔗 Cypress documentation: The Test Runner

Asynchronous tests
Every Cypress command takes some time to execute. But from
the spec point of view, the execution happens instantly.

COMMAND QUEUE

In fact, Cypress commands are merely declarative. The execution


happens asynchronously. By calling cy.visit and cy.title,
we add commands to a queue. The queue is processed later.

As a consequence, we do not need to wait for the result of


cy.visit. Cypress automatically waits for the page to load
before proceeding with the next command.

For the same reason, cy.title does not immediately return a


string, but a Chainer that allows more declarations.
In the Jasmine unit and integration tests we wrote before, we
had to manage time ourselves. When dealing with asynchronous
commands and values, we had to use async / await,
fakeAsync and other means explicitly.

This is not necessary when writing Cypress tests. The Cypress


API is designed for expressiveness and readability. Cypress hides
the fact that all commands take time.

SYNCHRONOUS ASSERTIONS

Sometimes it is necessary to access and inspect a value


synchronously. Cypress allows this in form of callback functions
that are executed after a certain command was processed. You
can pass a callback function to the should command or the
more general then command.

Inside these callbacks, assertions on plain, unwrapped values


are written using Chai’s expect function. We will get to know
this practise later.

🔗 Cypress API reference: should with callback function


🔗 Cypress API reference: then command

Automatic retries and waiting


A key feature of Cypress is that it retries certain commands and
assertions.

For example, Cypress queries the document title and compares it


with the expected title. If the title does not match instantly,
Cypress will retry the cy.title command and the should
assertion for four seconds. When the timeout is reached, the
spec fails.

WAIT AUTOMATICALLY

Other commands are not retried, but have a built-in waiting


logic. For example, we are going to use Cypress’ click method
to click on an element.

Cypress automatically waits for four seconds for the element to


be clickable. Cypress scrolls the element into view and checks if it
is visible and not disabled. After several other checks, the
Cypress performs the click.

The retry and waiting timeout can be configured for all tests or
individual commands.

RETRY SPECS

If a spec fails despite these retries and waiting, Cypress can be


configured to retry the whole spec. This is the last resort if a
particular spec produces inconsistent results.
These features makes end-to-end tests more reliable, but also
easier to write. In other frameworks, you have to wait manually
and there is no automatic retry of commands, assertions or
specs.

🔗 Cypress introduction: Commands are asynchronous


🔗 Cypress documentation: Interacting with Elements
🔗 Cypress documentation: Retry-ability
🔗 Cypress documentation: Test Retries

Testing the counter increment


In our first Cypress test, we have checked the page title
successfully. Let us test the counter’s increment feature.

The test needs to perform the following steps:

1. Navigate to “/”.

2. Find the element with the current count and read its text
content.

3. Expect that the text is “5”, since this is the start count for the
first counter.

4. Find the increment button and click it.


5. Find the element with the current count and read its text
content (again).

6. Expect that the text now reads “6”.

We have used cy.visit('/') to navigate to an address. The


path “/” translates to http://localhost:4200/ since this is the
configured baseUrl.

Finding elements
The next step is to find an element in the current page. Cypress
provides several ways to find elements. We are going to use the
cy.get method to find an element by CSS selector.

cy.get('.example')

cy.get returns a Chainer, an asynchronous wrapper around the


found elements, enriched with useful methods.

Just like with unit and integration test, the immediate question
is: How should we find an element – by id, name, class or by
other means?

FIND BY TEST ID

As discussed in querying the DOM with test ids, this guide


recommends to mark elements with test ids.
These are data attributes like data-testid="example". In the
test, we use a corresponding attribute selector to find the
elements, for example:

cy.get('[data-testid="example"]')

FIND BY TYPE

Test ids are recommended, but other ways to find elements are
still useful in some cases. For example, you might want to check
the presence and the content of an h1 element. This element has
a special meaning and you should not find it with an arbitrary
test id.

The benefit of a test id is that it can be used on any element.


Using a test id means ignoring the element type (like h1) and
other attributes. The test does not fail if those change.

But if there is a reason for this particular element type or


attribute, your test should verify the usage.

🔗 Cypress API reference: cy.get


🔗 Cypress Best Practices: Selecting Elements

Interacting with elements


To test the counter Component, we want to verify that the start
count for the first counter is “5”. The current count lives in an
element with the test id count. So the element finder is:

cy.get('[data-testid="count"]')

PRESENCE AND CONTENT

The cy.get command already has an assertion built-in: It


expects to find at least one element matching the selector.
Otherwise, the spec fails.

Next, we check the element’s text content to verify the start


count. Again, we use the should method to create an assertion.

cy.get('[data-testid="count"]').should('have.text', '5');

The have.text assertion compares the text content with the


given string.

We did it! We have found an element and checked its content.

CLICK

Now let us increment the count. We find and click on the


increment button (test id increment-button). Cypress offers
the cy.click method for this purpose.

cy.get('[data-testid="increment-button"]').click();

The Angular code under test handles the click event. Finally, we
verify that the visible count has increased by one. We repeat the
should('have.text', …) command, but expect a higher
number.

The test suite now looks like this:

describe('Counter', () => {

beforeEach(() => {

cy.visit('/');

});

it.only('has the correct title', () => {

cy.title().should('equal', 'Angular Workshop: Counters');

});

it('increments the count', () => {

cy.get('[data-testid="count"]').should('have.text', '5');

cy.get('[data-testid="increment-button"]').click();

cy.get('[data-testid="count"]').should('have.text', '6');

});

});

The next feature we need to test is the decrement button. The


spec works similar to the increment spec. It clicks on the
decrement button (test id decrement-button) and checks that
the count has decreased.

it('decrements the count', () => {

cy.get('[data-testid="decrement-button"]').click();

cy.get('[data-testid="count"]').should('have.text', '4');

});

Last but not least, we test the reset feature. The user can enter a
new count into a form field (test id reset-input) and click on
the reset button (test id reset-button) to set the new count.

FILL OUT FORM

The Cypress Chainer has a generic method for sending keys to


an element that the keyboard can interact with: type.

To enter text into the form field, we pass a string to the type
method.

cy.get('[data-testid="reset-input"]').type('123');

Next, we click on the reset button and finally expect the change.

it('resets the count', () => {

cy.get('[data-testid="reset-input"]').type('123');

cy.get('[data-testid="reset-button"]').click();

cy.get('[data-testid="count"]').should('have.text', '123');

});

This is the full test suite:

describe('Counter', () => {

beforeEach(() => {

cy.visit('/');

});

it('has the correct title', () => {

cy.title().should('equal', 'Angular Workshop: Counters');

});

it('increments the count', () => {

cy.get('[data-testid="count"]').should('have.text', '5');

cy.get('[data-testid="increment-button"]').click();

cy.get('[data-testid="count"]').should('have.text', '6');

});

it('decrements the count', () => {

cy.get('[data-testid="decrement-button"]').click();

cy.get('[data-testid="count"]').should('have.text', '4');

});

it('resets the count', () => {

cy.get('[data-testid="reset-input"]').type('123');

cy.get('[data-testid="reset-button"]').click();

cy.get('[data-testid="count"]').should('have.text', '123');

});

});

On the start page of the counter project, there are in fact nine
counter instances. The cy.get command therefore returns nine
elements instead of one.

FIRST MATCH

Commands like type and click can only operate on one


element, so we need to reduce the element list to the first result.
This is achieved by Cypress’ first command inserted in the
chain.

it('increments the count', () => {

cy.get('[data-testid="count"]').first().should('have.text', '5');

cy.get('[data-testid="increment-button"]').first().click();

cy.get('[data-testid="count"]').first().should('have.text', '6');

});

This also applies to the other specs. If the element under test
only appears once, the first command is not necessary, of
course.

All counter features are now tested. In the next chapters, we will
refactor the code to improve its readability and maintainability.

🔗 Counter E2E test code


🔗 Cypress API reference: click
🔗 Cypress API reference: type
🔗 Cypress API reference: first
🔗 Cypress FAQ: How do I get an element’s text contents?

Custom Cypress commands


The test we wrote is quite repetitive. The pattern
cy.get('[data-testid="…"]') is repeated over and over.

The first improvement is to write a helper that hides this detail.


We have already written two similar functions as unit testing
helpers, findEl and findEls.

FIND BY TEST ID

The easiest way to create a Cypress helper for finding elements


is a function.
function findEl(testId: string):
Cypress.Chainable<JQuery<HTMLElement>> {

return cy.get(`[data-testid="${testId}"]`);

This would allow us to write findEl('count') instead of


cy.get('[data-testid="count"]').

CUSTOM COMMANDS

This works fine, but we opt for a another way. Cypress supports
adding custom commands to the cy namespace. We are going
to add the command byTestId so we can write
cy.byTestId('count').

Custom commands are placed in


cypress/support/commands.ts. This file is automatically
created by the Angular schematic. Using
Cypress.Commands.add, we add our own command as a
method of cy. The first parameter is the command name, the
second is the implementation as a function.

CY.BYTESTID

The simplest version looks like this:

Cypress.Commands.add(

'byTestId',

(id: string) =>

cy.get(`[data-testid="${id}"]`)

);

Now we can write cy.byTestId('count'). We can still fall back


to cy.get if we want to find an element by other means.

cy.byTestId should have the same flexibility as the generic


cy.get. So we add the second options parameter as well. We
borrow the function signature from the official cy.get typings.

Cypress.Commands.add(

'byTestId',

// Borrow the signature from cy.get

<E extends Node = HTMLElement>(

id: string,

options?: Partial<

Cypress.Loggable & Cypress.Timeoutable & Cypress.Withinable &


Cypress.Shadow

>,

): Cypress.Chainable<JQuery<E>> =

cy.get(`[data-testid="${id}"]`, options),

);

For proper type checking, we need to tell the TypeScript compiler


that we have extended the cy namespace. In commands.ts, we
extend the Chainable interface with a method declaration for
byTestId.

declare namespace Cypress {

interface Chainable {

/**

* Get one or more DOM elements by test id.

* @param id The test id

* @param options The same options as cy.get

*/

byTestId<E extends Node = HTMLElement>(

id: string,

options?: Partial<

Cypress.Loggable & Cypress.Timeoutable & Cypress.Withinable


& Cypress.Shadow

>,

): Cypress.Chainable<JQuery<E>>;

You do not have to understand these type definitions in detail.


They simply make sure that you can pass the same options to
cy.byTestId that you can pass to cy.get.

Save commands.ts, then open cypress/support/e2e.ts and


activate the line that imports command.ts.

import './commands';

This is it! We now have a strictly typed command cy.byTestId.


Using the command, we can declutter the test.

describe('Counter (with helpers)', () => {

beforeEach(() => {

cy.visit('/');

});

it('has the correct title', () => {

cy.title().should('equal', 'Angular Workshop: Counters');

});

it('increments the count', () => {

cy.byTestId('count').first().should('have.text', '5');

cy.byTestId('increment-button').first().click();

cy.byTestId('count').first().should('have.text', '6');

});

it('decrements the count', () => {

cy.byTestId('decrement-button').first().click();

cy.byTestId('count').first().should('have.text', '4');

});

it('resets the count', () => {

cy.byTestId('reset-input').first().type('123');

cy.byTestId('reset-button').first().click();

cy.byTestId('count').first().should('have.text', '123');

});

});

Keep in mind that all these first calls are only necessary since
there are multiple counters on the example page under test. If
there is only one element with the given test id on the page, you
do not need them.

🔗 Counter E2E test with helpers


🔗 Full code: commands.ts
🔗 Cypress documentation: Custom commands
🔗 Cypress documentation: Types for custom commands

Testing the Flickr search


We have learned the basics of Cypress by testing the counter
app. Let us delve into end-to-end testing with Cypress by testing
a more complex app, the Flickr search.

🔗 Flickr photo search: Source code


🔗 Flickr photo search: Run the app
Before writing any code, let us plan what the end-to-end test
needs to do:

1. Navigate to “/”.

2. Find the search input field and enter a search term, e.g.
“flower”.

3. Find the submit button and click on it.

4. Expect photo item links to flickr.com to appear.

5. Click on a photo item.

6. Expect the full photo details to appear.

NONDETERMINISTIC API

The application under test queries a third-party API with


production data. The test searches for “flower” and with each
test run, Flickr returns potentially different results.

There are two ways to deal with this dependency during testing:
1. Test against the real Flickr API.

2. Fake the Flickr API and return a fixed response.

If we test against the real Flickr API, we cannot be specific in our


expectations due to changing search results. We can superficially
test the search results and the full photo. We merely know that
the clicked photo has “flower” in its title or tags.

REAL VS. FAKE API

This has pros and cons. Testing against the real Flickr API makes
the test realistic, but less reliable. If the Flickr API has a short
hiccup, the test fails although there is no bug in our code.

Running the test against a fake API allows us to inspect the


application deeply. Did the application render the photos the API
returned? Are the photo details shown correctly?

Keep in mind that unit, integration and end-to-end tests


complement each other. The Flickr search is also tested
extensively using unit and integration tests.

Each type of test should do what it does best. The unit tests
already put the different photo Components through their
paces. The end-to-end test does not need to achieve that level of
detail.
With Cypress, both type of tests are possible. For a start, we will
test against the real Flickr API. Then, we will fake the API.

Testing the search form


We create a file called cypress/e2e/flickr-search.cy.ts.
We start with a test suite.

describe('Flickr search', () => {

const searchTerm = 'flower';

beforeEach(() => {

cy.visit('/');

});

it('searches for a term', () => {

/* … */

});

});

We instruct the browser to enter “flower” into the search field


(test id search-term-input). Then we click on the submit
button (test id submit-search).

it('searches for a term', () => {

cy.byTestId('search-term-input')

.first()

.clear()

.type(searchTerm);

cy.byTestId('submit-search').first().click();

/* … */

});

CLEAR, THEN TYPE

The type command does not overwrite the form value with a
new value, but sends keyboard input, key by key.

Before entering “flower”, we need to clear the field since it


already has a pre-filled value. Otherwise we would append
“flower” to the existing value. We use Cypress’ clear method for
that purpose.

Clicking on the submit button starts the search. When the Flickr
API has responded, we expect the search results to be appear.

EXPECT SEARCH RESULTS

A search result consists of a link (a element, test id photo-item-


link) and an image (img element, test id photo-item-image).

We expect 15 links to appear since this is amount of results


requested from Flickr.

cy.byTestId('photo-item-link')

.should('have.length', 15)

By writing should('have.length', 15), we assert that there


are 15 elements.
Each link needs to have an href containing
https://www.flickr.com/photos/. We cannot check for an
exact URL since results are the dynamic. But we know that all
Flickr photo URLs have the same structure.

There is no direct Chai assertion for checking that each link in


the list has an href attribute containing
https://www.flickr.com/photos/. We need to check each
link in the list individually.

The Chainer has an each method to call a function for each


element. This works similar to JavaScript’s forEach array
method.

cy.byTestId('photo-item-link')

.should('have.length', 15)

.each((link) => {

/* Check the link */

});

Cypress has three surprises for us.

SYNCHRONOUS JQUERY OBJECT

1. link is a synchronous value. Inside the each callback, we are


in synchronous JavaScript land. (We could do asynchronous
operations here, but there is no need.)
2. link has the type JQuery<HTMLElement>. This is an
element wrapped with the popular jQuery library. Cypress
chose jQuery because many JavaScript developers are already
familiar with it. To read the href attribute, we use
link.attr('href').

3. We cannot use Cypress’ should method since it only exists on


Cypress Chainers. But we are dealing with a jQuery object
here. We have to use a standard Chai assertion. We use
expect together with to.contain.

This brings us to:

cy.byTestId('photo-item-link')

.should('have.length', 15)

.each((link) => {

expect(link.attr('href')).to.contain(

'https://www.flickr.com/photos/'

);

});

The test now looks like this:

describe('Flickr search', () => {

const searchTerm = 'flower';

beforeEach(() => {

cy.visit('/');

});

it('searches for a term', () => {

cy.byTestId('search-term-input')

.first()

.clear()

.type(searchTerm);

cy.byTestId('submit-search').first().click();

cy.byTestId('photo-item-link')

.should('have.length', 15)

.each((link) => {

expect(link.attr('href')).to.contain(

'https://www.flickr.com/photos/'

);

});

cy.byTestId('photo-item-image').should('have.length', 15);

});

});

To start the tests, we first start the development server with ng


serve and then start Cypress:

ng run flickr-search:cypress-open

This opens the test runner where we click on flickr-


search.cy.ts.

🔗 Flickr search E2E test code


🔗 Cypress API reference: clear
🔗 Cypress API reference: each
🔗 jQuery API reference: attr
🔗 Chai API reference: include (contain)
Testing the full photo
When the user clicks on a link in the result list, the click event is
caught and the full photo details are shown next to the list. (If
the user clicks with the control/command key pressed or right-
clicks, they can follow the link to flickr.com.)

In the end-to-end test, we add a spec to verify this behavior.

it('shows the full photo', () => {

/* … */

});

First, it searches for “flower”, just like the spec before.

cy.byTestId('search-term-input').first().clear().type(searchTerm);

cy.byTestId('submit-search').first().click();

Then we find all photo item links, but not to inspect them, but to
click on the first on:

cy.byTestId('photo-item-link').first().click();

The click lets the photo details appear. As mentioned above, we


cannot check for a specific title, a specific photo URL or specific
tags. The clicked photo might be a different one with each test
run.

Since we have searched for “flower”, the term is either in the


photo title or tags. We check the text content of the wrapper
element with the test id full-photo.

cy.byTestId('full-photo').should('contain', searchTerm);

CONTAIN VS. HAVE  TEXT

The contain assertion checks whether the given string is


somewhere in the element’s text content. (In contrast, the
have.text assertion checks whether the content equals the
given string. It does not allow additional content.)

Next, we check that a title and some tags are present and not
empty.

cy.byTestId('full-photo-title').should('not.have.text', '');

cy.byTestId('full-photo-tags').should('not.have.text', '');

The image itself needs to be present. We cannot check the src


attribute in detail.

cy.byTestId('full-photo-image').should('exist');

The spec now looks like this:

it('shows the full photo', () => {

cy.byTestId('search-term-
input').first().clear().type(searchTerm);

cy.byTestId('submit-search').first().click();

cy.byTestId('photo-item-link').first().click();

cy.byTestId('full-photo').should('contain', searchTerm);

cy.byTestId('full-photo-title').should('not.have.text', '');

cy.byTestId('full-photo-tags').should('not.have.text', '');

cy.byTestId('full-photo-image').should('exist');

});

The assertions contain, text and exist are defined by Chai-


jQuery, an assertion library for checking jQuery element lists.

Congratulations, we have successfully tested the Flickr search!


This example demonstrates several Cypress commands and
assertions. We also caught a glimpse of Cypress internals.

🔗 Flickr search E2E test code


🔗 Cypress documentation: Chai-jQuery assertions

Page objects
The Flickr search end-to-end test we have written is fully
functional. We can improve the code further to increase clarity
and maintainability.

We introduce a design pattern called page object. A design


pattern is a proven code structure, a best practice to solve a
common problem.

HIGH-LEVEL INTERACTIONS

A page object represents the web page that is scrutinized by an


end-to-end test. The page object provides a high-level interface
for interacting with the page.

So far, we have written low-level end-to-end tests. They find


individual elements by hard-coded test id, check their content
and click on them. This is fine for small tests.

But if the page logic is complex and there are diverse cases to
test, the test becomes an unmanageable pile of low-level
instructions. It is hard to find the gist of these tests and they are
hard to change.

A page object organizes numerous low-level instructions into a


few high-level interactions. What are the high-level interactions
in the Flickr search app?

1. Search photos using a search term

2. Read the photo list and interact with the items

3. Read the photo details

Where possible, we group these interactions into methods of the


page object.

PLAIN CLASS

A page object is merely an abstract pattern – the exact


implementation is up to you. Typically, the page object is
declared as a class that is instantiated when the test starts.
Let us call the class FlickrSearch and save it in a separate file,
cypress/pages/flickr-search.page.ts. The directory
pages is reserved for page objects, and the .page.ts suffix
marks the page object.

export class FlickrSearch {

public visit(): void {

cy.visit('/');

The class has a visit method that opens the page that the page
object represents.

In the test, we import the class and create an instance in a


beforeEach block.

import { FlickrSearch } from '../pages/flickr-search.page';

describe('Flickr search (with page object)', () => {

const searchTerm = 'flower';

let page: FlickrSearch;

beforeEach(() => {

page = new FlickrSearch();

page.visit();

});

/* … */

});

The FlickrSearch instance is stored in a variable declared in


the describe scope. This way, all specs can access the page
object.

SEARCH

Let us implement the first high-level interaction on the page


object: searching for photos. We move the relevant code from
the test into a method of the page object.

public searchFor(term: string): void {

cy.byTestId('search-term-input').first().clear().type(term);

cy.byTestId('submit-search').first().click();

The searchFor method expects a search term and performs all


necessary steps.

ELEMENT QUERIES

Other high-level interactions, like reading the photo list and the
photo details, cannot be translated into page object methods.
But we can move the test ids and element queries to the page
object.

public photoItemLinks(): Cypress.Chainable<JQuery<HTMLElement>> {

return cy.byTestId('photo-item-link');

public photoItemImages(): Cypress.Chainable<JQuery<HTMLElement>> {

return cy.byTestId('photo-item-image');

public fullPhoto(): Cypress.Chainable<JQuery<HTMLElement>> {

return cy.byTestId('full-photo');

public fullPhotoTitle(): Cypress.Chainable<JQuery<HTMLElement>> {

return cy.byTestId('full-photo-title');

public fullPhotoTags(): Cypress.Chainable<JQuery<HTMLElement>> {

return cy.byTestId('full-photo-tags');

public fullPhotoImage(): Cypress.Chainable<JQuery<HTMLElement>> {

return cy.byTestId('full-photo-image');

These methods return element Chainers.

Next, we rewrite the end-to-end test to use the page object


methods.

import { FlickrSearch } from '../pages/flickr-search.page';

describe('Flickr search (with page object)', () => {

const searchTerm = 'flower';

let page: FlickrSearch;

beforeEach(() => {

page = new FlickrSearch();

page.visit();

});

it('searches for a term', () => {

page.searchFor(searchTerm);

page

.photoItemLinks()

.should('have.length', 15)

.each((link) => {

expect(link.attr('href')).to.contain(

'https://www.flickr.com/photos/'

);

});

page.photoItemImages().should('have.length', 15);

});

it('shows the full photo', () => {

page.searchFor(searchTerm);

page.photoItemLinks().first().click();

page.fullPhoto().should('contain', searchTerm);

page.fullPhotoTitle().should('not.have.text', '');

page.fullPhotoTags().should('not.have.text', '');

page.fullPhotoImage().should('exist');

});

});

For the Flickr search above, a page object is probably too much
of a good thing. Still, the example demonstrates the key ideas of
page objects:

Identify repetitive high-level interactions and map them to


methods of the page object.

Move the finding of elements into the page object. The test
ids, tag names, etc. used for finding should live in a central
place.

When the markup of a page under test changes, the page


object needs an update, but the test should remain
unchanged.

Leave all assertions (should and expect) in the specs. Do not


move them to the page object.

HIGH-LEVEL TESTS

When writing end-to-end tests, you get lost in technical details


quickly: finding elements, clicking them, filling out form fields,
checking fields values and text content. But end-to-end tests
should not revolve around these low-level details. They should
describe the user journey on a high level.

The goal of this refactoring is not brevity. Using page objects


does not necessarily lead to less code. The purpose of page
objects is to separate low-level details – like finding elements by
test ids – from the high-level user journey through the
application. This makes the specs easier to read and the easier
to maintain.

You can use the page object pattern when you feel the need to
tidy up complex, repetitive tests. Once you are familiar with the
pattern, it also helps you to avoid writing such tests in the first
place.
🔗 Flickr search E2E test with page object
🔗 Flickr search page object

Faking the Flickr API


The end-to-end test we wrote for the Flickr search uses the real
Flickr API. As discussed, this makes the test realistic.

The test provides confidence that the application works hand in


hand with the third-party API. But it makes the test slower and
only allows unspecific assertions.

INTERCEPT HTTP REQUESTS

With Cypress, we can uncouple the dependency. Cypress allows


us to intercept HTTP requests and respond with fake data.

First of all, we need to set up the fake data. We have already


created fake photo objects for the FlickrService unit test. For
simplicity, we just import them:

import {

photo1,

photo1Link,

photos,

searchTerm,

} from '../../src/app/spec-helpers/photo.spec-helper';

Using the fake photos, we create a fake response object that


mimics the relevant part of the Flickr response.

const flickrResponse = {

photos: {

photo: photos,

},

};

FAKE SERVER WITH ROUTE

Now we instruct Cypress to intercept the Flickr API request and


answer it with fake data. This setup happens in the test’s
beforeEach block. The corresponding Cypress command is
cy.intercept.

beforeEach(() => {

cy.intercept(

method: 'GET',

url: 'https://www.flickr.com/services/rest/*',

query: {

tags: searchTerm,

method: 'flickr.photos.search',

format: 'json',

nojsoncallback: '1',

tag_mode: 'all',

media: 'photos',

per_page: '15',

extras: 'tags,date_taken,owner_name,url_q,url_m',

api_key: '*',

},

},

body: flickrResponse,

headers: {

'Access-Control-Allow-Origin': '*',

},

},

).as('flickrSearchRequest');

cy.visit('/');

});

cy.intercept can be called in different ways. Here, we pass


two objects:

1. A route matcher describing the requests to intercept. It


contains the HTTP GET method, the base URL and a whole
bunch of query string parameters. In the URL and the
api_key query parameter, the * character is a wildcard that
matches any string.

2. A route handler describing the response Cypress should send.


As JSON response body, we pass the flickrResponse fake
object.

Since the request to Flickr is cross-origin, we need to set the


Access-Control-Allow-Origin: * header. This allows
our Angular application at the origin
http://localhost:4200 to read the response from the
origin https://www.flickr.com/.
ALIAS

Finally, we give the request an alias by calling


.as('flickrSearchRequest'). This makes it possible to refer
to the request later using the @flickrSearchRequest alias.

After this setup, Cypress intercepts the request to Flickr and


handles it by itself. The original Flickr API is not reached.

The existing, rather generic specs still pass. Before we make


them more specific, we need to verify that Cypress found a
match and intercepted the HTTP request. Because if it did not,
the test would still pass.

WAIT FOR REQUEST

We can achieve this by explicitly waiting for the request after


starting the search.

it('searches for a term', () => {

cy.byTestId('search-term-
input').first().clear().type(searchTerm);

cy.byTestId('submit-search').first().click();

cy.wait('@flickrSearchRequest');

/* … */

});

cy.wait('@flickrSearchRequest') tells Cypress to wait for


a request that matches the specified criteria.
@flickrSearchRequest refers to the alias we have defined
above.

If Cypress does not find a matching request until a timeout, the


test fails. If Cypress caught the request,
we know that the
Angular application received the photos specified in the photos
array.

SPECIFIC ASSERTIONS

By faking the Flickr API, we gain complete control over the


response. We chose to return fixed data. The application under
test processes the data deterministically. As discussed, this
allows us to verify that the application correctly renders the
photos the API returned.

Let us write specific assertions that compare the photos in the


result list with those in the photos array.

it('searches for a term', () => {

cy.byTestId('search-term-
input').first().clear().type(searchTerm);

cy.byTestId('submit-search').first().click();

cy.wait('@flickrSearchRequest');

cy.byTestId('photo-item-link')

.should('have.length', 2)

.each((link, index) => {

expect(link.attr('href')).to.equal(

`https://www.flickr.com/photos/${photos[index].owner}/${photos[inde
x].id}`,

);

});

cy.byTestId('photo-item-image')

.should('have.length', 2)

.each((image, index) => {

expect(image.attr('src')).to.equal(photos[index].url_q);

});

});

Here, we walk through the links and images to ensure that the
URLs originate from the fake data. Previously, when testing
against the real API, we tested the links only superficially. We
could not test the image URLs at all.

Likewise, for the full photo spec, we make the assertions more
specific.

it('shows the full photo', () => {

cy.byTestId('search-term-
input').first().clear().type(searchTerm);

cy.byTestId('submit-search').first().click();

cy.wait('@flickrSearchRequest');

cy.byTestId('photo-item-link').first().click();

cy.byTestId('full-photo').should('contain', searchTerm);

cy.byTestId('full-photo-title').should('have.text',
photo1.title);

cy.byTestId('full-photo-tags').should('have.text', photo1.tags);

cy.byTestId('full-photo-image').should('have.attr', 'src',
photo1.url_m);

cy.byTestId('full-photo-link').should('have.attr', 'href',
photo1Link);

});

The specs now ensure that the application under test outputs
the data from the Flickr API. have.text checks an element’s text
content, whereas have.attr checks the src and href
attributes.

We are done! Our end-to-end test intercepts an API request and


responds with fake data in order to inspect the application
deeply.

INTERCEPT ALL REQUESTS

In the case of the Flickr search, we have intercepted an HTTP


request to a third-party API. Cypress allows to fake any request,
including those to your own HTTP APIs.

This is useful for returning deterministic responses crucial for


the feature under test. But it is also useful for suppressing
requests that are irrelevant for your test, like marginal images
and web analytics.

🔗 Flickr search E2E test with cy.intercept


🔗 Photo spec helper
🔗 Cypress documentation: Network Requests
🔗 Cypress API reference: intercept
🔗 Cypress API reference: wait
End-to-end testing: Summary
End-to-end tests used to be expensive while the outcome was
poor. It was hard to write tests that pass reliably when the
application is working correctly. This time could not be invested
in writing useful tests that uncover bugs and regressions.

For years, Protractor was the end-to-end testing framework most


Angular developers relied on. With Cypress, a framework arose
that sets new standards. Compared to Protractor, Cypress excels
in developer experience and cost-effectiveness.

PROTRACTOR AND CYPRESS EXAMPLES

While this guide recommends to start with Cypress, WebDriver-


based frameworks are still useful if you need to test a broad
range of browsers. For all Cypress tests in this guide, you will
find equivalent Protractor tests in the example repositories.

Even with Cypress, end-to-end tests are much more complex and
error-prone than unit and integration tests with Jasmine and
Karma. Then again, end-to-end tests are highly effective to test a
feature under realistic circumstances.

🔗 Counter: Cypress tests


🔗 Counter: Protractor tests
🔗 Flickr search: Cypress tests
🔗 Flickr search: Protractor tests
Summary
LEARNING OBJECTIVES

Reducing the frustration from implementing and testing an application


Taking small steps when learning testing techniques and expanding test coverage
Growing as a developer and as a team by practicing automated testing

Writing tests is often a frustrating experience that comes on top


of implementation troubles. The logical and technical complexity
is overwhelming and intimidating. All this drains motivation.

The goal of this guide is to dispel the fear of testing. While


testing Angular applications is a complex matter, this guide
breaks it down into small, approachable steps and aims to give a
well-balanced overview.

Find a testing strategy that reduces the frustration and benefits


the quality of your software. Once you have written tests for a
couple of features, you will learn which tests are worthwhile –
tests that uncover bugs and prevent regressions. Continue with
these successes, then slowly explore other kind of tests.

Luckily, the Angular community works steadily to make testing


accessible. Angular’s architecture facilitates the testing of all
relevant parts. The framework ships with robust testing tools. If
they do not fit your needs, there are mature community projects
with alternative workflows.
Testing does not only make your software more reliable, but also
evolves your coding practice in the long run. It requires to write
testable code, and testable code is usually simpler.

Automated software testing is challenging and rewarding for


various reasons. Despite all troubles, that makes it fascinating.
Index of example applications
All example applications are repositories on GitHub:

🔗 Counter Component
🔗 Flickr photo search
🔗 Sign-up form
🔗 TranslatePipe
🔗 ThresholdWarningDirective
🔗 PaginateDirective
References
Angular: Grundlagen, fortgeschrittene Themen und Best
Practices, Second Edition, Ferdinand Malcher, Johannes
Hoppe, Danny Koppenhagen. dpunkt.verlag, 2019. ISBN 978-
3-86490-646-6

Testing Angular Applications, Jesse Palmer, Corinna Cohn, Mike


Giambalvo, Craig Nishina. Manning Publications, 2018. ISBN
978-1-61729-364-1

Testing JavaScript Applications, Lucas da Costa. Manning


Publications, 2021. ISBN 978-1-61729-791-5

JavaScript Testing Recipes, James Coglan, 2016.


Acknowledgements
This book would not be possible without the support and input
from many individuals. I would like to thank everyone in the
Angular, JavaScript and web development communities who not
only shares their knowledge and works on open source tools,
but also advocates for inclusive communities and software.

Thanks to the teams at 9elements, Diebold Nixdorf and Keysight


Technologies for the opportunity to work on first-class Angular
applications. Thanks for the challenges, resources and patience
that allowed me to research automated testing in detail.

Thanks to Netanel Basal for valuable feedback on the book, for


creating Spectator and for many helpful articles on Angular and
testing.

Thanks to Nils Binder for helping with the design, including the
dark color scheme. Thanks to Melina Jacob for designing the e-
book cover.

Thanks to Tim Deschryver, Kent C. Dodds, Kara Erickson, Asim


Hussain, Tracy Lee, Brandon Roberts, Jesse Palmer, Corinna
Cohn, Mike Giambalvo, Craig Nishina and Lucas F. Costa for
insights on Angular, RxJS and automated testing.
About
Author: Mathias Schäfer (molily)

Mathias is a software developer with a focus on web


development and JavaScript web applications.

Mathias is working for 9elements, a software and design agency.

Twitter: @molily

Please send feedback and corrections to [email protected]


or file an issue on GitHub.

Published on February 17th, 2021.


License
License: Creative Commons Attribution-ShareAlike (CC BY-SA 4.0)

The example code is released into the public domain. See


Unlicense.

The Flickr search example application uses the Flickr API but is
not endorsed or certified by Flickr, Inc. or SmugMug, Inc. Flickr is
a trademark of Flickr, Inc. The displayed photos are property of
their respective owners.

Online book cover photo: Flying probes testing a printed circuit


board by genkur, licensed from iStock.

E-book cover and favicon: Official Angular logo by Google,


licensed under Creative Commons Attribution (CC BY 4.0).

Legal notice: Impressum und Datenschutz

You might also like