iT邦幫忙

2022 iThome 鐵人賽

DAY 27
0
Modern Web

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

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

  • 分享至 

  • xImage
  •  

今天接續來回顧學習的內容,列出重點關鍵字,在回顧的時候有記憶點。

1. Observables, Subscription 測試

  • spyOn 元件函式
  • 使用 BehaviorSubject ,帶入數字
  • 將 ApiService inject 到測試檔案
  • 用 asObservable() 將 getDataSubject 轉換成單純 Observable,將 value emit 出去
  • 因為在 ngOnInit 已經使用 getData(),元件狀態已更新,所以再次呼叫初始重新訂閱 getData
  • 用 next 送出新值
  it('should call handleData when recieving new data', () => {
    //Assign
    const spy = spyOn(component,'handleData');
    const getDataSubject = new BehaviorSubject<number>(3);
    const apiService = TestBed.inject(ApiService);
    spyOn(apiService,'getData').and.returnValue(getDataSubject.asObservable());

    //Act
    component.ngOnInit(); // Subscribe again since the function was just changed.
    getDataSubject.next(5); // Emit a new value

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

2. Navigation, Local Storage 測試

Navigation

用來測試 「Route」,常用在測試登入成功、未登入

  it('should navigate to the home page if the user is logged in', fakeAsync(() => {
    //Assign
    const router = TestBed.inject(Router);
    const authorisationService = TestBed.inject(AuthorisationService);
    spyOn(authorisationService, 'canActivate').and.returnValue(true); // The user is allowed to navigate

    //Act
    router.navigate(['home']); // try navigating home
    tick(); // Skip ahead to when navigation is complete

    //Assert
    expect(router.url).toBe('/home');
  }));

Local Storage

會需要用到 javascript prototype 取得 localstroage 的資料

  it('should load data from local storage', () => {
    //Assign
    const data = "value";
    const spy = spyOn(Storage.prototype, 'getItem').and.returnValue(JSON.stringify(data));
    // const spy = spyOn(localStorage, 'setItem').and.returnValue(JSON.stringify(data)); //doesn't work in Firefox

    //Act
    component.loadData();

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

3. Detector Change 測試

changeDetecor,可以偵測 UI 上任何更新,但是要測試 change detector 無法 inject to TestBed,需要建立新元件,在透過該元件呼叫 constructor service

4. 取得元素的方法

  1. query: 回傳查詢到的第一個元素,返回的是 debug element,
  2. querySelector: 回傳查詢到的第一個元素,返回 selector functions
  • 查詢方式
query(By.css('#title'))
querySelector('#title')
  1. queryAll: 回傳「所有」符合條件的 element
  2. querySelectorAll: 回傳「所有」符合條件的 element
  • 查詢方式
queryAll(By.css('.title'))
querySelectorAll('.title')

5. ngIf, ngFor, ngClass, ng-Select

ngIf

設定條件為true 或 false,在 query 其元素,根據情境預期成功或失敗

  it('should show the title when showTitle is true', () => {
    //Assign
    component.showTitle = true;

    //Act
    componentFixture.detectChanges();

    //Assert
    const element = componentFixture.debugElement.query(By.css('#title1'));
    expect(element).toBeTruthy();
  });

  it('shouldnt show the title when showTitle is false', () => {
    //Assign
    component.showTitle = false;

    //Act
    componentFixture.detectChanges();

    //Assert
    const element = componentFixture.debugElement.query(By.css('#title1'));
    expect(element).toBeFalsy();
  });

ngFor

設定陣列元素,在 query 其元素,根據情境預期成功或失敗

describe('queryAll', () => {
    it('should show the correct number of elements', () => {
      //Assign
      component.messages = ['a','b','c'];
      
      //Act
      componentFixture.detectChanges();

      //Assert
      const elements = componentFixture.debugElement.queryAll(By.css('.row'));
      expect(elements.length).toEqual(3);
    });
  });

ngClass

方法類似 ngIf,設定條件為true 或 false,在 query 其元素,根據情境預期成功或失敗

describe('querySelector', () => {
    it('should apply the class active when activeTitle is true', () => {
      //Assign
      component.activeTitle = true;
      
      //Act
      componentFixture.detectChanges();
      
      //Assert
      const element = componentFixture.debugElement.nativeElement.querySelector('#title1');
      expect(element.classList).toContain('active');
    });
  })

ng-select

重點在使用 key code trigger dropdown 元件

  describe('query', () => {
    it('should call the function selectOption when the user selects a new value from the dropdown', () => {
      //Assign
      component.options = ['option 1','option 2','option 3'];
      component.option = 'option 1';
      componentFixture.detectChanges();
      const spy = spyOn(component, 'selectOption');
      const element = componentFixture.debugElement.query(By.css('#optionDropdown'));
  
      //Act
      triggerKeyDownEvent(element, 32); // space to open the dropdown
      triggerKeyDownEvent(element, 40); // down arrow
      triggerKeyDownEvent(element, 13); // enter to select second option and close the dropdown
      
      //Assert
      expect(spy).toHaveBeenCalledWith('option 2');
    });
  })

  function triggerKeyDownEvent(element: DebugElement, which: number, key = ''): void {
    element.triggerEventHandler('keydown', {
        which: which,
        key: key,
        preventDefault: () => { },
    });
  }

7. @Input, @Output

Output

output event 會將其值 emit 回 parent component,所以要 new Event 物件帶入 output event name outputData,在 query 其元素,根據情境預期成功或失敗

  • HTML
<app-child-component id="title" (outputData)="updateData($event)"></app-child-component>
  • Spec
describe('query', () => {
    it('should call the correct function on child component output event', () => {
      //Assign
      const spy = spyOn(component,'updateData');
      const event = new Event('outputData');

      //Act
      const element = componentFixture.debugElement.query(By.css('#title'));
      element.nativeElement.dispatchEvent(event);
  
      //Assert
      expect(spy).toHaveBeenCalledWith(event);
    });
  })

Input

取得 input value,在 query 其元素,根據情境預期成功或失敗

  • HTML
<app-child-component id="title" [inputData]="title"></app-child-component>
<button id="button" type="submit" [disabled]="isDisabled">Submit</button>
  • Spec
 describe('query', () => {
    it('should pass down the correct data to app-child-component', () => {
      //Assign
      component.title = "a title";
      
      //Act
      componentFixture.detectChanges();
  
      //Assert
      const element = componentFixture.debugElement.query(By.css('#title'));
      expect(element.componentInstance.inputData).toEqual(component.title);
    });
  })

今日小結

來到重點回顧的尾聲,紀錄實做面遇到各種測試項目會用到的「關鍵字」和需要留意的概念,在學習的過程中意識到「實做」和「研究」有著不同的個性,雖說描述起來有點抽象,但前者偏重於「經驗應用」,後者著重在「學者精神」,有著「10萬個為什麼的精神」確認每個用法,如何用?用在哪?

「實做應用」會需要一點「學者精神」才可以早點完成作業,但不論或早或晚,都是過程體驗中必經之路,也許這次的學習之旅認識到單元測試 (Unit Test) 支線,下個時間點會見識軟體開發其他面向。


上一篇
Angular TDD 測試從0到1: Day 26 筆記回顧 Refelction (1)
下一篇
Angular TDD 測試從0到1: Day 28 聊聊「時間管理」
系列文
Angular TDD (Test-driven development ) 從0到130
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言