Angular Testing 筆記 --- 6 Detect Change with DebugElement。

在頁面上經常會有某些 DOM 因為某些條件下消失或是出現,我們接下來就會以Login Component 在使用者登入後顯示"登出" 按鈕的 DOM 元件做測試。

由於我們之前並沒有實做登入後的畫面,所以我們首先修改 login.component.html 內容加入一個簡單的登入後畫面。

加入登入後畫面


我們使用 angular 提供 *ngIf 在 Html 作為控制顯示登入後與登入前的畫面,所以要稍微修改原本的 form 表單,並且加入登入後的畫面程式碼在form 表單parent 之下。
程式碼大致如下:

login.component.html
<section class="loginData col-xs-12 col-sm-12 col-md-12 col-lg-12" id="loginData" *ngIf="!isAuthenticated" > <form [formGroup]="loginForm" (ngSubmit)="onSubmit()" id="loginForm"> <div class="form-group input-group col-xs-12 col-sm-12 col-md-12 col-lg-12"> <input type="email" name="email" id="email" formControlName="email" class="form-control email" placeholder="電子郵件(用於帳號登入)"> </div> <div class="form-group input-group col-xs-12 col-sm-12 col-md-12 col-lg-12"> <input type="password" name="password" id="password" formControlName="password" class="form-control password" placeholder="密碼"> </div> <button type="submit" class="btn btn-primary col-xs-12 col-sm-12 col-md-12 col-lg-12" [disabled]="loginForm.invalid"> 登入 </button> </form> </section> <section class="logout col-xs-12 col-sm-12 col-md-12 col-lg-12" id="logout" *ngIf="isAuthenticated" > <h3>你好: <span id="userEmail">{{this.loginForm.value.email}}</span> </h3> <button class="btn btn-primary col-xs-12 col-sm-12 col-md-12 col-lg-12" (click)="logout()" id="logoutBtn">登出</button> </section> ....... 略 .......

修改程式邏輯部份

接下來是修改程式邏輯部份,簡易的判斷使用者是否有登入並且切換登入後的畫面。
這邊我用一個變數 isAuthenticated 來存放 service.isAuthenticated() 回傳登入是否成功的 boolean 資料,並且Html 的 *ngIf 也是透過這個變數來檢查該切換到哪個畫面。

login.component.ts
export class LoginComponent implements OnInit { public loginForm: FormGroup; public isAuthenticated = false; // 切換 登入前/登入後 頁面使用 constructor( private fb: FormBuilder, private demo: DemoService ) { } ..... 略 .......

除了新增 isAuthenticated 變數外,還要修改 onSubmit() 這個當使用者按下登入的處理 function。

login.component.ts
onSubmit() { localStorage.setItem('token', JSON.stringify({ email: this.loginForm.value.email})); if (this.demo.isAuthenticated()) { this.isAuthenticated = true; } else { this.isAuthenticated = false; } }

接著加入登出的處理。

login.component.ts
logout() { localStorage.removeItem('token'); this.isAuthenticated = false; }

這樣子基本上就可以有登入後的簡易顯示使用者的email 並且還有一個『登出』的按鈕了。

登入前:


登入後:


撰寫測試

接來回到 login.component.spec.ts 開始著手撰寫測試內容。
我要測試的項目以以下兩種:

  • 登入前要有登入按鈕、登入按鈕文字為"登入"、登出按鈕是消失的。
  • 登入後要有登出按鈕、要顯示使用者的email、登入按鈕是消失的。
大概就是這兩項。


一樣我們在 login.component.spec.ts 額外寫一個 describe() 來為這次的 detect change 做撰寫測試內容。以下是基本的測試環境建立
// detect change describe('LoginComponent login', () => { let component: LoginComponent; let fixture: ComponentFixture<LoginComponent>; let service: DemoService; let el: DebugElement; // 要使用它來 query DOM beforeEach(() => { TestBed.configureTestingModule({ declarations: [ LoginComponent ], imports: [ ReactiveFormsModule ], providers: [ DemoService ] }); fixture = TestBed.createComponent(LoginComponent); component = fixture.componentInstance; service = TestBed.get(DemoService); el = fixture.debugElement; // debugElement fixture.detectChanges(); }); });

這邊我們注意到一個新的 Type 名稱: DebugElement ,這是一個在 @angular/core 裡提供物件。所以我們要 import  DebugElement 進來。

// for testing change detection. import { DebugElement } from '@angular/core';

DebugElement :

這是Angular 提供用來 query 測試時產生出來的 DOM 或是 頁面上的 elements,熟知 Angular 的人知道我們通常會用 fixture.nativeElement 這個物件來 query 我們 app 前端的 DOM 與 elements。實際上在測試的時也是可以用原本的 fixture nativeElement 去 query 前端的 DOM 並且用他做一些判斷存不存在或是修改文字、css....等 功能,但是為什麼 Angular 還要提供 DebugElement呢?主要是因為原本的 nativeElement 是依靠運行時的環境!但是你測試的時候可能是在沒有瀏覽器的 server 端做測試( 持續交付的流程下會採用 Jenkins、Travis或是CircleCI )這個時候就 沒有真的 DOM 可以使用 HTML api 去 query 

所以 Angular 依靠 DebugElement 抽象化物件來取代建立真正的 HTML elements tree 。Angular 建立 DebugElement tree 來包裝各個原生平台的 elements tree 並且安全支持在各種平台上。

接著我們來看看第一項測試的程式碼要如何寫。
it('Should get login button and without logout button \ before authenticated user.', () => { const loginBTN = el.nativeElement.querySelector('button[type="submit"]'); const logoutBTN = el.nativeElement.querySelector('#logoutBtn'); // 有 login 按鈕 expect(loginBTN).toBeTruthy(); // 登入按鈕文字為 "登入" expect(loginBTN.textContent.trim()).toBe('登入'); // 沒有登出按鈕 expect(logoutBTN).toBeNull(); });

這邊我們可以看到 DebugElement 的使用上也跟我們在使用 Angular 原本的 nativeElement 用法一樣,可以使用 querySelector() 來 query 我們要的 DOM 並透過回傳的物件作為我們測試的用途。

再來是第二項的測試,這邊必須用到上一章節提到的 fixture.detectChanges() 作為我們執行 submit 後要求 Angular 去做 data binding 跟畫面上的變化。
此外我們使用的是 Reactive Form 的表單,所以在手動給予 Email 跟 Password 的時候記得要用 patchValue() 的方式。
it('should show user email, logout button and \ without login button after authenticated.', () => { const testEmail = 'clover@example.com'; const testPass = 'abcd1234'; spyOn(service, 'isAuthenticated').and.returnValue(true); component.loginForm.patchValue({email: testEmail, password: testPass}); component.onSubmit(); fixture.detectChanges(); // 重要! const userInfo = el.nativeElement.querySelector('#userEmail'); const loginBTN = el.nativeElement.querySelector('button[type="submit"]'); const logoutBTN = el.nativeElement.querySelector('#logoutBtn'); // 顯示使用者的 email expect(userInfo.textContent.trim()).toBe(testEmail); // 有登出按鈕 expect(logoutBTN).toBeTruthy(); // 沒有登入按鈕 expect(loginBTN).toBeNull(); });

這樣就完成了我們在使用者登入後要能看見使用者的 Email 登出按鈕登入按鈕必須消失的畫面。

------- 待續


REF:

留言