重構(gòu)是保持項(xiàng)目?jī)?yōu)秀的必經(jīng)之路,在前面的小節(jié)中,我們發(fā)現(xiàn)分頁(yè)模塊在數(shù)據(jù)量大時(shí)表示的差強(qiáng)人意:

上圖的總頁(yè)碼數(shù)為21,這明顯有些過(guò)長(zhǎng)。本節(jié)我們嘗試將其其默認(rèn)的最大頁(yè)碼數(shù)設(shè)置為7。
當(dāng)我們解決此類問(wèn)題時(shí),首要的任務(wù)的是重現(xiàn)錯(cuò)誤。在重現(xiàn)錯(cuò)誤的過(guò)程中,包括了對(duì)錯(cuò)誤產(chǎn)生原因的猜想,以及最終對(duì)該猜想的驗(yàn)證(這個(gè)過(guò)程可能是不自覺(jué)的)。
## 啟動(dòng)分頁(yè)組件
我們來(lái)到分頁(yè)組件所在的位置:`src/app/clazz/page`,找到其對(duì)應(yīng)的單元測(cè)試文件。并添加一個(gè)測(cè)試用例:
```typescript
+++ b/first-app/src/app/clazz/page/page.component.spec.ts
@@ -68,4 +68,7 @@ describe('PageComponent', () => {
expect(navHtml.style.visibility).toEqual('visible');
});
+ fit('將總頁(yè)碼的最大數(shù)量控制在7頁(yè)', () => {
+
+ });
});
```
然后使用`ng t`來(lái)啟動(dòng)單元測(cè)試。
## 分析問(wèn)題
想修正或完善一些功能時(shí),我們往往使用的按數(shù)據(jù)流的方向進(jìn)行逆向推導(dǎo)的方法。比如當(dāng)前出現(xiàn)的問(wèn)題是分頁(yè)數(shù)量過(guò)多,則應(yīng)該先想看是直接生成html的V層的代碼是哪些:
```html
<li *ngFor="let p of pages①" [ngClass]="{active: currentPage === p}" class="page-item">
<span class="page-link" (click)="onPage(p)">{{p + 1}}</span>
</li>
```
由上述代碼直接推導(dǎo)出,分頁(yè)數(shù)據(jù)過(guò)多的原因是由于①標(biāo)注的`pages`數(shù)組中的元素過(guò)多引起的。
然后接著向前推導(dǎo):`pages`變量的值是由C層傳過(guò)來(lái)的,則接下來(lái)應(yīng)該找到`pages`變量在C層中的賦值情況:
```typescript
pages: number[] = []; ① ??
currentPage = 0;
@Input()
set page(page: Page<any>) {
this.inputPage = page; ④ ??
console.log('set page被調(diào)用');
console.log('當(dāng)前頁(yè)', this.inputPage.number);
console.log('總頁(yè)數(shù)', this.inputPage.totalPages);
// 生成頁(yè)數(shù)數(shù)組
this.pages = []; ② ??
for (let i = 0; i < this.inputPage.totalPages; i++) {
this.pages.push(i); ③ ??
}
// 設(shè)置當(dāng)前頁(yè)
this.currentPage = this.inputPage.number;
}
```
查閱C層的代碼(可以使用ctrl+f進(jìn)行快速查找),發(fā)現(xiàn)對(duì)`pages`變量進(jìn)行設(shè)置的地點(diǎn)有3處;第①處為初始化、第②處為初始化,此兩處操作會(huì)將`pages`清空,所以問(wèn)題應(yīng)該在第③處,即按照傳入的`inputPage.totalPages`的值進(jìn)行遍歷,而`pages`最終的個(gè)數(shù)取決于`inputPage.totalPages`的值的大小。
按照逆向的理論繼續(xù)向上找`inputPage.totalPages`的值是由 ④決定的,而④正好是`Input()`調(diào)用,即父組件傳入。
所以最終得到以下結(jié)論:父組件向當(dāng)前組件傳入`page`時(shí),將按傳入的`page`對(duì)象上的`totalPages`的值的大小來(lái)初始化組件的頁(yè)碼。
## 重現(xiàn)問(wèn)題
那么如果想重現(xiàn)問(wèn)題,則傳入一個(gè)具有較大的`totalPages`的`page`對(duì)象即可,我們?cè)跍y(cè)試用例中構(gòu)造這個(gè)對(duì)象:
```typescript
+++ b/first-app/src/app/clazz/page/page.component.spec.ts
@@ -69,6 +69,11 @@ describe('PageComponent', () => {
});
fit('將總頁(yè)碼的最大數(shù)量控制在7頁(yè)', () => {
-
+ component.page = {
+ number: 2,
+ size: 20,
+ totalPages: 20
+ } as Page<any>;
+ fixture.autoDetectChanges();
});
});
```
問(wèn)題成功被重現(xiàn):

