Skip to content

Commit 1130124

Browse files
committed
services added
1 parent 16f627b commit 1130124

7 files changed

Lines changed: 475 additions & 0 deletions

File tree

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`.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { defer } from 'rxjs';
2+
3+
export function asyncData<T>(data: T) {
4+
return defer(() => Promise.resolve(data));
5+
}
6+
7+
export function asyncError<T>(errorObject: any) {
8+
return defer(() => Promise.reject(errorObject));
9+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { MasterService } from "./master.service";
2+
import { ValueService } from "./value.service";
3+
4+
class FakeValueService {
5+
getValue() {
6+
return "faked service value";
7+
}
8+
}
9+
10+
describe("MasterService", () => {
11+
let service: MasterService;
12+
13+
it("#getValue should return real value from the real service", () => {
14+
service = new MasterService(new ValueService());
15+
expect(service.getValue()).toBe("real value");
16+
});
17+
18+
it("#getValue should return faked value from a fakeService", () => {
19+
service = new MasterService(new FakeValueService() as ValueService);
20+
expect(service.getValue()).toBe("faked service value");
21+
});
22+
23+
it("#getValue should return faked value from a fake object", () => {
24+
const fake = { getValue: () => "fake value" };
25+
service = new MasterService(fake as ValueService);
26+
expect(service.getValue()).toBe("fake value");
27+
});
28+
29+
it("#getValue should return value from a spy", () => {
30+
const valueServiceSpy = jasmine.createSpyObj("ValueService", ["getValue"]);
31+
valueServiceSpy.getValue.and.returnValue("stub value");
32+
33+
service = new MasterService(valueServiceSpy);
34+
35+
expect(service.getValue())
36+
.withContext("service returned stub value")
37+
.toBe("stub value");
38+
expect(valueServiceSpy.getValue.calls.count())
39+
.withContext("spy method was called once")
40+
.toBe(1);
41+
expect(valueServiceSpy.getValue.calls.mostRecent().returnValue).toBe(
42+
"stub value"
43+
);
44+
});
45+
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Injectable } from "@angular/core";
2+
import { ValueService } from "./value.service";
3+
4+
@Injectable({
5+
providedIn: "root",
6+
})
7+
export class MasterService {
8+
constructor(private valueService: ValueService) {}
9+
10+
getValue() {
11+
return this.valueService.getValue();
12+
}
13+
}

0 commit comments

Comments
 (0)