Tối ưu mock trong Angular Unit Test

Tối ưu mock trong Angular Unit Test

Một số cách để tái sử dụng / giảm thời gian tạo mock trong unit test khi làm việc với Angular

Tái sử dụng mock cơ bản nhất

Mock về bản chất cũng là các đơn vị tổ chức bình thường của framework, vì thế chúng ta cũng có thể / nên / cần tái sử dụng như các thành phần thực tế.

Ví dụ chúng ta có file spec:

// banner.component.spec.ts
import { async, ComponentFixture, TestBed } from "@angular/core/testing";
import { Component } from "@angular/core";
import { BannerComponent } from "./banner.component";

@Component(selector: 'app-popup')
class MockPopupComponent {
}

describe("BannerComponent", () => {
  let component: BannerComponent;
  let fixture: ComponentFixture<BannerComponent>;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [
        BannerComponent,
        MockPopupComponent
      ],
    }).compileComponents();
  }));

// ...
});

Chúng ta có thể di chuyển mock ra một files ví dụ ut-helpers.ts và export như các thành phần bình thường:

// ut-helper.ts
import { Component } from "@angular/core";

@Component(selector: 'app-popup')
export class MockPopupComponent {
}

Sau đó import vào file specs thôi.

// banner.component.spec.ts
import { async, ComponentFixture, TestBed } from "@angular/core/testing";
import { BannerComponent } from "./banner.component";
import { MockPopupComponent } from "./ut-helpers";

describe("BannerComponent", () => {
  let component: BannerComponent;
  let fixture: ComponentFixture<BannerComponent>;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [BannerComponent, MockPopupComponent],
    }).compileComponents();
  }));

// ...
});

Dùng function để tạo ra mock

Nếu bạn cảm thấy việc mock từng class vẫn hơi mệt thì có thể tham khảo cách sau.

Chúng ta tạo một function như sau:

// ut-helper.ts
import { Component } from "@angular/core";

export const mockComponent = (selector: string) => {
  return Component({ selector, template: '' })(class Mock {});
};

Function mockComponent() nhận vào duy nhất một parametter là selector name, sau đó trả tra class với tên bất kì (ở ví dụ trên là Mock) đã được annotate với decorator Component().

Và để sử dụng, chỉ cần gọi function trên với selector cần mock:

// banner.component.spec.ts
import { async, ComponentFixture, TestBed } from "@angular/core/testing";
import { BannerComponent } from "./banner.component";
import { mockComponent } from "./ut-helpers";

describe("BannerComponent", () => {
  let component: BannerComponent;
  let fixture: ComponentFixture<BannerComponent>;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [BannerComponent, mockComponent('app-popup')],
    }).compileComponents();
  }));

// ...
});

Và tất nhiên cũng không nên sử dụng mockComponent() trực tiếp, vì chúng ta sẽ cần phải nhớ selector. Chúng ta có thể khởi tạo sẵn mock trong helpers:

// ut-helper.ts
import { Component } from "@angular/core";

export const mockComponent = (selector: string) => {
  return Component({ selector, template: '' })(class Mock {});
};

export const MockAppPopupComponent = mockComponent('app-popup');

Và import dễ dàng nhờ vào auto-suggestion của editor/IDE:

// banner.component.spec.ts
import { async, ComponentFixture, TestBed } from "@angular/core/testing";
import { BannerComponent } from "./banner.component";
import { MockAppPopupComponent } from "./ut-helpers";

describe("BannerComponent", () => {
  let component: BannerComponent;
  let fixture: ComponentFixture<BannerComponent>;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [BannerComponent, MockAppPopupComponent],
    }).compileComponents();
  }));

// ...
});

Nâng cấp mockComponent() để tạo mock với input

Chắc chắn chúng ta sẽ gặp trường hợp cần mock function chứa decorator @Input(). Giả sử popup.component.ts:

// `popup.component.ts`
import { Component } from "@angular/core";

@Component(selector: 'app-popup')
export class PopupComponent {
  @Input() title;
  @Input() content;
}

Lúc này giao diện của hàm mockComponent sẽ cần như thế này:

mockComponent('app-popup', ['title', 'content']);

Chúng ta sẽ điều chỉnh mockComponent() một chút như sau:

// ut-helper.ts
export const mockComponent = (selector: string, inputProps: string[] = []) => {
  const MockComponent = class Mock {};
  inputProps.forEach((prop) => Input()(MockComponent.prototype, prop));
  return Component({ selector, template: "" })(MockComponent);
};

Chúng ta có thể chạm tới từng thuộc tính của mock class thông qua prototype, sau đó chú thích với decorator Input().

Các thành phần còn lại

Các bạn cũng có thể tạo các function trả về mock cho các đơn vị tổ chức khác trong Angular như Directive, Pipe, ... một cách tương tự.

export const mockDirective = (selector: string, inputProps: string[] = []) => {
  const MockDirective = class Mock {};
  inputProps.forEach((prop) => Input()(MockDirective.prototype, prop));
  return Directive({ selector })(MockDirective);
};

export const mockPipe = (name: string): Pipe => {
  return <any>Pipe({ name })(
    class MockPipe {
      transform(input) {
        return input;
      }
    }
  );
};

Previous Post