## 單元測(cè)試
在解決問(wèn)題時(shí),我們常常會(huì)“故此失彼”。所以在動(dòng)手前,盡可能多的考慮一些情況是非常有必要的。我們?cè)诖私o出幾種分頁(yè)情況:
第一種:當(dāng)總頁(yè)數(shù)小于7時(shí)能夠準(zhǔn)確顯示,比如:`1 2 3 [4] 5`、`1 [2] 3`
第二種:總頁(yè)數(shù)等于7時(shí),比如:`1 2 3 4 [5] 6 7`、`1 2 3 4 5 6 [7]`
第三種:總頁(yè)數(shù)大于7頁(yè),需要能夠正確的處理以下各種情況:
第1頁(yè),共8頁(yè):`[1] 2 3 4 5 6 7`
第3頁(yè),共8頁(yè):`1 2 [3] 4 5 6 7`
第4頁(yè),共8頁(yè):`1 2 3 [4] 5 6 7`
第10頁(yè),共19頁(yè):`7 8 9 [10] 11 12 13`
第15頁(yè),共18頁(yè):`12 13 14 [15] 16 17 18`
第16頁(yè),共18頁(yè):`12 13 14 15 [16] 17 18`
第18頁(yè),共18頁(yè):`12 13 14 15 16 17 [18]`
### 測(cè)試用例
為了避免不小心把哪個(gè)功能給遺漏掉或KILL掉,可以為每個(gè)小功能點(diǎn)建立一個(gè)測(cè)試用例,然后使用斷言的方法來(lái)對(duì)功能進(jìn)行保證。在完善功能后可以統(tǒng)一的執(zhí)行單元測(cè)試,當(dāng)每個(gè)單元測(cè)試都順利通過(guò)時(shí),就說(shuō)明整個(gè)功能開(kāi)發(fā)的沒(méi)有問(wèn)題了。
#### 總頁(yè)碼小于7
`1 2 3 [4] 5`、`1 [2] 3`
```typescript
fit('總頁(yè)碼小于7', () => {
// 共5頁(yè),當(dāng)前第4頁(yè)
component.page = {
number: 3,
size: 20,
totalPages: 5
} as Page<any>;
fixture.detectChanges();
expect(component.pages.length).toBe(5);
// 共3頁(yè),當(dāng)前第2頁(yè)
component.page = {
number: 1,
size: 20,
totalPages: 3
} as Page<any>;
fixture.detectChanges();
expect(component.pages.length).toBe(3);
});
```
測(cè)試通過(guò),說(shuō)明當(dāng)前代碼可以滿足此要求。無(wú)需對(duì)代碼進(jìn)行改動(dòng)。
#### 總頁(yè)數(shù)等于7時(shí)
`1 2 3 4 [5] 6 7`、`1 2 3 4 5 6 [7]`
```typescript
fit('總頁(yè)碼等于7', () => {
// 共7頁(yè),當(dāng)前第5頁(yè)
component.page = {
number: 4,
size: 20,
totalPages: 7
} as Page<any>;
fixture.detectChanges();
expect(component.pages.length).toBe(7);
// 共7頁(yè),當(dāng)前第7頁(yè)
component.page = {
number: 6,
size: 20,
totalPages: 7
} as Page<any>;
fixture.detectChanges();
expect(component.pages.length).toBe(7);
});
```
測(cè)試通過(guò),說(shuō)明當(dāng)前代碼可以滿足此要求。無(wú)需對(duì)代碼進(jìn)行改動(dòng)。
#### 總頁(yè)數(shù)大于7頁(yè)
第1頁(yè),共8頁(yè):`[1] 2 3 4 5 6 7`
```typescript
fit('總頁(yè)數(shù)大于7, 第1頁(yè),共8頁(yè)', () => {
// 共1頁(yè),當(dāng)前第8頁(yè)
component.page = {
number: 0,
size: 20,
totalPages: 8
} as Page<any>;
fixture.detectChanges();
// 共顯示7頁(yè)
expect(component.pages.length).toBe(7);
// 頭是第1頁(yè)
expect(component.pages[0]).toBe(0);
// 尾是第7頁(yè)
expect(component.pages.pop()).toBe(6);
});
```
發(fā)生異常如下:

