iT邦幫忙

2022 iThome 鐵人賽

DAY 26
0
Modern Web

Angular TDD (Test-driven development ) 從0到1系列 第 26

Angular TDD 測試從0到1: Day 26 筆記回顧 Refelction (1)

  • 分享至 

  • xImage
  •  

今天來回顧這幾週學習的內容,用重點整理的方式,讓自己在短時間內抓到重點。

1. Component 測試

  • 簡單的元件測試會檢查 html 的值有沒有存在,以這個測試項目為例,取得 UserComponent ,再取得 debugElement 查詢 UI 有沒 data-test 的屬性
<div data-test="login">
  <h1>User logged in</h1>
  <p>User is: {{ user.name }}</p>
</div>
describe('Component: User', () => {
    beforeEach(() => {
        TestBed.configureTestingModule({
            declarations: [UserComponent]
        });
    });

    it('should show login class', () => {
        let fixture = TestBed.createComponent(UserComponent);
        let app = fixture.debugElement.componentInstance;
        expect(fixture.nativeElement.querySelector('[data-test="login"]')).toBeTruthy();
    });
})

2. Service 測試

  • HTML
<div *ngIf="isLoggedIn">
  <h1>User logged in</h1>
  <p>User is: {{ user.name }}</p>
</div>
<div *ngIf="!isLoggedIn">
  <h1>User not logged in</h1>
  <p>Please log in first</p>
</div>
  • Service
export class UserService {
  user = {
    name: 'Max'
  };
}
  • Typescript
@Component({
  selector: 'app-user',
  templateUrl: './user.component.html',
  styleUrls: ['./user.component.css'],
  providers: [UserService, DataService]
})
export class UserComponent implements OnInit {
  user: {name: string};
  isLoggedIn = false;
  data: string;

  constructor(private userService: UserService, private dataService: DataService) { }

  ngOnInit() {
    this.user = this.userService.user;
    this.dataService.getDetails().then((data: string) => this.data = data);
  }

}
  • Spec
    測試 Service 主要需要 inject service 進行調用
    (1) 測試 service 有沒有存在
    (2) 測試 value 有沒有被更新且與 service回的 value 一樣
    (3) 測試 假設登入成功的情境
    (4) 測試 假設登入失敗的情境
describe('Component: User', () => {
  beforeEach(() => {
    TestBed.configureTestingModule({
      declarations: [UserComponent]
    });
  });

  it('should create the app', () => {
    let fixture = TestBed.createComponent(UserComponent);
    let app = fixture.debugElement.componentInstance;
    expect(app).toBeTruthy();
  });

  it('should use the user name from the service', () => {
    let fixture = TestBed.createComponent(UserComponent);
    let app = fixture.debugElement.componentInstance;
    let userService = fixture.debugElement.injector.get(UserService);
    fixture.detectChanges();
    expect(userService.user.name).toEqual(app.user.name);
  });

  it('should display the user name if user is logged in', () => {
    let fixture = TestBed.createComponent(UserComponent);
    let app = fixture.debugElement.componentInstance;
    app.isLoggedIn = true;
    fixture.detectChanges();
    let compiled = fixture.debugElement.nativeElement;
    expect(compiled.querySelector('p').textContent).toContain(app.user.name);
  });

  it('shouldn\'t display the user name if user is not logged in', () => {
    let fixture = TestBed.createComponent(UserComponent);
    let app = fixture.debugElement.componentInstance;
    fixture.detectChanges();
    let compiled = fixture.debugElement.nativeElement;
    expect(compiled.querySelector('p').textContent).not.toContain(app.user.name);
  });

  
});

3. Async vs fakeAsync 測試

接續 Service 的 HTML, Typescript, 測試呼叫 dataService 同步異步的行為

  • dataService
