Skip to content

Commit 6bed4f8

Browse files
committed
async services component tests added
1 parent 00bd25b commit 6bed4f8

7 files changed

Lines changed: 475 additions & 4 deletions

File tree

05-testing/02-angular-2025/06-component-async-service.md

Lines changed: 181 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
## Component with async service
22

33
```bash
4-
npx ng g s twain
4+
npx ng g s twain/twain
55
```
66

7-
Create `quote.ts`
7+
Create `twain/quote.ts`
88

99
```ts
1010
export interface Quote {
@@ -40,7 +40,7 @@ export class TwainService {
4040
```
4141

4242
```bash
43-
npx ng g c twain
43+
npx ng g c twain/twain --inline-styles --inline-template --flat
4444
```
4545

4646
Update `twain.component.ts`
@@ -154,4 +154,181 @@ The spy is designed such that any call to `getQuote` receives an observable with
154154

155155
> **NOTE**: It is best to limit the usage of spies to only what is necessary for the test. Creating mocks or spies for more than what's necessary can be brittle. As the component and injectable evolves, the unrelated tests can fail because they no longer mock enough behaviors that would otherwise not affect the test.
156156
157-
Notice that we are not able to test the transition between '...' to the 'test quote' text. Why is that? Basically our real service behaves as async and the Spy is behaving as sync, no time for transition here
157+
Notice that we are not able to test the transition between '...' to the 'test quote' text. Why is that? Basically our real service behaves as async and the Spy is behaving as sync, no time for transition here.
158+
159+
### Async Behavior
160+
161+
Create `twain.component.async-utils.spec.ts`
162+
163+
```ts
164+
import {
165+
ComponentFixture,
166+
fakeAsync,
167+
TestBed,
168+
tick,
169+
} from '@angular/core/testing';
170+
171+
import { TwainComponent } from './twain.component';
172+
import { TwainService } from './twain.service';
173+
import { defer, of } from 'rxjs';
174+
import { provideHttpClient } from '@angular/common/http';
175+
import { asyncData, asyncError } from '../async-observable-helpers';
176+
177+
describe('TwainComponent', () => {
178+
let component: TwainComponent;
179+
let fixture: ComponentFixture<TwainComponent>;
180+
let testQuote: string;
181+
let getQuoteSpy: jasmine.Spy;
182+
183+
beforeEach(() => {
184+
TestBed.configureTestingModule({
185+
imports: [TwainComponent],
186+
providers: [TwainService, provideHttpClient()],
187+
});
188+
testQuote = 'Test Quote';
189+
190+
// Create a fake TwainService object with `getQuote()` spy
191+
const twainService = TestBed.inject(TwainService);
192+
getQuoteSpy = spyOn(twainService, 'getQuote').and.returnValue(
193+
of(testQuote)
194+
);
195+
196+
fixture = TestBed.createComponent(TwainComponent);
197+
component = fixture.componentInstance;
198+
});
199+
200+
describe('when test with synchronous observable', () => {
201+
it('should not show quote before OnInit', () => {
202+
const quoteEl = fixture.nativeElement.querySelector('.twain');
203+
const errorEl = fixture.nativeElement.querySelector('.error');
204+
expect(quoteEl.textContent).withContext('nothing displayed').toBe('');
205+
expect(errorEl).withContext('should not show error element').toBeNull();
206+
expect(getQuoteSpy.calls.any())
207+
.withContext('getQuote not yet called')
208+
.toBe(false);
209+
});
210+
211+
// The quote would not be immediately available if the service were truly async.
212+
it('should show quote after component initialized', async () => {
213+
fixture.detectChanges();
214+
const quoteEl = fixture.nativeElement.querySelector('.twain');
215+
await fixture.whenStable();
216+
217+
// sync spy result shows testQuote immediately after init
218+
expect(quoteEl.textContent).toBe(testQuote);
219+
expect(getQuoteSpy.calls.any()).withContext('getQuote called').toBe(true);
220+
});
221+
222+
// The error would not be immediately available if the service were truly async.
223+
// Use `fakeAsync` because the component error calls `setTimeout`
224+
it('should display error when TwainService fails', fakeAsync(() => {
225+
getQuoteSpy.and.returnValue(
226+
defer(() => {
227+
return new Promise((_, reject) => {
228+
setTimeout(() => {
229+
reject('TwainService test failure');
230+
});
231+
});
232+
})
233+
);
234+
235+
fixture.detectChanges(); // onInit();
236+
237+
tick(); // flush the setTimeout()
238+
239+
fixture.detectChanges(); // update errorMessage
240+
241+
const errorEl = fixture.nativeElement.querySelector('.error');
242+
const quoteEl = fixture.nativeElement.querySelector('.twain');
243+
244+
expect(errorEl.textContent)
245+
.withContext('should display error')
246+
.toMatch(/test failure/);
247+
expect(quoteEl.textContent)
248+
.withContext('should show placeholder')
249+
.toBe('...');
250+
}));
251+
});
252+
253+
describe('when test asynchronous observable', () => {
254+
beforeEach(() => {
255+
getQuoteSpy.and.returnValue(asyncData(testQuote));
256+
});
257+
258+
it('should not show quote before OnInit', () => {
259+
const quoteEl = fixture.nativeElement.querySelector('.twain');
260+
const errorEl = fixture.nativeElement.querySelector('.error');
261+
expect(quoteEl.textContent).withContext('nothing displayed').toBe('');
262+
expect(errorEl).withContext('should not show error element').toBeNull();
263+
expect(getQuoteSpy.calls.any())
264+
.withContext('getQuote not yet called')
265+
.toBe(false);
266+
});
267+
268+
it('should still not show quote after component initialized', () => {
269+
const quoteEl = fixture.nativeElement.querySelector('.twain');
270+
const errorEl = fixture.nativeElement.querySelector('.error');
271+
fixture.detectChanges();
272+
expect(quoteEl.textContent).withContext('nothing displayed').toBe('...');
273+
expect(errorEl).withContext('should not show error element').toBeNull();
274+
expect(getQuoteSpy.calls.any())
275+
.withContext('getQuote not yet called')
276+
.toBe(true);
277+
});
278+
279+
it('should show quote after getQuote (fakeAsync)', fakeAsync(() => {
280+
const quoteEl = fixture.nativeElement.querySelector('.twain');
281+
const errorEl = fixture.nativeElement.querySelector('.error');
282+
fixture.detectChanges(); // nOnInit
283+
expect(quoteEl.textContent).withContext('nothing displayed').toBe('...');
284+
285+
tick(); // flush the observable to get the quote
286+
fixture.detectChanges(); // update view
287+
288+
expect(quoteEl.textContent)
289+
.withContext('should show quote')
290+
.toBe(testQuote);
291+
expect(errorEl).withContext('should not show error element').toBeNull();
292+
}));
293+
294+
it('should show quote after getQuote (async)', async () => {
295+
const quoteEl = fixture.nativeElement.querySelector('.twain');
296+
const errorEl = fixture.nativeElement.querySelector('.error');
297+
fixture.detectChanges(); // ngOnInit
298+
expect(quoteEl.textContent).withContext('nothing displayed').toBe('...');
299+
300+
await fixture.whenStable();
301+
fixture.detectChanges(); // update view with quote
302+
expect(quoteEl.textContent)
303+
.withContext('should show quote')
304+
.toBe(testQuote);
305+
expect(errorEl).withContext('should not show error element').toBeNull();
306+
});
307+
308+
// TODO: Have a deeper look
309+
// TODO: Implement using 'async'
310+
it('should display error when TwainService fails', fakeAsync(() => {
311+
const quoteEl = fixture.nativeElement.querySelector('.twain');
312+
const errorEl = fixture.nativeElement.querySelector('.error');
313+
getQuoteSpy.and.returnValue(
314+
asyncError<string>('TwainService test failure')
315+
);
316+
317+
fixture.detectChanges(); // ngOnInit
318+
tick(); // flush the observable
319+
fixture.detectChanges();
320+
321+
expect(quoteEl.textContent).withContext('nothing displayed').toBe('...');
322+
console.log(errorEl)
323+
// expect(errorEl.textContent)
324+
// .withContext('should display error')
325+
// .toMatch(/test failure/);
326+
}));
327+
});
328+
});
329+
330+
```
331+
332+
```bash
333+
npx ng test --include app/twain/twain.component.async-utils.spec.ts
334+
```
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export interface Quote {
2+
id: number;
3+
quote: string;
4+
}
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import {
2+
ComponentFixture,
3+
fakeAsync,
4+
TestBed,
5+
tick,
6+
} from '@angular/core/testing';
7+
8+
import { TwainComponent } from './twain.component';
9+
import { TwainService } from './twain.service';
10+
import { defer, of } from 'rxjs';
11+
import { provideHttpClient } from '@angular/common/http';
12+
import { asyncData, asyncError } from '../async-observable-helpers';
13+
14+
describe('TwainComponent', () => {
15+
let component: TwainComponent;
16+
let fixture: ComponentFixture<TwainComponent>;
17+
let testQuote: string;
18+
let getQuoteSpy: jasmine.Spy;
19+
20+
beforeEach(() => {
21+
TestBed.configureTestingModule({
22+
imports: [TwainComponent],
23+
providers: [TwainService, provideHttpClient()],
24+
});
25+
testQuote = 'Test Quote';
26+
27+
// Create a fake TwainService object with `getQuote()` spy
28+
const twainService = TestBed.inject(TwainService);
29+
getQuoteSpy = spyOn(twainService, 'getQuote').and.returnValue(
30+
of(testQuote)
31+
);
32+
33+
fixture = TestBed.createComponent(TwainComponent);
34+
component = fixture.componentInstance;
35+
});
36+
37+
describe('when test with synchronous observable', () => {
38+
it('should not show quote before OnInit', () => {
39+
const quoteEl = fixture.nativeElement.querySelector('.twain');
40+
const errorEl = fixture.nativeElement.querySelector('.error');
41+
expect(quoteEl.textContent).withContext('nothing displayed').toBe('');
42+
expect(errorEl).withContext('should not show error element').toBeNull();
43+
expect(getQuoteSpy.calls.any())
44+
.withContext('getQuote not yet called')
45+
.toBe(false);
46+
});
47+
48+
// The quote would not be immediately available if the service were truly async.
49+
it('should show quote after component initialized', async () => {
50+
fixture.detectChanges();
51+
const quoteEl = fixture.nativeElement.querySelector('.twain');
52+
await fixture.whenStable();
53+
54+
// sync spy result shows testQuote immediately after init
55+
expect(quoteEl.textContent).toBe(testQuote);
56+
expect(getQuoteSpy.calls.any()).withContext('getQuote called').toBe(true);
57+
});
58+
59+
// The error would not be immediately available if the service were truly async.
60+
// Use `fakeAsync` because the component error calls `setTimeout`
61+
it('should display error when TwainService fails', fakeAsync(() => {
62+
getQuoteSpy.and.returnValue(
63+
defer(() => {
64+
return new Promise((_, reject) => {
65+
setTimeout(() => {
66+
reject('TwainService test failure');
67+
});
68+
});
69+
})
70+
);
71+
72+
fixture.detectChanges(); // onInit();
73+
74+
tick(); // flush the setTimeout()
75+
76+
fixture.detectChanges(); // update errorMessage
77+
78+
const errorEl = fixture.nativeElement.querySelector('.error');
79+
const quoteEl = fixture.nativeElement.querySelector('.twain');
80+
81+
expect(errorEl.textContent)
82+
.withContext('should display error')
83+
.toMatch(/test failure/);
84+
expect(quoteEl.textContent)
85+
.withContext('should show placeholder')
86+
.toBe('...');
87+
}));
88+
});
89+
90+
describe('when test asynchronous observable', () => {
91+
beforeEach(() => {
92+
getQuoteSpy.and.returnValue(asyncData(testQuote));
93+
});
94+
95+
it('should not show quote before OnInit', () => {
96+
const quoteEl = fixture.nativeElement.querySelector('.twain');
97+
const errorEl = fixture.nativeElement.querySelector('.error');
98+
expect(quoteEl.textContent).withContext('nothing displayed').toBe('');
99+
expect(errorEl).withContext('should not show error element').toBeNull();
100+
expect(getQuoteSpy.calls.any())
101+
.withContext('getQuote not yet called')
102+
.toBe(false);
103+
});
104+
105+
it('should still not show quote after component initialized', () => {
106+
const quoteEl = fixture.nativeElement.querySelector('.twain');
107+
const errorEl = fixture.nativeElement.querySelector('.error');
108+
fixture.detectChanges();
109+
expect(quoteEl.textContent).withContext('nothing displayed').toBe('...');
110+
expect(errorEl).withContext('should not show error element').toBeNull();
111+
expect(getQuoteSpy.calls.any())
112+
.withContext('getQuote not yet called')
113+
.toBe(true);
114+
});
115+
116+
it('should show quote after getQuote (fakeAsync)', fakeAsync(() => {
117+
const quoteEl = fixture.nativeElement.querySelector('.twain');
118+
const errorEl = fixture.nativeElement.querySelector('.error');
119+
fixture.detectChanges(); // nOnInit
120+
expect(quoteEl.textContent).withContext('nothing displayed').toBe('...');
121+
122+
tick(); // flush the observable to get the quote
123+
fixture.detectChanges(); // update view
124+
125+
expect(quoteEl.textContent)
126+
.withContext('should show quote')
127+
.toBe(testQuote);
128+
expect(errorEl).withContext('should not show error element').toBeNull();
129+
}));
130+
131+
it('should show quote after getQuote (async)', async () => {
132+
const quoteEl = fixture.nativeElement.querySelector('.twain');
133+
const errorEl = fixture.nativeElement.querySelector('.error');
134+
fixture.detectChanges(); // ngOnInit
135+
expect(quoteEl.textContent).withContext('nothing displayed').toBe('...');
136+
137+
await fixture.whenStable();
138+
fixture.detectChanges(); // update view with quote
139+
expect(quoteEl.textContent)
140+
.withContext('should show quote')
141+
.toBe(testQuote);
142+
expect(errorEl).withContext('should not show error element').toBeNull();
143+
});
144+
145+
// TODO: Have a deeper look
146+
// TODO: Implement using 'async'
147+
it('should display error when TwainService fails', fakeAsync(() => {
148+
const quoteEl = fixture.nativeElement.querySelector('.twain');
149+
const errorEl = fixture.nativeElement.querySelector('.error');
150+
getQuoteSpy.and.returnValue(
151+
asyncError<string>('TwainService test failure')
152+
);
153+
154+
fixture.detectChanges(); // ngOnInit
155+
tick(); // flush the observable
156+
fixture.detectChanges();
157+
158+
expect(quoteEl.textContent).withContext('nothing displayed').toBe('...');
159+
console.log(errorEl)
160+
// expect(errorEl.textContent)
161+
// .withContext('should display error')
162+
// .toMatch(/test failure/);
163+
}));
164+
});
165+
});
166+

0 commit comments

Comments
 (0)