22 |
--------------------------------------------------------------------------------
/acknowledgements.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: chapter
3 | title: Acknowledgements
4 | description: Thanks to those who helped me writing this book
5 | ---
6 |
7 | # Acknowledgements
8 |
9 | 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 knowledge and works on open source tools, but also advocates for inclusive communities and software.
10 |
11 | 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.
12 |
13 | Thanks to [Netanel Basal](https://netbasal.com/) for valuable feedback on the book, for creating Spectator and for many helpful articles on Angular and testing.
14 |
15 | Thanks to [Nils Binder](https://ichimnetz.com/) for helping with the design, including the dark color scheme. Thanks to Melina Jacob for designing the e-book cover.
16 |
17 | Thanks to [Tim Deschryver](https://timdeschryver.dev/), [Kent C. Dodds](https://kentcdodds.com/), [Kara Erickson](https://twitter.com/karaforthewin), [Asim Hussain](https://asim.dev/), [Tracy Lee](https://twitter.com/ladyleet), [Brandon Roberts](https://brandonroberts.dev/), [Jesse Palmer](https://jesselpalmer.com/), Corinna Cohn, [Mike Giambalvo](https://twitter.com/heathkit), [Craig Nishina](https://twitter.com/cnishina), [Lucas F. Costa](https://lucasfcosta.com/) and [Jessica Sachs](https://jess.sh/) for insights on Angular, RxJS and automated testing.
18 |
19 |
20 |
--------------------------------------------------------------------------------
/angular-testing-principles.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: chapter
3 | title: Angular testing principles
4 | description: Dependency injection, mocking, starting the Karma test suite
5 | ---
6 |
7 | # Angular testing principles
8 |
9 |
16 |
17 | ## Testability
18 |
19 | 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.
20 |
21 |
22 |
23 | 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?
24 |
25 | An important reason is **testability**. Angular’s architecture guarantees that all application parts can be tested easily in a similar way.
26 |
27 |
28 |
29 | 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.
30 |
31 | ## Dependency injection and faking
32 |
33 | 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.
34 |
35 | 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.
36 |
37 |
38 |
39 | 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.
40 |
41 |
42 |
43 | This is of immense importance for automated testing. In our test, we can decide how to deal with a dependency:
44 |
45 | - We can either provide an **original**, fully-functional implementation. In this case, we are writing an [integration test](../testing-principles/#integration-tests) that includes direct and indirect dependencies.
46 | - Or we provide a **fake** implementation that does not have side effects. In this case, we are writing a [unit test](../testing-principles/#unit-tests) that tries to test the application part in *isolation*.
47 |
48 | 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.
49 |
50 |
53 |
54 | ## Testing tools
55 |
56 | 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.
57 |
58 |
59 |
60 | The Angular team already made decisions for you: [Jasmine](https://jasmine.github.io/) as testing framework and [Karma](https://karma-runner.github.io/) as test runner. Implementation and test code is bundled with [Webpack](https://webpack.js.org). Application parts are typically tested inside Angular’s [TestBed](https://angular.io/api/core/testing/TestBed).
61 |
62 | 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.
63 |
64 |
65 |
66 | For example, some Angular developers use [Jest](https://jestjs.io/) instead of Jasmine and Karma. Some use [Spectator](../testing-components-with-spectator/#testing-components-with-spectator) or the [Angular Testing Library](https://github.com/testing-library/angular-testing-library) instead of using `TestBed` directly.
67 |
68 | 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.
69 |
70 | Once you have reached the limits of a particular setup, you should investigate whether alternatives make testing your application easier, faster and more reliable.
71 |
72 | ## Testing conventions
73 |
74 | 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.
75 |
76 |
77 |
78 | 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.
79 |
80 |
81 |
82 | 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.
83 |
84 | Therefore, you should create **high-level testing tools** that cast your conventions into code in order to write short, readable and understandable tests.
85 |
86 | 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.
87 |
88 | ## Running the unit and integration tests
89 |
90 | 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:
91 |
92 | ```
93 | npm install -g @angular/cli
94 | ```
95 |
96 | 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.
97 |
98 | 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.
99 |
100 | The command for starting the unit and integration tests is:
101 |
102 | ```
103 | ng test
104 | ```
105 |
106 | First, this command finds all files in the directory tree that match the pattern `.spec.ts`. Using Webpack, it compiles them into a JavaScript bundle, together with its dependencies. The bundle code also initializes the Angular testing environment – the `TestBed`.
107 |
108 | Typically, an Angular application loads and starts an `AppModule`. This startup is called bootstrapping. The `AppModule` then imports other Modules, Components, Services, etc. This way, the bundler finds all parts of the application.
109 |
110 | The test bundle works differently. It does not start with one Module in order to walk through its dependencies. It merely imports all files whose name ends with `.spec.ts`.
111 |
112 |
115 |
116 | 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 `.spec.ts` files are located in the same directory as the implementation code.
117 |
118 | In our example application, the `CounterComponent` is located in [src/app/components/counter/counter.component.ts](https://github.com/9elements/angular-workshop/blob/main/src/app/components/counter/counter.component.ts). The corresponding test file sits in [src/app/components/counter/counter.component.spec.ts](https://github.com/9elements/angular-workshop/blob/main/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.
119 |
120 |
121 |
122 | Second, `ng test` launches Karma, the test runner. Karma starts a development server at [http://localhost:9876/](http://localhost:9876/) that serves the JavaScript bundles compiled by Webpack.
123 |
124 | 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.
125 |
126 |
127 |
128 | 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.
129 |
130 | When running the tests in the [counter project](../example-applications/#the-counter-component), the browser output looks like this:
131 |
132 |
133 |
134 |
135 |
136 | This is the shell output:
137 |
138 | ```
139 | INFO [karma-server]: Karma v5.0.7 server started at http://0.0.0.0:9876/
140 | INFO [launcher]: Launching browsers Chrome with concurrency unlimited
141 | INFO [launcher]: Starting browser Chrome
142 | WARN [karma]: No captured browser, open http://localhost:9876/
143 | INFO [Chrome 84.0.4147.135 (Mac OS 10.15.6)]: Connected on socket yH0-wtoVtflRWMoWAAAA with id 76614320
144 | Chrome 84.0.4147.135 (Mac OS 10.15.6): Executed 46 of 46 SUCCESS (0.394 secs / 0.329 secs)
145 | TOTAL: 46 SUCCESS
146 | ```
147 |
148 | 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.
149 |
150 |
151 |
152 | 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.
153 |
154 | Test-driven development means letting the red-green cycle guide your development.
155 |
156 |
157 | - [Angular CLI reference: ng test](https://angular.io/cli/test)
158 |
159 |
160 | ## Configuring Karma and Jasmine
161 |
162 | Karma and Jasmine are configured in the file `karma.conf.js` in the project’s root directory. Since Angular 15, the Angular CLI does not create this file per default. If it does not exist, you can create it using this shell command:
163 |
164 | ```
165 | ng generate config karma
166 | ```
167 |
168 | There are many configuration options and plenty of plugins, so we will only look at a few.
169 |
170 |
171 |
172 | As mentioned, the standard configuration runs the tests in the Chrome browser. To run the tests in other browsers, we need to install different **launchers**.
173 |
174 | Each launcher needs to be loaded in the `plugins` array:
175 |
176 | ```javascript
177 | plugins: [
178 | require('karma-jasmine'),
179 | require('karma-chrome-launcher'),
180 | require('karma-jasmine-html-reporter'),
181 | require('karma-coverage'),
182 | require('@angular-devkit/build-angular/plugins/karma')
183 | ],
184 | ```
185 |
186 | There is already one launcher, `karma-chrome-launcher`. This is an npm package.
187 |
188 | To install other launchers, we first need to install the respective npm package. Let us install the Firefox launcher. Run this shell command:
189 |
190 | ```
191 | npm install --save-dev karma-firefox-launcher
192 | ```
193 |
194 | Then we require the package in `karma.conf.js`:
195 |
196 | ```javascript
197 | plugins: [
198 | require('karma-jasmine'),
199 | require('karma-chrome-launcher'),
200 | require('karma-firefox-launcher'),
201 | require('karma-jasmine-html-reporter'),
202 | require('karma-coverage'),
203 | require('@angular-devkit/build-angular/plugins/karma'),
204 | ],
205 | ```
206 |
207 | To run the tests in Firefox as well, we need to add the Firefox to the browsers list: `browsers: ['Chrome']` becomes `browsers: ['Chrome', 'Firefox']`.
208 |
209 | Karma will now start two browsers to run the tests in parallel.
210 |
211 |
212 |
213 | Another important concept of Karma are **reporters**. They format and output the test results. In the default configuration, three reporters are active:
214 |
215 | 1. The built-in `progress` reporter outputs text on the shell. While the tests are running, it outputs the progress:
216 |
217 | `Chrome 84.0.4147.135 (Mac OS 10.15.6): Executed 9 of 46 SUCCESS (0.278 secs / 0.219 secs)`
218 |
219 | And finally:
220 |
221 | `Chrome 84.0.4147.135 (Mac OS 10.15.6): Executed 46 of 46 SUCCESS (0.394 secs / 0.329 secs)`
222 | `TOTAL: 46 SUCCESS`
223 |
224 | 2. The standard HTML reporter `kjhtml` (npm package: `karma-jasmine-html-reporter`) renders the results in the browser.
225 |
226 |
227 |
228 |
229 |
230 | 3. The coverage reporter (npm package: `karma-coverage`) creates the test coverage report. See [measuring code coverage](../measuring-code-coverage/#measuring-code-coverage).
231 |
232 | By editing the `reporters` array, you can add reporters or replace the existing ones:
233 |
234 | ```javascript
235 | reporters: ['progress', 'kjhtml'],
236 | ```
237 |
238 | For example, to add a reporter that creates JUnit XML reports, first install the npm package:
239 |
240 | ```
241 | npm install --save-dev karma-junit-reporter
242 | ```
243 |
244 | Next, require it as a plugin:
245 |
246 | ```javascript
247 | plugins: [
248 | require('karma-jasmine'),
249 | require('karma-chrome-launcher'),
250 | require('karma-jasmine-html-reporter'),
251 | require('karma-coverage'),
252 | require('karma-junit-reporter'),
253 | require('@angular-devkit/build-angular/plugins/karma'),
254 | ],
255 | ```
256 |
257 | Finally, add the reporter:
258 |
259 | ```javascript
260 | reporters: ['progress', 'kjhtml', 'junit'],
261 | ```
262 |
263 | After running the tests with `ng test`, you will find an XML report file in the project directory.
264 |
265 |
266 |
267 | The configuration for the Jasmine adapter is located in `jasmine` object inside the `client` object:
268 |
269 | ```javascript
270 | client: {
271 | jasmine: {
272 | // you can add configuration options for Jasmine here
273 | // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
274 | // for example, you can disable the random execution with `random: false`
275 | // or set a specific seed with `seed: 4321`
276 | },
277 | clearContext: false // leave Jasmine Spec Runner output visible in browser
278 | },
279 | ```
280 |
281 | 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](../test-suites-with-jasmine/#expectations) later.) In almost all cases, specs without expectations stem from an error in the test code.
282 |
283 | ```javascript
284 | client: {
285 | jasmine: {
286 | failSpecWithNoExpectations: true,
287 | },
288 | clearContext: false // leave Jasmine Spec Runner output visible in browser
289 | },
290 | ```
291 |
292 |
93 |
94 | ## Developer tools
95 |
96 | 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.
97 |
98 |
99 |
100 | Focus the browser window and open the developer tools. In Chrome, Firefox and Edge, you can use the F12 key.
101 |
102 | You can use the developer tools to:
103 |
104 | - Write debug output to the console using `console.log`, `console.debug` and friends.
105 | - Use the JavaScript debugger. You can either set breakpoints in the developer tools or place a `debugger` statement.
106 | - Inspect the DOM of rendered Components.
107 |
108 | ## Debug output and the JavaScript debugger
109 |
110 | 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.
111 |
112 |
115 |
116 | Use debug output to answer these questions:
117 |
118 | - Is the test, suite, spec run at all?
119 | - Does the test execution reach the log command?
120 | - Did the test call the class, method, function under test correctly?
121 | - Are callbacks called correctly? Do Promises complete or fail? Do Observables emit, complete or error?
122 | - For Component tests:
123 | - Is Input data passed correctly?
124 | - Are the lifecycle methods called correctly?
125 |
126 |
129 |
130 | Some people prefer to use `debugger` instead of console output.
131 |
132 |
133 |
134 |
135 |
136 | 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.
137 |
138 |
139 |
140 | The `console` methods have their own pitfalls. For performance reasons, browsers do not write the output to the console synchronously, but asynchronously.
141 |
142 | 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.
143 |
144 | ```typescript
145 | const exampleObject = { name: 'Usagi Tsukino' };
146 | console.log(exampleObject);
147 | ```
148 |
149 | 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.
150 |
151 | ```typescript
152 | const exampleObject = { name: 'Usagi Tsukino' };
153 | console.log(exampleObject);
154 | exampleObject.name = 'Sailor Moon';
155 | ```
156 |
157 | On the console, the object representation may show `name: 'Sailor Moon'` instead of `name: 'Usagi Tsukino'`.
158 |
159 | One way to prevent this confusion is to create a snapshot of the object. You convert the object to a JSON string:
160 |
161 | ```typescript
162 | const exampleObject = { name: 'Usagi Tsukino' };
163 | console.log(JSON.stringify(exampleObject, null, ' '));
164 | exampleObject.name = 'Sailor Moon';
165 | ```
166 |
167 |
168 |
169 | If you want an interactive representation on the console, create a copy of the object with `JSON.stringify` followed by `JSON.parse`:
170 |
171 | ```typescript
172 | const exampleObject = { name: 'Usagi Tsukino' };
173 | console.log(JSON.parse(JSON.stringify(exampleObject)));
174 | exampleObject.name = 'Sailor Moon';
175 | ```
176 |
177 | Obviously, this only works for objects that can be serialized as JSON.
178 |
179 | ## Inspect the DOM
180 |
181 | 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.
182 |
183 |
184 |
185 |
186 |
187 | In the screenshot above, you see the rendered Component on the left side and the inspected DOM on the right side.
188 |
189 |
190 |
191 | 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.
192 |
193 | 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.
194 |
195 | ## Jasmine debug runner
196 |
197 | The Karma page at [http://localhost:9876](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.
198 |
199 | In the developer tools, you can select the iframe window context (Chrome is pictured):
200 |
201 |
202 |
203 |
204 |
205 | This way you can access global objects and the DOM of the document where the tests run.
206 |
207 |
208 |
209 | 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](http://localhost:9876/debug.html).
210 |
211 |
212 |
213 |
214 |
215 | The debug test runner does not have an iframe, it loads Jasmine directly. Also it automatically logs spec runs on the shell.
216 |
217 | If you change the test or implementation code, the debug runner does not re-run the tests. You have to reload the page manually.
218 |
219 |
220 |
--------------------------------------------------------------------------------
/example-applications.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: chapter
3 | title: Angular example applications with tests
4 | description: Fully-tested Angular projects used as example in this book
5 | ---
6 |
7 | # Example applications
8 |
9 |
15 |
16 | In this guide, we will explore the different aspects of testing Angular applications by looking at two examples.
17 |
18 | ## The counter Component
19 |
20 |
21 | - [Counter Component: Source code](https://github.com/9elements/angular-workshop)
22 | - [Counter Component: Run the app](https://9elements.github.io/angular-workshop/)
23 |
24 |
25 |
28 |
29 |
34 |
35 | The counter is a reusable Component that increments, decrements and resets a number using buttons and input fields.
36 |
37 |
38 |
39 | 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.
40 |
41 | The goals of this example are:
42 |
43 | - **Simplicity**: Quickly grasp what the Component is supposed to do.
44 | - **Cover core Angular features**: Reusable Components with state, Inputs, Outputs, templates, event handling.
45 | - **Scalability**: Starting point for more complex application architectures.
46 |
47 |
48 |
49 | The counter comes in three flavors with different state management solutions:
50 |
51 | 1. An independent, self-sufficient counter Component that holds its own state.
52 | 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.
53 | 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.
54 |
55 | While the counter seems easy to implement, it already offers valuable challenges from a testing perspective.
56 |
57 | ## The Flickr photo search
58 |
59 |
60 | - [Flickr photo search: Source code](https://github.com/9elements/angular-flickr-search)
61 | - [Flickr photo search: Run the app](https://9elements.github.io/angular-flickr-search/)
62 |
63 |
64 |
67 |
68 |
73 |
74 | This application allows you to search for photos on Flickr, the popular photo hosting site.
75 |
76 |
77 |
78 | 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.
79 |
80 | This application is straight-forward and relatively simple to implement. Still it raises important questions:
81 |
82 | - **App structure**: How to split responsibilities into Components and how to model dependencies.
83 | - **API communication**: How to fetch data by making HTTP requests and update the user interface.
84 | - **State management**: Where to hold the state, how to pass it down in the Component tree, how to alter it.
85 |
86 | The Flickr search comes in two flavors using different state management solutions:
87 |
88 |
89 |
90 | 1. The state is managed in the top-level Component, passed down in the Component tree and changed using Outputs.
91 | 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.
92 |
93 | 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.
94 |
95 |
96 |
--------------------------------------------------------------------------------
/faking-dependencies.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: chapter
3 | title: Faking dependencies (Mocking)
4 | description: How to mock dependencies to test Components and Services in isolation
5 | ---
6 |
7 | # Faking dependencies
8 |
9 |
17 |
18 | When testing a piece of code, you need to decide between an [integration test](../testing-principles/#integration-tests) and a [unit test](../testing-principles/#unit-tests). 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.
19 |
20 |
21 |
22 | These replacements are also called *test doubles*, *stubs* or *mocks*. Replacing a dependency is called *stubbing* or *mocking*.
23 |
24 | Since these terms are used inconsistently and their difference is subtle, **this guide uses the term “fake” and “faking”** for any dependency substitution.
25 |
26 |
27 |
28 | 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.
29 |
30 | ## Equivalence of fake and original
31 |
32 | A fake implementation must have the same shape as 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.
33 |
34 |
35 |
36 | 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.
37 |
38 | 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.
39 |
40 | 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.
41 |
42 | 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.
43 |
44 |
45 |
46 | 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?
47 |
48 | 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.
49 |
50 |
51 |
52 | 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.
53 |
54 | ## Effective faking
55 |
56 | 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.
57 |
58 |
59 |
60 | 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.
61 |
62 | 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.
63 |
64 | This guide will present thorough faking techniques that do not allow a slip. They imitate the original code while shielding the original from calls.
65 |
66 | ## Faking functions with Jasmine spies
67 |
68 | Jasmine provides simple yet powerful patterns to create fake implementations. The most basic pattern is the **Jasmine spy** for replacing a function dependency.
69 |
70 |
71 |
72 | 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.
73 |
74 | For example, we declare in a spec: “Expect that the spy has been called two times with the values `mickey` and `minnie`, respectively.”
75 |
76 | 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.
77 |
78 |
81 |
82 | A standalone spy is created by calling `jasmine.createSpy`:
83 |
84 | ```typescript
85 | const spy = jasmine.createSpy('name');
86 | ```
87 |
88 | `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.
89 |
90 | Assume we have class `TodoService` responsible for fetching a to-do list from the server. The class uses the [Fetch API](https://developer.mozilla.org/de/docs/Web/API/Fetch_API) to make an HTTP request. (This is a plain TypeScript example. It is uncommon to use `fetch` directly in an Angular app.)
91 |
92 | ```typescript
93 | class TodoService {
94 | constructor(
95 | // Bind `fetch` to `window` to ensure that `window` is the `this` context
96 | private fetch = window.fetch.bind(window)
97 | ) {}
98 |
99 | public async getTodos(): Promise {
100 | const response = await this.fetch('/todos');
101 | if (!response.ok) {
102 | throw new Error(
103 | `HTTP error: ${response.status} ${response.statusText}`
104 | );
105 | }
106 | return await response.json();
107 | }
108 | }
109 | ```
110 |
111 |
112 |
113 | 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.
114 |
115 | 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.
116 |
117 | 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`.
118 |
119 | ```typescript
120 | // Fake todos and response object
121 | const todos = [
122 | 'shop groceries',
123 | 'mow the lawn',
124 | 'take the cat to the vet'
125 | ];
126 | const okResponse = new Response(JSON.stringify(todos), {
127 | status: 200,
128 | statusText: 'OK',
129 | });
130 |
131 | describe('TodoService', () => {
132 | it('gets the to-dos', async () => {
133 | // Arrange
134 | const fetchSpy = jasmine.createSpy('fetch')
135 | .and.returnValue(okResponse);
136 | const todoService = new TodoService(fetchSpy);
137 |
138 | // Act
139 | const actualTodos = await todoService.getTodos();
140 |
141 | // Assert
142 | expect(actualTodos).toEqual(todos);
143 | expect(fetchSpy).toHaveBeenCalledWith('/todos');
144 | });
145 | });
146 | ```
147 |
148 | There is a lot to unpack in this example. Let us start with the fake data before the `describe` block:
149 |
150 | ```typescript
151 | const todos = [
152 | 'shop groceries',
153 | 'mow the lawn',
154 | 'take the cat to the vet'
155 | ];
156 | const okResponse = new Response(JSON.stringify(todos), {
157 | status: 200,
158 | statusText: 'OK',
159 | });
160 | ```
161 |
162 | First, we define the fake data we want the `fetch` spy to return. Essentially, this is an array of strings.
163 |
164 |
165 |
166 | 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.)
167 |
168 | Then, we declare a test suite using `describe`:
169 |
170 | ```typescript
171 | describe('TodoService', () => {
172 | /* … */
173 | });
174 | ```
175 |
176 | The suite contains one spec that tests the `getTodos` method:
177 |
178 | ```typescript
179 | it('gets the to-dos', async () => {
180 | /* … */
181 | });
182 | ```
183 |
184 | The spec starts with *Arrange* code:
185 |
186 | ```typescript
187 | // Arrange
188 | const fetchSpy = jasmine.createSpy('fetch')
189 | .and.returnValue(okResponse);
190 | const todoService = new TodoService(fetchSpy);
191 | ```
192 |
193 | Here, we create a spy. With `.and.returnValue(…)`, we set a fixed return value: the successful response.
194 |
195 |
196 |
197 | 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.
198 |
199 | In the *Act* phase, we call the method under test:
200 |
201 | ```typescript
202 | const actualTodos = await todoService.getTodos();
203 | ```
204 |
205 | `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.
206 |
207 | In the *Assert* phase, we create two expectations:
208 |
209 | ```typescript
210 | expect(actualTodos).toEqual(todos);
211 | expect(fetchSpy).toHaveBeenCalledWith('/todos');
212 | ```
213 |
214 |
215 |
216 | 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.)
217 |
218 |
219 |
220 | 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'`.
221 |
222 | Both expectations are necessary to guarantee that `getTodos` works correctly.
223 |
224 |
225 |
226 | 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:
227 |
228 | ```typescript
229 | if (!response.ok) {
230 | throw new Error(
231 | `HTTP error: ${response.status} ${response.statusText}`
232 | );
233 | }
234 | ```
235 |
236 | 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.
237 |
238 | 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:
239 |
240 | ```typescript
241 | const errorResponse = new Response('Not Found', {
242 | status: 404,
243 | statusText: 'Not Found',
244 | });
245 | ```
246 |
247 | Assuming the server does not return JSON in the error case, the response body is simply the string `'Not Found'`.
248 |
249 | Now we add a second spec for the error case:
250 |
251 | ```typescript
252 | describe('TodoService', () => {
253 | /* … */
254 | it('handles an HTTP error when getting the to-dos', async () => {
255 | // Arrange
256 | const fetchSpy = jasmine.createSpy('fetch')
257 | .and.returnValue(errorResponse);
258 | const todoService = new TodoService(fetchSpy);
259 |
260 | // Act
261 | let error;
262 | try {
263 | await todoService.getTodos();
264 | } catch (e) {
265 | error = e;
266 | }
267 |
268 | // Assert
269 | expect(error).toEqual(new Error('HTTP error: 404 Not Found'));
270 | expect(fetchSpy).toHaveBeenCalledWith('/todos');
271 | });
272 | });
273 | ```
274 |
275 | In the *Arrange* phase, we inject a spy that returns the error response.
276 |
277 |
278 |
279 | 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.
280 |
281 | 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.
282 |
283 | 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](../testing-services/#testing-a-service-that-sends-http-requests)).
284 |
285 |
286 | - [TodoService: Implementation and test code](https://github.com/9elements/angular-workshop/blob/main/src/app/services/todos-service.spec.ts)
287 | - [Jasmine reference: Spies](https://jasmine.github.io/api/edge/Spy.html)
288 |
289 |
290 | ## Spying on existing methods
291 |
292 | 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.
293 |
294 |
295 |
296 | 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.
297 |
298 | For this purpose, we can use the `spyOn` method:
299 |
300 | ```typescript
301 | spyOn(window, 'fetch');
302 | ```
303 |
304 |
305 |
306 | 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.
307 |
308 | `spyOn` returns the created spy, enabling us to set a return value, like we have learned above.
309 |
310 | ```typescript
311 | spyOn(window, 'fetch')
312 | .and.returnValue(okResponse);
313 | ```
314 |
315 | We can create a version of `TodoService` that does not rely on construction injection, but uses `fetch` directly:
316 |
317 | ```typescript
318 | class TodoService {
319 | public async getTodos(): Promise {
320 | const response = await fetch('/todos');
321 | if (!response.ok) {
322 | throw new Error(
323 | `HTTP error: ${response.status} ${response.statusText}`
324 | );
325 | }
326 | return await response.json();
327 | }
328 | }
329 | ```
330 |
331 | The test suite then uses `spyOn` to catch all calls to `window.fetch`:
332 |
333 | ```typescript
334 | // Fake todos and response object
335 | const todos = [
336 | 'shop groceries',
337 | 'mow the lawn',
338 | 'take the cat to the vet'
339 | ];
340 | const okResponse = new Response(JSON.stringify(todos), {
341 | status: 200,
342 | statusText: 'OK',
343 | });
344 |
345 | describe('TodoService', () => {
346 | it('gets the to-dos', async () => {
347 | // Arrange
348 | spyOn(window, 'fetch')
349 | .and.returnValue(okResponse);
350 | const todoService = new TodoService();
351 |
352 | // Act
353 | const actualTodos = await todoService.getTodos();
354 |
355 | // Assert
356 | expect(actualTodos).toEqual(todos);
357 | expect(window.fetch).toHaveBeenCalledWith('/todos');
358 | });
359 | });
360 | ```
361 |
362 | 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.
363 |
364 | 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.
365 |
366 |
371 |
--------------------------------------------------------------------------------
/index-of-example-applications.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: chapter
3 | title: Index of example applications
4 | description: Angular example projects with tests used across this book
5 | ---
6 |
7 | # Index of example applications
8 |
9 | All example applications are repositories on GitHub:
10 |
11 |
37 | {{ site.title }}
38 | {{ site.subtitle }}
39 | A free online book and e-book
40 |
41 |
42 |
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?
43 |
44 |
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.
45 |
46 |
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.
47 |
48 |
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.
49 |
50 |
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.
58 |
59 |
64 |
65 | {% include footer.html %}
66 |
--------------------------------------------------------------------------------
/introduction.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: chapter
3 | title: Introduction
4 | description: Why is automated testing so controversial and contested?
5 | ---
6 |
7 | # Introduction
8 |
9 |
17 |
18 | 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.
19 |
20 | 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.
21 |
22 | 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.
23 |
24 | 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.
25 |
26 | 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.
27 |
28 | 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.
29 |
30 | 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.
31 |
32 | 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.
33 |
34 | 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.
35 |
36 | 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.
37 |
38 |
39 |
--------------------------------------------------------------------------------
/license.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: chapter
3 | title: License
4 | description: Copyright and contact information for this book
5 | ---
6 |
7 | # License
8 |
9 | License: Creative Commons Attribution-ShareAlike (CC BY-SA 4.0)
10 |
11 | The example code is released into the public domain. See [Unlicense](https://unlicense.org/).
12 |
13 | The [Flickr search example application](https://github.com/9elements/angular-flickr-search) uses the [Flickr API](https://www.flickr.com/services/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.
14 |
15 | Online book cover photo: Flying probes testing a printed circuit board by genkur, [licensed from iStock](https://www.istockphoto.com/photo/printed-circuit-board-during-a-flying-probe-test-gm1144549508-307752215).
16 |
17 | E-book cover and favicon: [Official Angular logo](https://angular.io/presskit) by Google, licensed under [Creative Commons Attribution (CC BY 4.0)](https://creativecommons.org/licenses/by/4.0/).
18 |
19 | Legal notice: Impressum und Datenschutz
20 |
--------------------------------------------------------------------------------
/localhost.crt:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIDvjCCAqagAwIBAgIJAPPZEZ134kpJMA0GCSqGSIb3DQEBCwUAMIGKMQswCQYD
3 | VQQGEwJVUzELMAkGA1UECAwCS1MxDzANBgNVBAcMBk9sYXRoZTELMAkGA1UECgwC
4 | SVQxFjAUBgNVBAsMDUlUIERlcGFydG1lbnQxJDAiBgkqhkiG9w0BCQEWFXdlYm1h
5 | c3RlckBleGFtcGxlLmNvbTESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIxMDQzMDE3
6 | MDA1MloXDTIyMDQzMDE3MDA1MlowgYoxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJL
7 | UzEPMA0GA1UEBwwGT2xhdGhlMQswCQYDVQQKDAJJVDEWMBQGA1UECwwNSVQgRGVw
8 | YXJ0bWVudDEkMCIGCSqGSIb3DQEJARYVd2VibWFzdGVyQGV4YW1wbGUuY29tMRIw
9 | EAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
10 | AQCfH9gpwtANrB/XRrF250GRWrFWOuIMq8xNgs1FUQlfmdPOmAbFtBOI7sLL0oj+
11 | A4uyulF5wruRopsZbCumo3d8M9IpsgXBelqs31MDfxtsC2TSDUDwNPHGHZfKpQtC
12 | nMj8R9+Dt/degpZDaOM/tu3EBWnyEIboNT0mEmikOE+qyZFKs6dPssu9LY1DHQK2
13 | ptF133/69fy/jqYh8gCJMgUovBL8AWNr0zAC6LMidH9MuAqdxeTV0+oPZUQuqe5W
14 | JghdqxITbggj47T4Aa+Wi/LGBN/Rx93iZIBQAKvz5PcK6v7cvCGqxQaEjyGD01L0
15 | SJmvaD/7QuevayM9NZMlkOUTAgMBAAGjJTAjMCEGA1UdEQQaMBiCCyoubG9jYWxo
16 | b3N0gglsb2NhbGhvc3QwDQYJKoZIhvcNAQELBQADggEBAF0FZaSwDGzDAJrp/zTx
17 | xAIm3CG7cPGkD4RZuZBixYx51uqdBt59r4igNJeleqwnLBCESOsYJCzhQtVaFkWV
18 | 03a9rA8VnH6rzj0PC1zv3+7Vi45i/q+5RTA+FdWKsN6UF8mTf37jnIevnI9xU4mW
19 | a2wG+QitWq5gA+OFn/aD3VWVL48/oM1n6/fVxfmm7FtiIJEdQt872ZONoQhcgpJm
20 | fK4lvtKpVp3szHMdYQ6l5DCl9WS2du4M6z5CpL9XFM26CRzleN7tyZo6l3BiY+oW
21 | y+flQQTTGR+g1PFPKjFhdmhPOxF5efGgGgi8zbi6s4HAftzRaU0da4e6l1SyrW0y
22 | fQ8=
23 | -----END CERTIFICATE-----
24 |
--------------------------------------------------------------------------------
/localhost.key:
--------------------------------------------------------------------------------
1 | -----BEGIN PRIVATE KEY-----
2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCfH9gpwtANrB/X
3 | RrF250GRWrFWOuIMq8xNgs1FUQlfmdPOmAbFtBOI7sLL0oj+A4uyulF5wruRopsZ
4 | bCumo3d8M9IpsgXBelqs31MDfxtsC2TSDUDwNPHGHZfKpQtCnMj8R9+Dt/degpZD
5 | aOM/tu3EBWnyEIboNT0mEmikOE+qyZFKs6dPssu9LY1DHQK2ptF133/69fy/jqYh
6 | 8gCJMgUovBL8AWNr0zAC6LMidH9MuAqdxeTV0+oPZUQuqe5WJghdqxITbggj47T4
7 | Aa+Wi/LGBN/Rx93iZIBQAKvz5PcK6v7cvCGqxQaEjyGD01L0SJmvaD/7QuevayM9
8 | NZMlkOUTAgMBAAECggEBAIhxIsY0QHfoxmiuOk9NXiCH1pWs6dWZnhY8eUzdfp8V
9 | 5NM58lyl2zZIHInu3f6JBclcD67LNlBbUIjNAuThN/ws5yFPf7X19zhSSfkujVHo
10 | tyuRp8QQcdvB1j0xpeUZURFZg6OLJDZK3ROyJWGltlylk8G3QCZuOB+kG0vs6Qr0
11 | b0ZlmBfU3EehKDqgo0QbNAwOHQowNhyBsTSZPcgvD3uCXoAM4Hkfw8pDIwIY8Gz5
12 | sG+ACjywyQ6yG40l/0XYvgl5Bh6URItLp6X7nPCNkNeyIkJV6HMXvJiZrXcgPwim
13 | ILEjguN9kOiXt/AiULmtmrZ9zoBzX04j8EhbuguhoyECgYEAzkAMPpZ73RTwkuYX
14 | iPiBVJTZ3LGBN2h/XO5Q+ODhYrRP3OV2eJFBjch/z4DqiP8RS8zUxd5GX4bDAA/q
15 | GE8azC79wSS+040iOaDF7a1P+Wh4QQ/7XE3NDQTa2xJAbP0PZ9rvlnAkrVn2cny6
16 | Fk1TXYMM8FWJhRzCzK051QDF74MCgYEAxYHHLDzAj1fpl/NRt83svjRiMofgFunP
17 | HO3q8AtGt/kBBfmNyiGTTSqhLAW+TZ2dWr9tmTLX7hyUY4Ln0FMc1pxHcko+tIZa
18 | 1zI+37L7+vHsloANWwpUT9YHBkqw2ixf8O/uX5UGYc2SzyUnsT9SZ1iOlYPQJ6Yb
19 | 1V2Ay5lnLzECgYAXup6uBLozcVlMTVSf2Zdnl8iI75MiQM+GbZS7TYQgywX1MCE1
20 | NeEI4uxxfy98m3vt8J76NNx72RFOTIZuTYuTukRPmF6sECzD3I9pDOuKkk5jjecp
21 | c3oH6WsUkUEASQ0gsbum3zgZCaSk/1yZfEP/Gji+3dh4jBqNWdCxhOlA6QKBgQC7
22 | 7Girgb6iZT6A8uY9IjVxIPySIdCpXJxRZVsWPVRzdfxwR+uOePXkBXzHG0vgI+j1
23 | 0JCipMrp582VBZg2Eu6skJQ2fcg+Elxax5clV/MD6a534K1Ug3aHZBjY9rZhULmU
24 | 0WYZEf25j1VxvWOP9bUdWhiI0Jt9LkLreAU1M+gG8QKBgBBhJ3ARo372JjOAqniV
25 | EVcQW2v1sMGZ8Qa2P4D8oBOW2hjHzyMwsjgzFzr27M48TWZ1TmrtBOdZk0SismbI
26 | Gbz3iFROMgVo8/8wVaXGDprvoCJs5zM0N9HudzmFHVM8VgKPwbPI6JV0JkURC2qF
27 | HsGxhC/iwRCoHJAXZclkodJi
28 | -----END PRIVATE KEY-----
29 |
--------------------------------------------------------------------------------
/measuring-code-coverage.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: chapter
3 | title: Measuring code coverage
4 | description: How to measure test coverage of Angular applications
5 | ---
6 |
7 | # Measuring code coverage
8 |
9 |
17 |
18 | 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.
19 |
20 | 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.
21 |
22 | ## Coverage report
23 |
24 | In Angular’s Karma and Jasmine setup, [Istanbul](https://istanbul.js.org/) 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.
25 |
26 | To activate Istanbul when running the tests, add the `--code-coverage` parameter:
27 |
28 | ```
29 | ng test --code-coverage
30 | ```
31 |
32 | After the tests have completed, Istanbul saves the report in the `coverage` directory located in the Angular project directory.
33 |
34 | 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.
35 |
36 | The report for the Flickr search example looks like this:
37 |
38 |
39 |
40 |
41 |
42 | Istanbul creates an HTML page for every directory and every file. By following the links, you can descend to reports for the individual files.
43 |
44 | For example, the coverage report for [photo-item.component.ts](https://github.com/9elements/angular-flickr-search/blob/main/src/app/components/photo-item/photo-item.component.ts) of the Flickr search:
45 |
46 |
47 |
48 |
49 |
50 | 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”.
51 |
52 | 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.
53 |
54 |
55 |
56 |
57 |
58 | You can tell from the coverage report above that the `handleClick` method is never called. A key Component behavior is untested.
59 |
60 | ## How to use the coverage report
61 |
62 | Now that we know how to generate the report, what should we do with it?
63 |
64 | In the chapter [The right amount of testing](../testing-principles/#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.
65 |
66 | 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?
67 |
68 |
69 |
70 | 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.
71 |
72 | Whatever your current coverage score is, use the reporting to monitor and improve your testing practice. As described in [Tailoring your testing approach](../testing-principles/#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.
73 |
74 |
75 |
76 | 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.
77 |
78 | 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](../angular-testing-principles/#configuring-karma-and-jasmine).
79 |
80 | In `karma.conf.js`, you can add global thresholds for statements, branches, functions and lines.
81 |
82 | ```
83 | coverageReporter: {
84 | /* … */
85 | check: {
86 | global: {
87 | statements: 75,
88 | branches: 75,
89 | functions: 75,
90 | lines: 75,
91 | },
92 | },
93 | },
94 | ```
95 |
96 | 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.
97 |
98 |
99 |
100 | 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.
101 |
102 | 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.
103 |
104 | 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.
105 |
106 |
30 |
--------------------------------------------------------------------------------
/target-audience.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: chapter
3 | title: Target Audience
4 | description: What you should know when reading this book
5 | ---
6 |
7 | # Target audience
8 |
9 |
16 |
17 | The target audience of this guide are intermediate Angular developers. You should be familiar with Angular’s core concepts.
18 |
19 |
20 |
21 | 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](https://angular.io/docs).
22 |
23 | 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.
24 |
25 |
26 |
27 | 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.
28 |
29 | The official Angular documentation offers a comprehensive [guide on testing](https://angular.io/guide/testing). It is a recommended reading, but this guide does not assume you have read it.
30 |
31 |
32 |
--------------------------------------------------------------------------------
/terminology.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: chapter
3 | title: Terminology
4 | description: How this book uses Angular technical terms
5 | ---
6 |
7 | # Terminology
8 |
9 |
15 |
16 | Before we dive in, a quick note regarding the technical terms.
17 |
18 | 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.
19 |
20 | When referring to core Angular concepts, this guide uses **upper case**:
21 |
22 | *Module, Component, Service, Input, Output, Directive, Pipe*, etc.
23 |
24 | When using these terms in the general sense, this guide uses **lower case**:
25 |
26 | *module, component, service, input, output*, etc.
27 |
28 |
29 |
--------------------------------------------------------------------------------
/test-suites-with-jasmine.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: chapter
3 | title: Test suites with Jasmine
4 | description: How to create and structure efficient tests using the Jasmine library
5 | ---
6 |
7 | # Test suites with Jasmine
8 |
9 |
17 |
18 | Angular ships with Jasmine, a JavaScript framework that enables you to write and execute unit and integration tests. Jasmine consists of three important parts:
19 |
20 | 1. A library with classes and functions for constructing tests.
21 | 2. A test execution engine.
22 | 3. A reporting engine that outputs test results in different formats.
23 |
24 | If you are new to Jasmine, it is recommended to read the [official Jasmine tutorial](https://jasmine.github.io/tutorials/your_first_suite). This guide provides a short introduction to Jasmine, exploring the basic structure and terminology that will be used throughout this guide.
25 |
26 | ## Creating a test suite
27 |
28 | In terms of Jasmine, a test consists of one or more **suites**. A suite is declared with a `describe` block:
29 |
30 | ```typescript
31 | describe('Suite description', () => {
32 | /* … */
33 | });
34 | ```
35 |
36 | Each suite *describes* a piece of code, the *code under test*.
37 |
38 |
41 |
42 | `describe` is a function that takes two parameters.
43 |
44 | 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.
45 | 2. A function containing the suite definition.
46 |
47 | A `describe` block groups related specs that we will learn about in the next chapter.
48 |
49 |
52 |
53 | `describe` blocks can be nested to structure big suites and divide them into logical sections:
54 |
55 | ```typescript
56 | describe('Suite description', () => {
57 | describe('One aspect', () => {
58 | /* … */
59 | });
60 | describe('Another aspect', () => {
61 | /* … */
62 | });
63 | });
64 | ```
65 |
66 | Nested `describe` blocks add a human-readable description to a group of specs. They can also host their own setup and teardown logic.
67 |
68 | ## Specifications
69 |
70 |
73 |
74 | Each suit consists of one or more *specifications*, or short, **specs**. A spec is declared with an `it` block:
75 |
76 | ```typescript
77 | describe('Suite description', () => {
78 | it('Spec description', () => {
79 | /* … */
80 | });
81 | /* … more specs … */
82 | });
83 | ```
84 |
85 | 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.
86 |
87 |
88 |
89 | 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).
90 |
91 | 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.
92 |
93 | 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:
94 |
95 | ```typescript
96 | it('increments the count', () => {
97 | /* … */
98 | });
99 | it('resets the count', () => {
100 | /* … */
101 | });
102 | ```
103 |
104 | After `it`, typically a verb follows, like `increments` and `resets` in the example.
105 |
106 |
107 |
108 | 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.
109 |
110 |
111 | - [Jasmine tutorial: Your first suite](https://jasmine.github.io/tutorials/your_first_suite)
112 |
113 |
114 | ## Structure of a test
115 |
116 | 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**.
117 |
118 |
119 |
120 | 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.
121 | 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.
122 | 3. **Assert** is the phase where the code behavior is checked and verified. For example, the actual output is compared to the expected output.
123 |
124 | How could the structure of the spec `it('resets the count', /* … */)` for the `CounterComponent` look like?
125 |
126 | 1.
Arrange:
127 |
128 | - Create an instance of `CounterComponent`.
129 | - Render the Component into the document.
130 |
131 | 2.
Act:
132 |
133 | - Find and focus the reset input field.
134 | - Enter the text “5”.
135 | - Find and click the “Reset” button.
136 |
137 | 3.
Assert:
138 |
139 | - Expect that the displayed count now reads “5”.
140 |
141 |
142 |
143 | This structure makes it easier to come up with a test and also to implement it. Ask yourself:
144 |
145 | - What is the necessary setup? Which dependencies do I need to provide? How do they behave? (*Arrange*)
146 | - What is the user input or API call that triggers the behavior I would like to test? (*Act*)
147 | - What is the expected behavior? How do I prove that the behavior is correct? (*Assert*)
148 |
149 |
150 |
151 | 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.”
152 |
153 | ## Expectations
154 |
155 | 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.
156 |
157 | Let us examine a simple contrived example, an `add` function:
158 |
159 | ```typescript
160 | const add = (a, b) => a + b;
161 | ```
162 |
163 | A primitive test without any testing tools could look like this:
164 |
165 | ```typescript
166 | const expectedValue = 5;
167 | const actualValue = add(2, 3);
168 | if (expectedValue !== actualValue) {
169 | throw new Error(
170 | `Wrong return value: ${actualValue}. Expected: ${expectedValue}`
171 | );
172 | }
173 | ```
174 |
175 |
178 |
179 | 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**.
180 |
181 | ```typescript
182 | const expectedValue = 5;
183 | const actualValue = add(2, 3);
184 | expect(actualValue).toBe(expectedValue);
185 | ```
186 |
187 | 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.
188 |
189 |
190 |
191 | `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`.
192 |
193 | `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.
194 |
195 | For checking the deep equality of two objects, Jasmine offers the `toEqual` matcher. This example illustrates the difference:
196 |
197 | ```typescript
198 | // Fails, the two objects are not identical
199 | expect({ name: 'Linda' }).toBe({ name: 'Linda' });
200 |
201 | // Passes, the two objects are not identical but deeply equal
202 | expect({ name: 'Linda' }).toEqual({ name: 'Linda' });
203 | ```
204 |
205 | 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.
206 |
207 |
208 |
209 | 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.
210 |
211 |
215 |
216 | ## Efficient test suites
217 |
218 | 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.
219 |
220 |
221 |
222 | 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.
223 |
224 | 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.
225 |
226 | ```typescript
227 | describe('Suite description', () => {
228 | beforeAll(() => {
229 | console.log('Called before all specs are run');
230 | });
231 | afterAll(() => {
232 | console.log('Called after all specs are run');
233 | });
234 |
235 | beforeEach(() => {
236 | console.log('Called before each spec is run');
237 | });
238 | afterEach(() => {
239 | console.log('Called after each spec is run');
240 | });
241 |
242 | it('Spec 1', () => {
243 | console.log('Spec 1');
244 | });
245 | it('Spec 2', () => {
246 | console.log('Spec 2');
247 | });
248 | });
249 | ```
250 |
251 | This suite has two specs and defines shared setup and teardown code. The output is:
252 |
253 | ```
254 | Called before all specs are run
255 | Called before each spec is run
256 | Spec 1
257 | Called after each spec is run
258 | Called before each spec is run
259 | Spec 2
260 | Called after each spec is run
261 | Called after all specs are run
262 | ```
263 |
264 | Most tests we are going to write will have a `beforeEach` block to host the *Arrange* code.
265 |
266 |
267 |
--------------------------------------------------------------------------------
/testing-modules.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: chapter
3 | title: Testing Modules
4 | description: Automated smoke tests for Angular Modules
5 | ---
6 |
7 | # Testing Modules
8 |
9 |
15 |
16 | 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.
17 |
18 |
19 |
20 | Angular Modules are classes, but most of the time, the class itself is empty. The essence lies in the metadata set with `@NgModule({ … })`.
21 |
22 | We could sneak into the metadata and check whether certain Services are provided, whether third-party Modules are imported and whether Components are exported.
23 |
24 | But such a test would simply **mirror the implementation**. Code duplication does not give you more confidence, it only increases the cost of change.
25 |
26 | 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.
27 |
28 |
29 |
30 | There are certain Module errors that only surface during runtime. These can be caught with a *smoke test*. Given this Module:
31 |
32 | ```typescript
33 | import { NgModule } from '@angular/core';
34 | import { CommonModule } from '@angular/common';
35 | import { ExampleComponent } from './example.component';
36 |
37 | @NgModule({
38 | declarations: [ExampleComponent],
39 | imports: [CommonModule],
40 | })
41 | export class FeatureModule {}
42 | ```
43 |
44 | We write this smoke test:
45 |
46 | ```typescript
47 | import { TestBed } from '@angular/core/testing';
48 | import { FeatureModule } from './example.module';
49 |
50 | describe('FeatureModule', () => {
51 | beforeEach(() => {
52 | TestBed.configureTestingModule({
53 | imports: [FeatureModule],
54 | });
55 | });
56 |
57 | it('initializes', () => {
58 | const module = TestBed.inject(FeatureModule);
59 | expect(module).toBeTruthy();
60 | });
61 | });
62 | ```
63 |
64 | The integration test uses the `TestBed` to import the Module under test. It verifies that no error occurs when importing the Module.
65 |
66 |
67 |
--------------------------------------------------------------------------------
/testing-pipes.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: chapter
3 | title: Testing Pipes
4 | description: How to test simple and complex, pure and impure Angular Pipes
5 | ---
6 |
7 | # Testing Pipes
8 |
9 |
15 |
16 | 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.
17 |
18 | 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.
19 |
20 | In this example, the value from `user.birthday` is transformed by the `date` Pipe:
21 |
22 | ```
23 | {% raw %}{{ user.birthday | date }}{% endraw %}
24 | ```
25 |
26 |
27 |
28 | 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.
29 |
30 | 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.
31 |
32 |
33 |
34 | 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.
35 |
36 | ## GreetPipe
37 |
38 | 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.
39 |
40 | ```typescript
41 | import { Pipe, PipeTransform } from '@angular/core';
42 |
43 | @Pipe({ name: 'greet' })
44 | export class GreetPipe implements PipeTransform {
45 | transform(name: string): string {
46 | return `Hello, ${name}!`;
47 | }
48 | }
49 | ```
50 |
51 | In a Component template, we transform a value using the Pipe:
52 |
53 | ```
54 | {% raw %}{{ 'Julie' | greet }}{% endraw %}
55 | ```
56 |
57 | The `GreetPipe` take the string `'Julie'` and computes a new string, `'Hello, Julie!'`.
58 |
59 |
60 |
61 | There are two important ways to test a Pipe:
62 |
63 | 1. Create an instance of the Pipe class manually. Then call the `transform` method.
64 |
65 | This way is fast and straight-forward. It requires minimal setup.
66 |
67 | 2. Set up a `TestBed`. Render a host Component that uses the Pipe. Then check the text content in the DOM.
68 |
69 | 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.
70 |
71 | 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.
72 |
73 | ## GreetPipe test
74 |
75 | The `GreetPipe` does not have any dependencies. We opt for the first way and write a unit test that examines the single instance.
76 |
77 | 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.
78 |
79 | ```typescript
80 | describe('GreetPipe', () => {
81 | let greetPipe: GreetPipe;
82 |
83 | beforeEach(() => {
84 | greetPipe = new GreetPipe();
85 | });
86 |
87 | it('says Hello', () => {
88 | expect(greetPipe.transform('Julie')).toBe('Hello, Julie!');
89 | });
90 | });
91 | ```
92 |
93 | We call the `transform` method with the string `'Julie'` and expect the output `'Hello, Julie!'`.
94 |
95 | 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.
96 |
97 | ## Testing Pipes with dependencies
98 |
99 | 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.
100 |
101 | We are introducing and testing the `TranslatePipe`, a complex Pipe with a Service dependency.
102 |
103 |
104 | - [TranslatePipe: Source code](https://github.com/molily/translate-pipe)
105 | - [TranslatePipe: Run the app](https://molily.github.io/translate-pipe/)
106 |
107 |
108 |
111 |
112 |
117 |
118 | The example application lets you change the user interface language during runtime. A popular solution for this task is the [ngx-translate](https://github.com/ngx-translate/core) library. For the purpose of this guide, we will adopt ngx-translate’s proven approach but implement and test the code ourselves.
119 |
120 | ### TranslateService
121 |
122 | The current language is stored in the `TranslateService`. This Service also loads and holds the translations for the current language.
123 |
124 | 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.
125 |
126 | The `TranslateService` looks like this:
127 |
128 | ```typescript
129 | import { HttpClient } from '@angular/common/http';
130 | import { EventEmitter, Injectable } from '@angular/core';
131 | import { Observable, of } from 'rxjs';
132 | import { map, take } from 'rxjs/operators';
133 |
134 | export interface Translations {
135 | [key: string]: string;
136 | }
137 |
138 | @Injectable()
139 | export class TranslateService {
140 | /** The current language */
141 | private currentLang = 'en';
142 |
143 | /** Translations for the current language */
144 | private translations: Translations | null = null;
145 |
146 | /** Emits when the language change */
147 | public onTranslationChange = new EventEmitter();
148 |
149 | constructor(private http: HttpClient) {
150 | this.loadTranslations(this.currentLang);
151 | }
152 |
153 | /** Changes the language */
154 | public use(language: string): void {
155 | this.currentLang = language;
156 | this.loadTranslations(language);
157 | }
158 |
159 | /** Translates a key asynchronously */
160 | public get(key: string): Observable {
161 | if (this.translations) {
162 | return of(this.translations[key]);
163 | }
164 | return this.onTranslationChange.pipe(
165 | take(1),
166 | map((translations) => translations[key])
167 | );
168 | }
169 |
170 | /** Loads the translations for the given language */
171 | private loadTranslations(language: string): void {
172 | this.translations = null;
173 | this.http
174 | .get(`assets/${language}.json`)
175 | .subscribe((translations) => {
176 | this.translations = translations;
177 | this.onTranslationChange.emit(translations);
178 | });
179 | }
180 | }
181 | ```
182 |
183 | This is what the Service provides:
184 |
185 | 1. `use` method: Set the current language and load the translations as JSON via HTTP.
186 | 2. `get` method: Get the translation for a key.
187 | 3. `onTranslationChange` `EventEmitter`: Observing changes on the translations as a result of `use`.
188 |
189 | 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.
190 |
191 |
288 |
289 | ### TranslatePipe test
290 |
291 | 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.
292 |
293 | `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.
294 |
295 | ```typescript
296 | let translateService: Pick<
297 | TranslateService, 'onTranslationChange' | 'get'
298 | >;
299 | /* … */
300 | translateService = {
301 | onTranslationChange: new EventEmitter(),
302 | get(key: string): Observable {
303 | return of(`Translation for ${key}`);
304 | },
305 | };
306 | ```
307 |
308 | 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.
309 |
310 |
311 |
312 | 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.
313 |
314 | Let us start with the host Component:
315 |
316 | ```typescript
317 | const key1 = 'key1';
318 | const key2 = 'key2';
319 |
320 | @Component({
321 | template: '{% raw %}{{ key | translate }}{% endraw %}',
322 | })
323 | class HostComponent {
324 | public key = key1;
325 | }
326 | ```
327 |
328 | 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.
329 |
330 | Let us set up the test suite:
331 |
332 | ```typescript
333 | describe('TranslatePipe: with TestBed and HostComponent', () => {
334 | let fixture: ComponentFixture;
335 | let translateService: Pick<
336 | TranslateService, 'onTranslationChange' | 'get'
337 | >;
338 |
339 | beforeEach(async () => {
340 | translateService = {
341 | onTranslationChange: new EventEmitter(),
342 | get(key: string): Observable {
343 | return of(`Translation for ${key}`);
344 | },
345 | };
346 |
347 | await TestBed.configureTestingModule({
348 | declarations: [TranslatePipe, HostComponent],
349 | providers: [
350 | { provide: TranslateService, useValue: translateService }
351 | ],
352 | }).compileComponents();
353 |
354 | translateService = TestBed.inject(TranslateService);
355 | fixture = TestBed.createComponent(HostComponent);
356 | });
357 |
358 | /* … */
359 | });
360 | ```
361 |
362 | 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.
363 |
364 |
365 |
366 | What needs to be tested? We need to check that `{% raw %}{{ key | translate }}{% endraw %}` evaluates to `Translation for key1`. There are two cases that need to be tested though:
367 |
368 | 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.
369 | 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.
370 |
371 | In the test, we write specs for both scenarios:
372 |
373 | ```typescript
374 | it('translates the key, sync service response', /* … */);
375 | it('translates the key, async service response', /* … */);
376 | ```
377 |
378 | Let us start with the first case. The spec is straight-forward.
379 |
380 | ```typescript
381 | it('translates the key, sync service response', () => {
382 | fixture.detectChanges();
383 | expectContent(fixture, 'Translation for key1');
384 | });
385 | ```
386 |
387 | Remember, the `TranslateService` fake returns an Observable created with `of`.
388 |
389 | ```typescript
390 | return of(`Translation for ${key}`);
391 | ```
392 |
393 | This Observable emits one value and completes immediately. This mimics the case in which the Service has already loaded the translations.
394 |
395 | 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.
396 |
397 | Finally, we use the [`expectContent` Component helper](https://github.com/molily/translate-pipe/blob/main/src/app/spec-helpers/element.spec-helper.ts) to test the DOM output.
398 |
399 |
400 |
401 | 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](https://rxjs.dev/api/operators/delay) for simplicity.
402 |
403 | 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.
404 |
405 |
408 |
409 | 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](../testing-complex-forms/#successful-form-submission).
410 |
411 | 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.
412 |
413 | `fakeAsync` wraps the function passed to `it`:
414 |
415 | ```typescript
416 | it('translates the key, async service response', fakeAsync(() => {
417 | /* … */
418 | });
419 | ```
420 |
421 | Next, we need to change the `TranslateService`’s `get` method to make it asynchronous.
422 |
423 | ```typescript
424 | it('translates the key, async service response', fakeAsync(() => {
425 | translateService.get = (key) =>
426 | of(`Async translation for ${key}`).pipe(delay(100));
427 | /* … */
428 | });
429 | ```
430 |
431 |
432 |
433 | 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.
434 |
435 | Now, we can call `detectChanges` for the first time.
436 |
437 | ```typescript
438 | it('translates the key, async service response', fakeAsync(() => {
439 | translateService.get = (key) =>
440 | of(`Async translation for ${key}`).pipe(delay(100));
441 | fixture.detectChanges();
442 | /* … */
443 | });
444 | ```
445 |
446 | The Pipe’s `transform` method is called for the first time and returns `null` since the Observable does not emit a value immediately.
447 |
448 | So we expect that the output is empty:
449 |
450 | ```typescript
451 | it('translates the key, async service response', fakeAsync(() => {
452 | translateService.get = (key) =>
453 | of(`Async translation for ${key}`).pipe(delay(100));
454 | fixture.detectChanges();
455 | expectContent(fixture, '');
456 | /* … */
457 | });
458 | ```
459 |
460 |
461 |
462 | Here comes the interesting part. We want the Observable to emit a value now. We simulate the passage of 100 milliseconds with `tick(100)`.
463 |
464 | ```typescript
465 | it('translates the key, async service response', fakeAsync(() => {
466 | translateService.get = (key) =>
467 | of(`Async translation for ${key}`).pipe(delay(100));
468 | fixture.detectChanges();
469 | expectContent(fixture, '');
470 |
471 | tick(100);
472 | /* … */
473 | });
474 | ```
475 |
476 | This causes the Observable to emit the translation and complete. The Pipe receives the translation and saves it.
477 |
478 | 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.
479 |
480 | ```typescript
481 | it('translates the key, async service response', fakeAsync(() => {
482 | translateService.get = (key) =>
483 | of(`Async translation for ${key}`).pipe(delay(100));
484 | fixture.detectChanges();
485 | expectContent(fixture, '');
486 |
487 | tick(100);
488 | fixture.detectChanges();
489 | expectContent(fixture, 'Async translation for key1');
490 | }));
491 | ```
492 |
493 | Testing these details may seem pedantic at first. But the logic in `TranslatePipe` exists for a reason.
494 |
495 | There are two specs left to write:
496 |
497 | ```typescript
498 | it('translates a changed key', /* … */);
499 | it('updates on translation change', /* … */);
500 | ```
501 |
502 | 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.
503 |
504 |
505 |
506 | 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`.
507 |
508 | ```typescript
509 | it('translates a changed key', () => {
510 | fixture.detectChanges();
511 | fixture.componentInstance.key = key2;
512 | fixture.detectChanges();
513 | expectContent(fixture, 'Translation for key2');
514 | });
515 | ```
516 |
517 | After a change detection, the DOM contains the updated translation for `key2`.
518 |
519 |
520 |
521 | 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.
522 |
523 | 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.
524 |
525 | ```typescript
526 | it('updates on translation change', () => {
527 | fixture.detectChanges();
528 | translateService.get = (key) =>
529 | of(`New translation for ${key}`);
530 | translateService.onTranslationChange.emit({});
531 | fixture.detectChanges();
532 | expectContent(fixture, 'New translation for key1');
533 | });
534 | ```
535 |
536 | We made it! Writing these specs is challenging without doubt.
537 |
538 | `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.
539 |
540 |