# 初始單元測(cè)試
原型永遠(yuǎn)是第一位的!本節(jié)中我們快速的對(duì)登錄組件進(jìn)行初始化,并嘗試使用代碼來測(cè)試登錄按鈕的綁定狀態(tài)。
## 初始化
打開shell并進(jìn)行`src/app`文件夾,使用`ng g c login`初始化登錄組件:
```bash
panjiedeMacBook-Pro:app panjie$ ng g c login
CREATE src/app/login/login.component.css (0 bytes)
CREATE src/app/login/login.component.html (20 bytes)
CREATE src/app/login/login.component.spec.ts (619 bytes)
CREATE src/app/login/login.component.ts (271 bytes)
UPDATE src/app/app.module.ts (806 bytes)
```
然后,我們參考[bootstrap示例登錄界面](https://getbootstrap.com/docs/5.0/forms/overview/)對(duì)原型初始化如下:
```html
<form class="container-sm">
<div class="mb-3">
<label for="username" class="form-label">用戶名</label>
<input type="text" class="form-control" id="username" aria-describedby="usernameHelp">
<div id="usernameHelp" class="form-text">我們不會(huì)分享你的登錄信息</div>
</div>
<div class="mb-3">
<label for="exampleInputPassword1" class="form-label">密碼</label>
<input type="password" class="form-control" id="exampleInputPassword1">
</div>
<button type="submit" class="btn btn-primary">登錄</button>
</form>
```
## 屬性與方法
```typescript
+++ b/first-app/src/app/login/login.component.ts
@@ -1,4 +1,4 @@
-import { Component, OnInit } from '@angular/core';
+import {Component, OnInit} from '@angular/core';
@Component({
selector: 'app-login',
@@ -6,10 +6,18 @@ import { Component, OnInit } from '@angular/core';
styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnInit {
+ teacher = {} as {
+ username: string,
+ password: string
+ };
- constructor() { }
+ constructor() {
+ }
ngOnInit(): void {
}
+ onSubmit(): void {
+ console.log('點(diǎn)擊了登錄按鈕');
+ }
}
```
V層綁定:
```html
+++ b/first-app/src/app/login/login.component.html
@@ -1,12 +1,14 @@
-<form class="container-sm">
+<form class="container-sm" (ngSubmit)="onSubmit()">
<div class="mb-3">
<label for="username" class="form-label">用戶名</label>
- <input type="text" class="form-control" id="username" aria-describedby="usernameHelp">
+ <input type="text" class="form-control" id="username" aria-describedby="usernameHelp"
+ [(ngModel)]="teacher.username" name="username">
<div id="usernameHelp" class="form-text">我們不會(huì)分享你的登錄信息</div>
</div>
<div class="mb-3">
<label for="exampleInputPassword1" class="form-label">密碼</label>
- <input type="password" class="form-control" id="exampleInputPassword1">
+ <input type="password" class="form-control" id="exampleInputPassword1"
+ [(ngModel)]="teacher.password" name="password">
</div>
<button type="submit" class="btn btn-primary">登錄</button>
</form>
```
## 測(cè)試
軟件工程相比于交通、土木工程等其它實(shí)體工程有著先天的優(yōu)勢(shì) ---- 幾乎可以忽略不計(jì)的測(cè)試成本。所以我們?cè)陂_發(fā)中,要摒棄**我認(rèn)為**、**應(yīng)該**等字眼,當(dāng)不太清楚自己的代碼是否正確運(yùn)行時(shí),最簡(jiǎn)單的方法就是測(cè)試一下。
加入測(cè)試代碼:
```html
+++ b/first-app/src/app/login/login.component.html
@@ -1,3 +1,4 @@
+{{teacher | json}}
<form class="container-sm" (ngSubmit)="onSubmit()">
<div class="mb-3">
<label for="username" class="form-label">用戶名</label>
```
在單元測(cè)試中:
1. 加入FormModule以支持`[(ngModel)]`
2. 啟用自動(dòng)檢測(cè)變更以便捷觀察數(shù)據(jù)的時(shí)實(shí)變更情況
```typescript
+++ b/first-app/src/app/login/login.component.spec.ts
@@ -1,6 +1,7 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {LoginComponent} from './login.component';
+import {FormsModule} from '@angular/forms';
fdescribe('LoginComponent', () => {
let component: LoginComponent;
@@ -8,7 +9,10 @@ fdescribe('LoginComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
- declarations: [LoginComponent]
+ declarations: [LoginComponent],
+ imports: [
+ FormsModule
+ ]
})
.compileComponents();
});
@@ -21,5 +25,6 @@ fdescribe('LoginComponent', () => {
it('should create', () => {
expect(component).toBeTruthy();
+ fixture.autoDetectChanges();
});
});
```
使用`ng t `快速啟動(dòng)組件:

