Skip to content

Commit 581e53f

Browse files
authored
Merge pull request #796 from Lemoncode/angular-testing-2025
Angular testing 2025
2 parents ba76bf6 + 8447636 commit 581e53f

59 files changed

Lines changed: 19961 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Testing Pipes
2+
3+
> You can test pipes without the Angular testing utilities
4+
5+
```bash
6+
npx ng g p titlecase
7+
```
8+
9+
Update `titlecase.pipe.ts`
10+
11+
```ts
12+
import { Pipe, PipeTransform } from "@angular/core";
13+
14+
@Pipe({
15+
name: "titlecase",
16+
pure: true,
17+
})
18+
export class TitlecasePipe implements PipeTransform {
19+
transform(input: string): string {
20+
return input.length === 0
21+
? ""
22+
: input.replace(
23+
/\w\S*/g,
24+
(txt) => txt[0].toUpperCase() + txt.slice(1).toLowerCase()
25+
);
26+
}
27+
}
28+
```
29+
30+
Update `titlecase.pipe.spec.ts`
31+
32+
```ts
33+
import { TitlecasePipe } from './titlecase.pipe';
34+
35+
describe('TitlecasePipe', () => {
36+
const pipe = new TitlecasePipe();
37+
38+
it('transforms "abc" to "Abc"', () => {
39+
expect(pipe.transform('abc')).toBe('Abc');
40+
});
41+
42+
it('transforms "abc def" to "Abc Def"', () => {
43+
expect(pipe.transform('abc def')).toBe('Abc Def');
44+
});
45+
});
46+
47+
```
48+
49+
```bash
50+
npx ng test --include app/titlecase.pipe.spec.ts
51+
```
52+
53+
## Writing DOM tests to support a pipe test
54+
55+
Previous tests are working in isolation. They can't tell if the `TitleCasePipe` is working properly as applied in the application components.
56+
57+
### Exercise
58+
59+
Create a component that uses `HighlightDirective` and create a test that ensures that the directive is applied.
Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
# Testing Services
2+
3+
```bash
4+
npx ng g s value --skip-tests
5+
```
6+
7+
Update `value.service.spec.ts`
8+
9+
```ts
10+
import { Injectable } from "@angular/core";
11+
import { Observable, of } from "rxjs";
12+
13+
@Injectable({
14+
providedIn: "root",
15+
})
16+
export class ValueService {
17+
constructor() {}
18+
19+
getValue(): string {
20+
return "real value";
21+
}
22+
23+
getObservableValue(): Observable<string> {
24+
return of("observable value");
25+
}
26+
27+
getPromiseValue(): Promise<string> {
28+
return Promise.resolve("promise value");
29+
}
30+
}
31+
```
32+
33+
## Services with Dependencies
34+
35+
```bash
36+
npx ng g s master
37+
```
38+
39+
Update `master.service.ts`
40+
41+
```ts
42+
import { Injectable } from "@angular/core";
43+
import { ValueService } from "./value.service";
44+
45+
@Injectable({
46+
providedIn: "root",
47+
})
48+
export class MasterService {
49+
constructor(private valueService: ValueService) {}
50+
51+
getValue() {
52+
return this.valueService.getValue();
53+
}
54+
}
55+
```
56+
57+
We can test this kind of services on different ways. Angular gives us support to deal with dependencies, but we can relay on different techniques out of Angular goodies.
58+
59+
Update `master.service.spec.ts`
60+
61+
```ts
62+
import { MasterService } from "./master.service";
63+
import { ValueService } from "./value.service";
64+
65+
class FakeValueService {
66+
getValue() {
67+
return "faked service value";
68+
}
69+
}
70+
71+
describe("MasterService", () => {
72+
let service: MasterService;
73+
74+
it("#getValue should return real value from the real service", () => {
75+
service = new MasterService(new ValueService());
76+
expect(service.getValue()).toBe("real value");
77+
});
78+
79+
it("#getValue should return faked value from a fakeService", () => {
80+
service = new MasterService(new FakeValueService() as ValueService);
81+
expect(service.getValue()).toBe("faked service value");
82+
});
83+
84+
it("#getValue should return faked value from a fake object", () => {
85+
const fake = { getValue: () => "fake value" };
86+
service = new MasterService(fake as ValueService);
87+
expect(service.getValue()).toBe("fake value");
88+
});
89+
90+
it("#getValue should return value from a spy", () => {
91+
const valueServiceSpy = jasmine.createSpyObj("ValueService", ["getValue"]);
92+
valueServiceSpy.getValue.and.returnValue("stub value");
93+
94+
service = new MasterService(valueServiceSpy);
95+
96+
expect(service.getValue())
97+
.withContext("service returned stub value")
98+
.toBe("stub value");
99+
expect(valueServiceSpy.getValue.calls.count())
100+
.withContext("spy method was called once")
101+
.toBe(1);
102+
expect(valueServiceSpy.getValue.calls.mostRecent().returnValue).toBe(
103+
"stub value"
104+
);
105+
});
106+
});
107+
```
108+
109+
```bash
110+
npx ng test --include app/master.service.spec.ts
111+
```
112+
113+
These standard testing techniques are great for unit testing services in isolation.
114+
115+
However, you almost always inject services into application classes using Angular dependency injection and you should have tests that reflect that usage pattern. Angular testing utilities make it straightforward to investigate how injected services behave.
116+
117+
## Testing HTTP services
118+
119+
Data services that make HTTP calls to remote servers typically inject and delegate to the Angular `HttpClient` service for XHR calls.
120+
121+
You can test a data service with an injected `HttpClient` spy as you would test any service with a dependency.
122+
123+
```bash
124+
npx ng g s met
125+
```
126+
127+
Update `met.service.ts`
128+
129+
```ts
130+
import { HttpClient } from "@angular/common/http";
131+
import { Injectable } from "@angular/core";
132+
import { map, Observable, tap } from "rxjs";
133+
134+
// API REFERENCE: https://metmuseum.github.io/
135+
export interface Object {
136+
id: number;
137+
title: string;
138+
}
139+
140+
export interface Department {
141+
id: number;
142+
name: string;
143+
}
144+
145+
@Injectable({
146+
providedIn: "root",
147+
})
148+
export class MetService {
149+
private url = "https://collectionapi.metmuseum.org/public/collection/v1";
150+
151+
// https://collectionapi.metmuseum.org/public/collection/v1/objects/[objectID]
152+
// https://collectionapi.metmuseum.org/public/collection/v1/departments
153+
154+
constructor(private http: HttpClient) {}
155+
156+
getObjectById(id: number): Observable<Object> {
157+
return this.http.get<Object>(`${this.url}/objects/${id}`).pipe(
158+
tap((d) => console.log(d)),
159+
map((q: any) => {
160+
return q as Object;
161+
})
162+
);
163+
}
164+
165+
getDepartments(): Observable<Department[]> {
166+
return this.http.get<Department[]>(`${this.url}/departments`).pipe(
167+
tap((d) => console.log(d)),
168+
map((result: any) => {
169+
const { departments } = result;
170+
return (departments as any[]).map((d) => ({
171+
id: d.departmentId,
172+
name: d.displayName,
173+
}));
174+
})
175+
);
176+
}
177+
}
178+
```
179+
180+
Create `app/async-observable-helpers.ts`
181+
182+
```ts
183+
import { defer } from "rxjs";
184+
185+
export function asyncData<T>(data: T) {
186+
return defer(() => Promise.resolve(data));
187+
}
188+
189+
export function asyncError<T>(errorObject: any) {
190+
return defer(() => Promise.reject(errorObject));
191+
}
192+
```
193+
194+
Update `app/met.service.spec.ts`
195+
196+
```ts
197+
import { Department, MetService } from "./met.service";
198+
import { HttpClient } from "@angular/common/http";
199+
import { asyncData } from "./async-observable-helpers";
200+
201+
describe("MetService", () => {
202+
let service: MetService;
203+
let httpClientSpy: jasmine.SpyObj<HttpClient>;
204+
205+
beforeEach(() => {
206+
httpClientSpy = jasmine.createSpyObj("HttpClient", ["get"]);
207+
service = new MetService(httpClientSpy);
208+
});
209+
210+
it("should return expected departments (HttpClient called once)", (done: DoneFn) => {
211+
const departmentsModel: {
212+
departments: { departmentId: number; displayName: string }[];
213+
} = {
214+
departments: [
215+
{ departmentId: 1, displayName: "Foo" },
216+
{ departmentId: 2, displayName: "Boo" },
217+
],
218+
};
219+
220+
const expectedDepartments: Department[] = [
221+
{ id: 1, name: "Foo" },
222+
{ id: 2, name: "Boo" },
223+
];
224+
225+
httpClientSpy.get.and.returnValue(asyncData(departmentsModel));
226+
227+
service.getDepartments().subscribe({
228+
next: (departments) => {
229+
expect(departments)
230+
.withContext("expected departments")
231+
.toEqual(expectedDepartments);
232+
done();
233+
},
234+
error: done.fail,
235+
});
236+
expect(httpClientSpy.get.calls.count()).withContext("one call").toBe(1);
237+
});
238+
});
239+
```
240+
241+
```bash
242+
npx ng test --include app/met.service.spec.ts
243+
```
244+
245+
We can even test expected errors.
246+
247+
```diff
248+
....
249+
-import { HttpClient } from "@angular/common/http";
250+
+import { HttpClient, HttpErrorResponse } from "@angular/common/http";
251+
-import { asyncData } from "./async-observable-helpers";
252+
+import { asyncData, asyncError } from "./async-observable-helpers";
253+
....
254+
+ it("should return an error when the server returns a 404", (done: DoneFn) => {
255+
+ const errorResponse = new HttpErrorResponse({
256+
+ error: "test 404 error",
257+
+ status: 404,
258+
+ statusText: "Not Found",
259+
+ });
260+
+
261+
+ httpClientSpy.get.and.returnValue(asyncError(errorResponse));
262+
+
263+
+ service.getDepartments().subscribe({
264+
+ next: (departments) => done.fail("expected and error, not departments"),
265+
+ error: ({ error }) => {
266+
+ expect(error).toContain("test 404 error");
267+
+ done();
268+
+ },
269+
+ });
270+
});
271+
```
272+
273+
## HttpClientTestingModule
274+
275+
Extended interactions between a data service and the `HttpClient` can be complex and difficult to mock with spies.
276+
277+
The `HttpClientTestingModule` can make these testing scenarios more manageable.
278+
279+
Follow [HTTP Client Testing](https://angular.dev/guide/http/testing) for detail guide for using `HttpClientTestingModule`.

0 commit comments

Comments
 (0)