Refs
Why unit tests?
1// cd to app folder
2ng test // test whole app
1// Test only one file (jest.config.j s)
2testMatch: ['**/web-integration-form.component.spec.ts'],
Understanding concepts
1// First and nothing
2it('should create the app', () => {
3 let fixture = TestBed.createComponent(UserComponent); // create a store new component in "fixture"
4 let app = fixture.debugElement.componentInstance; // need "debugElement" if without browsers
5 expect(app).toBeTruthy();
6
7 // example of inspect a property "title" of component
8 // (what comes in the class)
9 expect(app.title)...
10});
⚠️ This is my personal note, it may be not a perfect (or “right”) way!
1// LexiconSessionService
2// 👇 We want to mock this one!
3export class LexiconSessionService {
4 get lexiconStatusObj(): SomeType {
5 return {
6 // something
7 };
8 }
9}
1beforeEach(() => {
2 TestBed.configureTestingModule({
3 declarations: [LexiconComponent], // wanna test
4 providers: [
5 // Despite we use `useValue` here
6 { provide: LexiconSessionService, useValue: mockLexiconSessionService },
7 ]
8 }).compileComponents();
9});
10
11beforeEach(() => {
12 fixture = TestBed.createComponent(LexiconComponent);
13 component = fixture.componentInstance;
14 fixture.detectChanges();
15});
16
17describe('Test something', () => {
18 it('should do something', () => {
19 const _lexicon = TestBed.inject(LexiconSessionService);
20 // 🤩 Key point!!!
21 Object.defineProperty(_lexicon, 'lexiconStatusObj', {
22 value: fakeLexiconStatusObj, // fake return value!!!
23 writable: true
24 });
25 // other tests which call _lexicon.lexiconStatusObj
26 });
27});
👉 Good to read: Towards Better Testing In Angular. Part 1 — Mocking Child Components | by Abdul Wahab Rafehi | Medium
💡 Recommended in the Angular Testing Guide, is to manually mock (or stub) components
If cannot find child component errors? (source)
declarations: [ChildComponent]
⇒ BUT it's not isolated, it depends onChildComponent
!- Cons: If in
ChildComponent
, we add some dependency ⇒ not working
- Use
schemas: NO_ERRORS_SCHEMA
to tell the compiler to ignore any elements or attributes it isn’t familiar with - Cons: if inside parent, we mispelling
<child></chidl>
or any wong things with child components ⇒ it still works but not really!! ⇒ There will be something wrong until we actually run the app. Difficult to test@Input
and@Output
.
- Mock / Stub child component ⇒ ensure to have the same selector. ⇒ có thể tạo 1 cái "mock" class của
ChildComponent
bằng 1 file.stub
- Cons: it's verbose + shortcoming if there are many inputs and outputs. It requires us to remember to change the stub each time we change the real component
- Using
ng-mock
1// Wanna test user-item.component.spec.ts
2// In its parent
3<app-user-item
4 *ngFor="let conv of convList; let i=index; trackBy: trackByConvId"
5 [conversation]="conv"
6></app-user-item>
Refs:
- Test parent and child components when passing data with input binding ⇒ defind in parent a simple child component (like the real one) + mocking a hero input if you wanna test child component.
- (read later) https://stackoverflow.com/questions/40541123/how-to-unit-test-if-an-angular-2-component-contains-another-component
1// Compiled html contains?
2it('should display the user name if user is logged in', () => {
3 let fixture = TestBed.createComponent(UserComponent);
4 let app = fixture.debugElement.componentInstance;
5 app.isLoggedIn = true;
6 fixture.detectChanges();
7 let compiled = fixture.debugElement.nativeElement;
8 expect(compiled.querySelector('p').textContent).toContain(app.user.name);
9 // not contains?
10 expect(compiled.querySelector('p').textContent).not.toContain(app.user.name);
11});
If a component depends on many services, you need to create mocks of these services in which the function/properties you need to use in your component.
👉 Official doc (search for "The following WelcomeComponent depends on the UserService to know the name of the user to greet.")
👉🏻 Official doc: Angular - Testing services
1// An example but not very good
2it('should use the user name from the service', () => {
3 let fixture = TestBed.createComponent(UserComponent);
4 let app = fixture.debugElement.componentInstance;
5 let userService = fixture.debugElement.injector.get(UserService);
6 fixture.detectChanges(); // <=======
7 // We need this line because it doesn't have the same state as init when we inject the service
8 // Without this, it's undefined at the beginning
9 expect(userService.user.name).toEqual(app.user.name);
10});
1// From: https://www.digitalocean.com/community/tutorials/testing-angular-with-jasmine-and-karma-part-1
2
3import { TestBed } from '@angular/core/testing';
4import { UsersService } from './users.service';
5
6describe('UsersService', () => {
7 let usersService: UsersService; // Add this
8
9 beforeEach(() => {
10 TestBed.configureTestingModule({
11 providers: [UsersService]
12 });
13 usersService = TestBed.get(UsersService); // Add this
14 });
15
16 it('should be created', () => { // Remove inject()
17 expect(usersService).toBeTruthy();
18 });
19});
1import { FormGroup, ReactiveFormsModule } from '@angular/forms';
👇🏻 Source.
1TestBed.configureTestingModule({
2 // imports: [FormsModule] // import the FormsModule if you want ngModel to be working inside the test
3 schemas: [NO_ERRORS_SCHEMA] // remove the FormsModule import and use that schema to only shallow test your component. Please refer to the official document for more information.
4})
1it('should create a FormGroup comprised of FormControls', () => {
2 component.ngOnInit();
3 expect(component.formGroup instanceof FormGroup).toBe(true);
4});
1<dynamic-form [questions]="myQuestions"></dynamic-form>
The input
questions
(in child) takes value from myQuestions
(in parent) ⇒ Life cycle hooks: check data-bound input > ngOnInit > other components.👉🏻 Form & submit testing example (the same source as above): returned payload, setValue and submit,... + codes
1it('should create a FormControl for each question', () => {
2 component.questions = [
3 {
4 controlType: 'text',
5 id: 'first',
6 label: 'My First',
7 required: false
8 },
9 {
10 controlType: 'text',
11 id: 'second',
12 label: 'Second!',
13 required: true
14 }
15 ];
16 component.ngOnInit();
17
18
19 expect(Object.keys(component.formGroup.controls)).toEqual([
20 'first', 'second'
21 ]);
22});
(src) A spy allows us to “spy” on a function and track attributes about it such as whether or not it was called, how many times it was called, and with which arguments it was called.
1// Async tasks
2
3it('shouldn\'t fetch data successfully if not called asynchronously', () => {
4 let fixture = TestBed.createComponent(UserComponent);
5 let app = fixture.debugElement.componentInstance;
6 let dataService = fixture.debugElement.injector.get(DataService);
7 let spy = spyOn(dataService, 'getDetails')
8 .and.returnValue(Promise.resolve('Data'));
9 fixture.detectChanges();
10 expect(app.data).toBe(undefined);
11});
12
13it('should fetch data successfully if called asynchronously', async(() => {
14 let fixture = TestBed.createComponent(UserComponent);
15 let app = fixture.debugElement.componentInstance;
16 let dataService = fixture.debugElement.injector.get(DataService);
17 let spy = spyOn(dataService, 'getDetails')
18 .and.returnValue(Promise.resolve('Data'));
19 fixture.detectChanges();
20 fixture.whenStable().then(() => {
21 expect(app.data).toBe('Data');
22 });
23}));
1// Alternative with tick
2// ie. from "async...whenStable()" => "fakeAsync...tick()"
3it('should fetch data successfully if called asynchronously', fakeAsync(() => {
4 let fixture = TestBed.createComponent(UserComponent);
5 let app = fixture.debugElement.componentInstance;
6 let dataService = fixture.debugElement.injector.get(DataService);
7 let spy = spyOn(dataService, 'getDetails')
8 .and.returnValue(Promise.resolve('Data'));
9 fixture.detectChanges();
10 tick(); // resolve it immediately, don't wanna wait
11 expect(app.data).toBe('Data');
12}));
1// DataService
2export class DataService {
3 getDetails() {
4 const resultPromise = new Promise((resolve, reject) => {
5 setTimeout(() => {
6 resolve('Data');
7 }, 1500);
8 });
9 return resultPromise;
10 }
11}
1// UserComponent
2this.dataService.getDetails().then((data: string) => this.data = data);
✳️ NullInjectorError: No provider for HttpClient!
1beforeEach(async(() => {
2 import { HttpClientTestingModule } from '@angular/common/http/testing';
3 TestBed.configureTestingModule({
4 imports: [HttpClientTestingModule]
5 }).compileComponents();
6}));
✳️ NullInjectorError: No provider for AngularFireDatabase!
Check thử xem nó xuất hiện lỗi từ service/component/module nào, ví dụ
Suy ra: chỉ cần mock cái
BotSessionService
là ok!1TestBed.configureTestingModule({
2 declarations: [LexiconComponent],
3 providers: [
4 { provide: BotSessionService, useClass: BotSessionServiceStub },
5 // ToasterService
6 ]
7}).compileComponents();
✳️ NullInjectorError: No provider for Router!
→ Just use a fake
Router
✳️ NullInjectorError (in general)
We can use
1{ provide: AnyService, useValue: jest.fn() },