- 測(cè)試一:數(shù)據(jù)綁定成功
- 測(cè)試二:按鈕綁定生效
## 單元測(cè)試
在一般的項(xiàng)目中,用人眼來對(duì)代碼進(jìn)行測(cè)試是不可靠的。它的不可靠主要體現(xiàn)在兩個(gè)方面:
* 隨著組件功能的增多,人眼同時(shí)檢測(cè)多種測(cè)試信息,免不了顧此失彼。開發(fā)了一個(gè)新功能同時(shí),也可能破壞了一個(gè)原有的正常的功能。
* 由于**應(yīng)該看什么**并沒有形成文檔。和合作開發(fā)中,張三在接手了李四的組件后,完全不知道應(yīng)該看什么,哪是對(duì)的,哪又是錯(cuò)的。
鑒于此,我們可以采用使用代碼來測(cè)試代碼的方法,由于這種方法是針對(duì)功能點(diǎn)的某個(gè)小的功能單元進(jìn)行測(cè)試,所以又被稱為**單元測(cè)試**,英文關(guān)鍵字為**Unit Test**。
在此,我們簡(jiǎn)單介紹下如何使用單元測(cè)試來驗(yàn)證登錄按鈕與C層的`onSubmit`方法是否綁定成功。
### 流程
此測(cè)試在思想上大概分為以下幾步:
1. 獲取V層中的登錄按鈕
2. 使用代碼來點(diǎn)擊這個(gè)按鈕
3. 查看C層中的方法是否被觸發(fā)
接下來,我們分別介紹上述步驟的實(shí)現(xiàn)方法。
### 獲取V層的登錄按鈕
在單元測(cè)試中,我們可以使用`fixture`來獲取組件V層相關(guān)的數(shù)據(jù),比如可以使用如下代碼來獲取當(dāng)前V層對(duì)應(yīng)的`dom`節(jié)點(diǎn):
```typescript
+++ b/first-app/src/app/login/login.component.spec.ts
@@ -26,5 +26,7 @@ fdescribe('LoginComponent', () => {
it('should create', () => {
expect(component).toBeTruthy();
fixture.autoDetectChanges();
+
+ console.log(fixture.elementRef.nativeElement);
});
});
```
來到控制臺(tái)點(diǎn)擊對(duì)應(yīng)的日志內(nèi)容,對(duì)應(yīng)當(dāng)前組件的`dom`。