export class DataService {
  getDetails() {
    const resultPromise = new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve('Data');
      }, 1500);
    });
    return resultPromise;
  }
}
  • Spec
    (1) 沒有使用 async 則會得到 undefined 結果
    (2) 使用 async 取得
    (3) 使用 fakeAsync, tick 時光機,不用等待時間到才能取值
  it('shouldn\'t fetch data successfully if not called asynchronously', () => {
    let fixture = TestBed.createComponent(UserComponent);
    let app = fixture.debugElement.componentInstance;
    let dataService = fixture.debugElement.injector.get(DataService);
    let spy = spyOn(dataService, 'getDetails')
      .and.returnValue(Promise.resolve('Data'));
    fixture.detectChanges();
    expect(app.data).toBe(undefined);
  });

  it('should fetch data successfully if called asynchronously', async(() => {
    let fixture = TestBed.createComponent(UserComponent);
    let app = fixture.debugElement.componentInstance;
    let dataService = fixture.debugElement.injector.get(DataService);
    let spy = spyOn(dataService, 'getDetails')
      .and.returnValue(Promise.resolve('Data'));
    fixture.detectChanges();
    fixture.whenStable().then(() => {
      expect(app.data).toBe('Data');
    });
  }));

  it('should fetch data successfully if called asynchronously', fakeAsync(() => {
    let fixture = TestBed.createComponent(UserComponent);
    let app = fixture.debugElement.componentInstance;
    let dataService = fixture.debugElement.injector.get(DataService);
    let spy = spyOn(dataService, 'getDetails')
      .and.returnValue(Promise.resolve('Data'));
    fixture.detectChanges();
    tick();
    expect(app.data).toBe('Data');

  }));

4. Pipe 測試

把TestBed... Angular測試模組抽掉,也可測試工具導向的功能,以這次的範例是寫一個 pipe 反轉字串

  • Typescript
import { Pipe } from "@angular/core";

@Pipe({
  name: 'reverse'
})
export class ReversePipe {
  transform(value: string) {
    return value.split("").reverse().join("");
  }
}
  • Spec
import { ReversePipe } from "./reverse.pipe";
describe('Pipe: ReversePipe', () => {
  it('should reverse the inputs', () => {
    let reversePipe = new ReversePipe();
    expect(reversePipe.transform('hello')).toEqual('olleh');
  });
});

5. Mock 資料設定 configureTestingModule

這邊開始是 Neil 課程的重點

將所有的 Mock 資料都寫到一隻 testing-utils.ts管理內容,在寫元件測試 spec 的時候,在 import configureTestingModule ,讓 TestBed 可以吃同一個設定

建立 Mock Component 方式

  1. 建立新 Component
  2. 元件名稱同原本元件名稱, i.e 原始元件稱「PlayComponent」, Mock 元件也用同名字
  3. Selector名稱同原本元件, i.e 原始元件選取器稱「app-play-component」, Mock 也同
  4. 不放入 Typescript
  5. 不放入 HTML
  6. 只保留 Inputs, Outputs
  7. 移除 Inputs 相關執行程式碼

建立 Mock Service 方式

  1. 建立新 Service
  2. 命不同的名稱, i.e Real Service名稱是 ApiService, Mock的話是 ApiServiceMock
  3. 不放入 Typescript
  4. 保留欄位和 Functions
  5. 函式回傳固定的值

使用細節回顧 Day 14 如何用 configureTestingModule 設定 Unit Test 環境

6. functions 測試

用 spyOn xx元件後的 'doSomething' 要對應實際元件上的函式名字,測試案例可以測試:
(1) 取得 doSomething 函式後,有沒有呼叫到 loadData()
(2) 不呼叫的話,要加 not

  it('should call a function during another function', () => {
    //Assign
    const spy = spyOn(component,'doSomething');

    //Act
    component.loadData();

    //Assert
    expect(spy).toHaveBeenCalledWith();
  });

  it('should not call a function during another function', () => {
    //Assign
    const spy = spyOn(component,'doSomething');

    //Act
    component.dontLoadData();

    //Assert
    expect(spy).not.toHaveBeenCalledWith();
  });

(3) 非必要,不測試 private function,但還是要測的話,就把 private function 名稱當作 key 執行私有函式呼叫測試

  it('should call a function during a private function', () => {
    //Assign
    const spy = spyOn(component,'insidePrivateFunction');

    //Act
    component['privateFunction']();

    //Assert
    expect(spy).toHaveBeenCalledWith();
  });

7. 變數更新 測試

總共有三個類型:

  1. 欄位變數更新
  2. function 回傳值更新
  3. getter, setter 的更新

細節可以回顧 Angular TDD 測試從0到1: Day 16 Typescript Unit Test(2)


上一篇
Angular TDD 測試從0到1: Day 25 補充 ng-Select
下一篇
Angular TDD 測試從0到1: Day 27 筆記回顧 Refelction (2)
系列文
Angular TDD (Test-driven development ) 從0到130
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言