這說(shuō)明當(dāng)前的代碼已經(jīng)不能夠滿足當(dāng)前需求了。
雖然單元測(cè)試報(bào)錯(cuò)了,但我們并不著急下手寫代碼。這是由于只有當(dāng)充分的了解所有的需求后,再上手寫代碼才是效率最高做無(wú)用功最少的。
第3頁(yè),共8頁(yè):`1 2 [3] 4 5 6 7`
```typescript
fit('1 2 [3] 4 5 6 7', () => {
// 共3頁(yè),當(dāng)前第8頁(yè)
component.page = {
number: 2,
size: 20,
totalPages: 8
} as Page<any>;
fixture.detectChanges();
// 共顯示7頁(yè)
expect(component.pages.length).toBe(7);
// 頭是第1頁(yè)
expect(component.pages[0]).toBe(0);
// 尾是第7頁(yè)
expect(component.pages.pop()).toBe(6);
});
```
第4頁(yè),共8頁(yè):`1 2 3 [4] 5 6 7`
```typescript
fit('1 2 3 [4] 5 6 7', () => {
// 共4頁(yè),當(dāng)前第8頁(yè)
component.page = {
number: 3,
size: 20,
totalPages: 8
} as Page<any>;
fixture.detectChanges();
// 共顯示7頁(yè)
expect(component.pages.length).toBe(7);
// 頭是第1頁(yè)
expect(component.pages[0]).toBe(0);
// 尾是第7頁(yè)
expect(component.pages.pop()).toBe(6);
});
```
第10頁(yè),共18頁(yè):`7 8 9 [10] 11 12 13`
```typescript
fit('7 8 9 [10] 11 12 13`', () => {
component.page = {
number: 9,
size: 20,
totalPages: 18
} as Page<any>;
fixture.detectChanges();
// 共顯示7頁(yè)
expect(component.pages.length).toBe(7);
expect(component.pages[0]).toBe(6);
expect(component.pages.pop()).toBe(12);
});
```
第15頁(yè),共18頁(yè):`12 13 14 [15] 16 17 18`
```typescript
fit('12 13 14 [15] 16 17 18', () => {
component.page = {
number: 14,
size: 20,
totalPages: 18
} as Page<any>;
fixture.detectChanges();
// 共顯示7頁(yè)
expect(component.pages.length).toBe(7);
expect(component.pages[0]).toBe(11);
expect(component.pages.pop()).toBe(17);
});
```
第16頁(yè),共18頁(yè):`12 13 14 15 [16] 17 18`
```typescript
fit('12 13 14 15 [16] 17 18', () => {
component.page = {
number: 15,
size: 20,
totalPages: 18
} as Page<any>;
fixture.detectChanges();
// 共顯示7頁(yè)
expect(component.pages.length).toBe(7);
expect(component.pages[0]).toBe(11);
expect(component.pages.pop()).toBe(17);
});
```
第18頁(yè),共18頁(yè):`12 13 14 15 16 17 [18]`
```typescript
fit('12 13 14 15 16 17 [18]', () => {
component.page = {
number: 17,
size: 20,
totalPages: 18
} as Page<any>;
fixture.detectChanges();
// 共顯示7頁(yè)
expect(component.pages.length).toBe(7);
expect(component.pages[0]).toBe(11);
expect(component.pages.pop()).toBe(17);
});
```
## 解決問(wèn)題
所有的單元測(cè)試都寫完后,現(xiàn)在開(kāi)始解決問(wèn)題。這種先寫單元測(cè)試再寫功能代碼的方法被稱為:`TDD`,學(xué)名叫做測(cè)試驅(qū)動(dòng)開(kāi)發(fā),即非常出名的 Test-Driven Development。
寫了這么多單元測(cè)試用例以后,我使用以下代碼來(lái)嘗試解決當(dāng)前最大數(shù)量為7的問(wèn)題。
> [info] 建議先自己寫寫,最后再參考教程中的實(shí)現(xiàn)代碼。相信教程中的代碼也不是最簡(jiǎn)潔的,期待你給出更加簡(jiǎn)潔的實(shí)現(xiàn)方案。
```typescript
+++ b/first-app/src/app/clazz/page/page.component.ts
@@ -22,10 +22,33 @@ export class PageComponent implements OnInit {
console.log('set page被調(diào)用');
console.log('當(dāng)前頁(yè)', this.inputPage.number);
console.log('總頁(yè)數(shù)', this.inputPage.totalPages);
+ // 初始化最大頁(yè)碼,起始頁(yè)碼
+ let maxCount;
+ let begin;
+
+ if (this.inputPage.totalPages > 7) {
+ // 大于7頁(yè)時(shí),僅顯示7頁(yè)
+ maxCount = 7;
+
+ // 起始頁(yè)為當(dāng)前頁(yè)-3.比如當(dāng)前頁(yè)為10,則應(yīng)該由7頁(yè)開(kāi)始
+ begin = this.inputPage.number - 3;
+ if (begin < 0) {
+ // 判斷是否越界,可以刪除下一行代碼查看錯(cuò)誤的效果
+ begin = 0;
+ } else if (begin > this.inputPage.totalPages - 7) {
+ // 判斷是否越界,可以刪除下一行代碼查看錯(cuò)誤的效果
+ begin = this.inputPage.totalPages - 7;
+ }
+ } else {
+ // 小于等于7頁(yè)時(shí),使用原算法。頁(yè)碼數(shù)為總頁(yè)數(shù),頁(yè)碼由0開(kāi)始
+ maxCount = this.inputPage.totalPages;
+ begin = 0;
+ }
+
// 生成頁(yè)數(shù)數(shù)組
this.pages = [];
- for (let i = 0; i < this.inputPage.totalPages; i++) {
- this.pages.push(i);
+ for (let i = 0; i < maxCount; i++, begin++) {
+ this.pages.push(begin);
}
```
最終所有的單元測(cè)試全部通過(guò),說(shuō)明滿足了所有的要求。

最后移除所有的`fit`,看單元測(cè)試是否都成功通過(guò)。成功通過(guò)則說(shuō)明我們當(dāng)前的功能完善未對(duì)其它的組件造成影響。
## 總結(jié)
本節(jié)中我們使用了`TDD`測(cè)試驅(qū)動(dòng)開(kāi)發(fā)的思想,先寫了單元測(cè)試用例,最后補(bǔ)功的功能代碼。這特別適用于某些輸入與輸出都比較簡(jiǎn)單,但邏輯實(shí)現(xiàn)稍微復(fù)雜的方法。
其實(shí)所有的方法都不是萬(wàn)能的,TDD的開(kāi)發(fā)思想雖然好,但卻并不適合于新手。但如若一直把自己當(dāng)前新手來(lái)看待,將可能永遠(yuǎn)也用不到TDD的開(kāi)發(fā)思想。所以我們建議是,在單元測(cè)試這里,看自己的能力能寫多少寫多少,在寫的過(guò)程中,如果感覺(jué)特別難就換一種方式。寫單元測(cè)試代碼時(shí)間應(yīng)該不大于寫功能代碼的2倍,如果時(shí)間超過(guò)了2倍,則應(yīng)該考慮減小單元測(cè)試的難度。對(duì)于是先書功能代碼還是單元測(cè)試代碼的問(wèn)題,則應(yīng)該:哪個(gè)容易寫哪個(gè)。
| 鏈接 | 名稱 |
| ------------------------------------------------------------ | --------------- |
| [https://github.com/mengyunzhi/angular11-guild/archive/step7.4.5.zip](https://github.com/mengyunzhi/angular11-guild/archive/step7.4.5.zip) | 本節(jié)源碼 |
| [https://baike.baidu.com/item/TDD/9064369](https://baike.baidu.com/item/TDD/9064369) ---- 注意:它的視頻錯(cuò)了 | TDD測(cè)試驅(qū)動(dòng)開(kāi)發(fā) |
| [https://zh.wikipedia.org/zh-hans/%E6%B5%8B%E8%AF%95%E9%A9%B1%E5%8A%A8%E5%BC%80%E5%8F%91](https://zh.wikipedia.org/zh-hans/%E6%B5%8B%E8%AF%95%E9%A9%B1%E5%8A%A8%E5%BC%80%E5%8F%91) | TDD測(cè)試驅(qū)動(dòng)開(kāi)發(fā) |
- 序言
- 第一章 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é)