HTML中的每個(gè)元素都對(duì)應(yīng)一個(gè)對(duì)象,該對(duì)象被稱為**文檔對(duì)象模型(Document Object Model)**。我們使用JS來操作HTML頁(yè)面的便是通過操作這個(gè)**文檔對(duì)象模型**。這樣一來,相較于傳統(tǒng)的直接編寫html代碼,網(wǎng)頁(yè)的生成便又多了一種方法:javascript 操作 dom。
獲取到組件dom后,我們?cè)诟鶕?jù)`dom`知識(shí)來獲取當(dāng)前`dom`下的子`dom` ---- 登錄按鈕。
```typescript
+++ b/first-app/src/app/login/login.component.spec.ts
@@ -2,6 +2,7 @@ import {ComponentFixture, TestBed} from '@angular/core/testing';
import {LoginComponent} from './login.component';
import {FormsModule} from '@angular/forms';
+import {root} from 'rxjs/internal-compatibility';
fdescribe('LoginComponent', () => {
let component: LoginComponent;
@@ -28,5 +29,8 @@ fdescribe('LoginComponent', () => {
fixture.autoDetectChanges();
console.log(fixture.elementRef.nativeElement);
+ const rootDivElement = fixture.elementRef.nativeElement as HTMLDivElement; ??
+ const submitButtonElement = rootDivElement.querySelector('button') as HTMLButtonElement; ??
+ console.log(submitButtonElement);
});
});
```
- 根據(jù)實(shí)際情況,使用`as`來為變量指定一個(gè)類型。 ??
## 使用代碼點(diǎn)擊登錄按鈕
使用代碼對(duì)按鈕進(jìn)行點(diǎn)擊非常簡(jiǎn)單,僅僅需要調(diào)用該對(duì)象的`click()`方法即可:
```typescript
+++ b/first-app/src/app/login/login.component.spec.ts
@@ -32,5 +32,7 @@ fdescribe('LoginComponent', () => {
const rootDivElement = fixture.elementRef.nativeElement as HTMLDivElement;
const submitButtonElement = rootDivElement.querySelector('button') as HTMLButtonElement;
console.log(submitButtonElement);
+
+ submitButtonElement.click();
});
});
```
此時(shí)在控制臺(tái)中成功的打印了相關(guān)日志,說明C層的`onSubmit`方法被成功的觸發(fā)了,從而證明了綁定是成功的。

上述過程中,我們成功的實(shí)現(xiàn)了:使用代碼來點(diǎn)擊**登錄按鈕**,但在最后的驗(yàn)證環(huán)節(jié)仍然是使用人眼進(jìn)行觀察的,這仍然沒有消除人眼觀察的**不可靠性**。
### 驗(yàn)證C層方法被觸發(fā)
遺憾的是,除了觀察,我們是沒有辦法直接驗(yàn)證某個(gè)C層的方法是否被成功的調(diào)用的。為了實(shí)現(xiàn)這種驗(yàn)證,我們采用:建造模擬C層的方法來間接達(dá)到這個(gè)目的。
所謂的模擬C層,就是在根據(jù)當(dāng)前的C層,建立一個(gè)外表看起來一模一樣的C層。原C層有什么方法,我們的模擬C層就會(huì)有什么方法;原C層的方法中有什么樣的參數(shù),我們的模擬C層的方法中也會(huì)有什么參數(shù)。
如果我們僅僅是為了驗(yàn)證某一個(gè)方法,則還可以在這個(gè)方法上安排一個(gè)間諜。這像極了我們?cè)陔娨晞≈锌吹降恼檻?zhàn)片。為了獲取一手的情況,我們?cè)谠跀撤降那閳?bào)部門安排一個(gè)間諜。此時(shí),敵方在我方間諜發(fā)送信息時(shí),實(shí)際的信息卻被我方獲取了。
Jasmie提供的`spyOn`方法提供了這種放置間諜的功能:
```typescript
+++ b/first-app/src/app/login/login.component.spec.ts
@@ -32,7 +32,8 @@ fdescribe('LoginComponent', () => {
const rootDivElement = fixture.elementRef.nativeElement as HTMLDivElement;
const submitButtonElement = rootDivElement.querySelector('button') as HTMLButtonElement;
console.log(submitButtonElement);
-
+
+ spyOn(component, 'onSubmit');
submitButtonElement.click();
});
});
```
此時(shí)再次運(yùn)行單元測(cè)試代碼,發(fā)現(xiàn)控制臺(tái)的日志不見了:

為了近一步確認(rèn)的確是間諜方法被調(diào)用了,我們還可以補(bǔ)充下`spyOn`方法:
```typescript
+++ b/first-app/src/app/login/login.component.spec.ts
@@ -33,7 +33,7 @@ fdescribe('LoginComponent', () => {
const submitButtonElement = rootDivElement.querySelector('button') as HTMLButtonElement;
console.log(submitButtonElement);
- spyOn(component, 'onSubmit');
+ spyOn(component, 'onSubmit').and.callFake(() => console.log('間諜方法被調(diào)用'));
submitButtonElement.click();
});
});
```

