🧪

Angular Unit Testing

Anh-Thi Dinh
draft

Refs

Why unit tests?

CLI

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

Is app properly created?

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});

TestBed

Different providers

Mocking

Mock a “read only” property of a class (”get”)

⚠️ 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});

Test a component

Child component with input

💡 Recommended in the Angular Testing Guide, is to manually mock (or stub) components
If cannot find child component errors? (source)
  1. declarations: [ChildComponent] ⇒ BUT it's not isolated, it depends on ChildComponent!
    1. Cons: If in ChildComponent, we add some dependency ⇒ not working
  1. Use schemas: NO_ERRORS_SCHEMA to tell the compiler to ignore any elements or attributes it isn’t familiar with
    1. 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.
  1. 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
    1. 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
  1. 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:
  1. 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.
  1. (read later) https://stackoverflow.com/questions/40541123/how-to-unit-test-if-an-angular-2-component-contains-another-component

nativeElement contains?

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});

Test a Service

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});

Reactive From tests

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})
👇🏻 Source + codes
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});

Async

(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);

Troubleshooting

✳️  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() },
 
Loading comments...