最后,我們加入以下驗(yàn)證代碼,來間諜來替我們驗(yàn)證`onSubmit`方法的確是被調(diào)用了。
```typescript
+++ b/first-app/src/app/login/login.component.spec.ts
@@ -34,6 +34,10 @@ fdescribe('LoginComponent', () => {
console.log(submitButtonElement);
spyOn(component, 'onSubmit').and.callFake(() => console.log('間諜方法被調(diào)用'));
+ // 點(diǎn)擊按鈕以前,onSubmit方法應(yīng)該被調(diào)用了0次。
+ expect??(component.onSubmit).toHaveBeenCalledTimes(0);
submitButtonElement.click();
+ // 點(diǎn)擊按鈕以后,onSubmit方法應(yīng)該被調(diào)用了1次。
+ expect??(component.onSubmit).toHaveBeenCalledTimes(1);
});
});
```
**expect**??代表期望,就是說我們預(yù)計(jì)組件的方法是被調(diào)用了0次或是1次,如果實(shí)際的情況與我們預(yù)計(jì)的相同,則該代碼將正確運(yùn)行;如果實(shí)際情況與我們預(yù)計(jì)的不同,則該處代碼將會(huì)觸發(fā)異常。

此時(shí),如果我們刪除V層中關(guān)于觸發(fā)C層的代碼,則會(huì)得到如下異常。

該異常提示我們:點(diǎn)擊了V層的登錄按鈕后,并沒有調(diào)用C層的`onSubmit`方法。而這種情況是不正確的。
## 本節(jié)作業(yè)
嘗試刪除V層中關(guān)于觸發(fā)C層的代碼來觸發(fā)異常。
| 名稱 | 地址 | 備注 |
| ----------------------- | ------------------------------------------------------------ | --------------------------- |
| html dom | [https://www.runoob.com/htmldom/htmldom-tutorial.html](https://www.runoob.com/htmldom/htmldom-tutorial.html) | |
| HtmlDivElement | [https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLElement](https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLElement) | 建議將語(yǔ)言切換為English查看 |
| HtmlButtonElement | [https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLButtonElement](https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLButtonElement) | 建議將語(yǔ)言切換為English查看 |
| Element.querySelector() | [https://developer.mozilla.org/en-US/docs/Web/API/Element/querySelector](https://developer.mozilla.org/en-US/docs/Web/API/Element/querySelector) | 看不太懂時(shí),再切回中文查看 |
| click() | [https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/click](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/click) | |
| spyOn | [https://jasmine.github.io/api/edge/global.html#spyOn](https://jasmine.github.io/api/edge/global.html#spyOn) | |
| Expect | [https://jasmine.github.io/api/edge/global.html#expect](https://jasmine.github.io/api/edge/global.html#expect) | |
| 本節(jié)源碼 | [https://github.com/mengyunzhi/angular11-guild/archive/step3.1.zip](https://github.com/mengyunzhi/angular11-guild/archive/step3.1.zip) | |
- 序言
- 第一章 Hello World
- 1.1 環(huán)境安裝
- 1.2 Hello Angular
- 1.3 Hello World!
- 第二章 教師管理
- 2.1 教師列表
- 2.1.1 初始化原型
- 2.1.2 組件生命周期之初始化
- 2.1.3 ngFor
- 2.1.4 ngIf、ngTemplate
- 2.1.5 引用 Bootstrap
- 2.2 請(qǐng)求后臺(tái)數(shù)據(jù)
- 2.2.1 HttpClient
- 2.2.2 請(qǐng)求數(shù)據(jù)
- 2.2.3 模塊與依賴注入
- 2.2.4 異步與回調(diào)函數(shù)
- 2.2.5 集成測(cè)試
- 2.2.6 本章小節(jié)
- 2.3 新增教師
- 2.3.1 組件初始化
- 2.3.2 [(ngModel)]
- 2.3.3 對(duì)接后臺(tái)
- 2.3.4 路由
- 2.4 編輯教師
- 2.4.1 組件初始化
- 2.4.2 獲取路由參數(shù)
- 2.4.3 插值與模板表達(dá)式
- 2.4.4 初識(shí)泛型
- 2.4.5 更新教師
- 2.4.6 測(cè)試中的路由
- 2.5 刪除教師
- 2.6 收尾工作
- 2.6.1 RouterLink
- 2.6.2 fontawesome圖標(biāo)庫(kù)
- 2.6.3 firefox
- 2.7 總結(jié)
- 第三章 用戶登錄
- 3.1 初識(shí)單元測(cè)試
- 3.2 http概述
- 3.3 Basic access authentication
- 3.4 著陸組件
- 3.5 @Output
- 3.6 TypeScript 類
- 3.7 瀏覽器緩存
- 3.8 總結(jié)
- 第四章 個(gè)人中心
- 4.1 原型
- 4.2 管道
- 4.3 對(duì)接后臺(tái)
- 4.4 x-auth-token認(rèn)證
- 4.5 攔截器
- 4.6 小結(jié)
- 第五章 系統(tǒng)菜單
- 5.1 延遲及測(cè)試
- 5.2 手動(dòng)創(chuàng)建組件
- 5.3 隱藏測(cè)試信息
- 5.4 規(guī)劃路由
- 5.5 定義菜單
- 5.6 注銷
- 5.7 小結(jié)
- 第六章 班級(jí)管理
- 6.1 新增班級(jí)
- 6.1.1 組件初始化
- 6.1.2 MockApi 新建班級(jí)
- 6.1.3 ApiInterceptor
- 6.1.4 數(shù)據(jù)驗(yàn)證
- 6.1.5 教師選擇列表
- 6.1.6 MockApi 教師列表
- 6.1.7 代碼重構(gòu)
- 6.1.8 小結(jié)
- 6.2 教師列表組件
- 6.2.1 初始化
- 6.2.2 響應(yīng)式表單
- 6.2.3 getTestScheduler()
- 6.2.4 應(yīng)用組件
- 6.2.5 小結(jié)
- 6.3 班級(jí)列表
- 6.3.1 原型設(shè)計(jì)
- 6.3.2 初始化分頁(yè)
- 6.3.3 MockApi
- 6.3.4 靜態(tài)分頁(yè)
- 6.3.5 動(dòng)態(tài)分頁(yè)
- 6.3.6 @Input()
- 6.4 編輯班級(jí)
- 6.4.1 測(cè)試模塊
- 6.4.2 響應(yīng)式表單驗(yàn)證
- 6.4.3 @Input()
- 6.4.4 FormGroup
- 6.4.5 自定義FormControl
- 6.4.6 代碼重構(gòu)
- 6.4.7 小結(jié)
- 6.5 刪除班級(jí)
- 6.6 集成測(cè)試
- 6.6.1 惰性加載
- 6.6.2 API攔截器
- 6.6.3 路由與跳轉(zhuǎn)
- 6.6.4 ngStyle
- 6.7 初識(shí)Service
- 6.7.1 catchError
- 6.7.2 單例服務(wù)
- 6.7.3 單元測(cè)試
- 6.8 小結(jié)
- 第七章 學(xué)生管理
- 7.1 班級(jí)列表組件
- 7.2 新增學(xué)生
- 7.2.1 exports
- 7.2.2 自定義驗(yàn)證器
- 7.2.3 異步驗(yàn)證器
- 7.2.4 再識(shí)DI
- 7.2.5 屬性型指令
- 7.2.6 完成功能
- 7.2.7 小結(jié)
- 7.3 單元測(cè)試進(jìn)階
- 7.4 學(xué)生列表
- 7.4.1 JSON對(duì)象與對(duì)象
- 7.4.2 單元測(cè)試
- 7.4.3 分頁(yè)模塊
- 7.4.4 子組件測(cè)試
- 7.4.5 重構(gòu)分頁(yè)
- 7.5 刪除學(xué)生
- 7.5.1 第三方dialog
- 7.5.2 批量刪除
- 7.5.3 面向?qū)ο?/a>
- 7.6 集成測(cè)試
- 7.7 編輯學(xué)生
- 7.7.1 初始化
- 7.7.2 自定義provider
- 7.7.3 更新學(xué)生
- 7.7.4 集成測(cè)試
- 7.7.5 可訂閱的路由參數(shù)
- 7.7.6 小結(jié)
- 7.8 總結(jié)
- 第八章 其它
- 8.1 打包構(gòu)建
- 8.2 發(fā)布部署
- 第九章 總結(jié)