重构是保持项目优秀的必经之路,在前面的小节中,我们发现分页模块在数据量大时表示的差强人意:
![image-20210522090115935](https://img.kancloud.cn/e1/ac/e1acb520c3475effd5d92c762184dede_1710x462.png)
上图的总页码数为21,这明显有些过长。本节我们尝试将其其默认的最大页码数设置为7。
当我们解决此类问题时,首要的任务的是重现错误。在重现错误的过程中,包括了对错误产生原因的猜想,以及最终对该猜想的验证(这个过程可能是不自觉的)。
## 启动分页组件
我们来到分页组件所在的位置:`src/app/clazz/page`,找到其对应的单元测试文件。并添加一个测试用例:
```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('将总页码的最大数量控制在7页', () => {
+
+ });
});
```
然后使用`ng t`来启动单元测试。
## 分析问题
想修正或完善一些功能时,我们往往使用的按数据流的方向进行逆向推导的方法。比如当前出现的问题是分页数量过多,则应该先想看是直接生成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>
```
由上述代码直接推导出,分页数据过多的原因是由于①标注的`pages`数组中的元素过多引起的。
然后接着向前推导:`pages`变量的值是由C层传过来的,则接下来应该找到`pages`变量在C层中的赋值情况:
```typescript
pages: number[] = []; ① 👈
currentPage = 0;
@Input()
set page(page: Page<any>) {
this.inputPage = page; ④ 👈
console.log('set page被调用');
console.log('当前页', this.inputPage.number);
console.log('总页数', this.inputPage.totalPages);
// 生成页数数组
this.pages = []; ② 👈
for (let i = 0; i < this.inputPage.totalPages; i++) {
this.pages.push(i); ③ 👈
}
// 设置当前页
this.currentPage = this.inputPage.number;
}
```
查阅C层的代码(可以使用ctrl+f进行快速查找),发现对`pages`变量进行设置的地点有3处;第①处为初始化、第②处为初始化,此两处操作会将`pages`清空,所以问题应该在第③处,即按照传入的`inputPage.totalPages`的值进行遍历,而`pages`最终的个数取决于`inputPage.totalPages`的值的大小。
按照逆向的理论继续向上找`inputPage.totalPages`的值是由 ④决定的,而④正好是`Input()`调用,即父组件传入。
所以最终得到以下结论:父组件向当前组件传入`page`时,将按传入的`page`对象上的`totalPages`的值的大小来初始化组件的页码。
## 重现问题
那么如果想重现问题,则传入一个具有较大的`totalPages`的`page`对象即可,我们在测试用例中构造这个对象:
```typescript
+++ b/first-app/src/app/clazz/page/page.component.spec.ts
@@ -69,6 +69,11 @@ describe('PageComponent', () => {
});
fit('将总页码的最大数量控制在7页', () => {
-
+ component.page = {
+ number: 2,
+ size: 20,
+ totalPages: 20
+ } as Page<any>;
+ fixture.autoDetectChanges();
});
});
```
问题成功被重现:
![image-20210607093427541](https://img.kancloud.cn/4a/b5/4ab56ec002309f298d8fcaacf20cf129_2500x302.png)
## 单元测试
在解决问题时,我们常常会“故此失彼”。所以在动手前,尽可能多的考虑一些情况是非常有必要的。我们在此给出几种分页情况:
第一种:当总页数小于7时能够准确显示,比如:`1 2 3 [4] 5`、`1 [2] 3`
第二种:总页数等于7时,比如:`1 2 3 4 [5] 6 7`、`1 2 3 4 5 6 [7]`
第三种:总页数大于7页,需要能够正确的处理以下各种情况:
第1页,共8页:`[1] 2 3 4 5 6 7`
第3页,共8页:`1 2 [3] 4 5 6 7`
第4页,共8页:`1 2 3 [4] 5 6 7`
第10页,共19页:`7 8 9 [10] 11 12 13`
第15页,共18页:`12 13 14 [15] 16 17 18`
第16页,共18页:`12 13 14 15 [16] 17 18`
第18页,共18页:`12 13 14 15 16 17 [18]`
### 测试用例
为了避免不小心把哪个功能给遗漏掉或KILL掉,可以为每个小功能点建立一个测试用例,然后使用断言的方法来对功能进行保证。在完善功能后可以统一的执行单元测试,当每个单元测试都顺利通过时,就说明整个功能开发的没有问题了。
#### 总页码小于7
`1 2 3 [4] 5`、`1 [2] 3`
```typescript
fit('总页码小于7', () => {
// 共5页,当前第4页
component.page = {
number: 3,
size: 20,
totalPages: 5
} as Page<any>;
fixture.detectChanges();
expect(component.pages.length).toBe(5);
// 共3页,当前第2页
component.page = {
number: 1,
size: 20,
totalPages: 3
} as Page<any>;
fixture.detectChanges();
expect(component.pages.length).toBe(3);
});
```
测试通过,说明当前代码可以满足此要求。无需对代码进行改动。
#### 总页数等于7时
`1 2 3 4 [5] 6 7`、`1 2 3 4 5 6 [7]`
```typescript
fit('总页码等于7', () => {
// 共7页,当前第5页
component.page = {
number: 4,
size: 20,
totalPages: 7
} as Page<any>;
fixture.detectChanges();
expect(component.pages.length).toBe(7);
// 共7页,当前第7页
component.page = {
number: 6,
size: 20,
totalPages: 7
} as Page<any>;
fixture.detectChanges();
expect(component.pages.length).toBe(7);
});
```
测试通过,说明当前代码可以满足此要求。无需对代码进行改动。
#### 总页数大于7页
第1页,共8页:`[1] 2 3 4 5 6 7`
```typescript
fit('总页数大于7, 第1页,共8页', () => {
// 共1页,当前第8页
component.page = {
number: 0,
size: 20,
totalPages: 8
} as Page<any>;
fixture.detectChanges();
// 共显示7页
expect(component.pages.length).toBe(7);
// 头是第1页
expect(component.pages[0]).toBe(0);
// 尾是第7页
expect(component.pages.pop()).toBe(6);
});
```
发生异常如下:
![image-20210607100555414](https://img.kancloud.cn/8b/d8/8bd86a4a7d634ac7076b889c0b4215b3_950x128.png)
这说明当前的代码已经不能够满足当前需求了。
虽然单元测试报错了,但我们并不着急下手写代码。这是由于只有当充分的了解所有的需求后,再上手写代码才是效率最高做无用功最少的。
第3页,共8页:`1 2 [3] 4 5 6 7`
```typescript
fit('1 2 [3] 4 5 6 7', () => {
// 共3页,当前第8页
component.page = {
number: 2,
size: 20,
totalPages: 8
} as Page<any>;
fixture.detectChanges();
// 共显示7页
expect(component.pages.length).toBe(7);
// 头是第1页
expect(component.pages[0]).toBe(0);
// 尾是第7页
expect(component.pages.pop()).toBe(6);
});
```
第4页,共8页:`1 2 3 [4] 5 6 7`
```typescript
fit('1 2 3 [4] 5 6 7', () => {
// 共4页,当前第8页
component.page = {
number: 3,
size: 20,
totalPages: 8
} as Page<any>;
fixture.detectChanges();
// 共显示7页
expect(component.pages.length).toBe(7);
// 头是第1页
expect(component.pages[0]).toBe(0);
// 尾是第7页
expect(component.pages.pop()).toBe(6);
});
```
第10页,共18页:`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页
expect(component.pages.length).toBe(7);
expect(component.pages[0]).toBe(6);
expect(component.pages.pop()).toBe(12);
});
```
第15页,共18页:`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页
expect(component.pages.length).toBe(7);
expect(component.pages[0]).toBe(11);
expect(component.pages.pop()).toBe(17);
});
```
第16页,共18页:`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页
expect(component.pages.length).toBe(7);
expect(component.pages[0]).toBe(11);
expect(component.pages.pop()).toBe(17);
});
```
第18页,共18页:`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页
expect(component.pages.length).toBe(7);
expect(component.pages[0]).toBe(11);
expect(component.pages.pop()).toBe(17);
});
```
## 解决问题
所有的单元测试都写完后,现在开始解决问题。这种先写单元测试再写功能代码的方法被称为:`TDD`,学名叫做测试驱动开发,即非常出名的 Test-Driven Development。
写了这么多单元测试用例以后,我使用以下代码来尝试解决当前最大数量为7的问题。
> [info] 建议先自己写写,最后再参考教程中的实现代码。相信教程中的代码也不是最简洁的,期待你给出更加简洁的实现方案。
```typescript
+++ b/first-app/src/app/clazz/page/page.component.ts
@@ -22,10 +22,33 @@ export class PageComponent implements OnInit {
console.log('set page被调用');
console.log('当前页', this.inputPage.number);
console.log('总页数', this.inputPage.totalPages);
+ // 初始化最大页码,起始页码
+ let maxCount;
+ let begin;
+
+ if (this.inputPage.totalPages > 7) {
+ // 大于7页时,仅显示7页
+ maxCount = 7;
+
+ // 起始页为当前页-3.比如当前页为10,则应该由7页开始
+ begin = this.inputPage.number - 3;
+ if (begin < 0) {
+ // 判断是否越界,可以删除下一行代码查看错误的效果
+ begin = 0;
+ } else if (begin > this.inputPage.totalPages - 7) {
+ // 判断是否越界,可以删除下一行代码查看错误的效果
+ begin = this.inputPage.totalPages - 7;
+ }
+ } else {
+ // 小于等于7页时,使用原算法。页码数为总页数,页码由0开始
+ maxCount = this.inputPage.totalPages;
+ begin = 0;
+ }
+
// 生成页数数组
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);
}
```
最终所有的单元测试全部通过,说明满足了所有的要求。
![image-20210607103851567](https://img.kancloud.cn/bc/9a/bc9a6aae5296523d7421d0477e349949_1786x550.png)
最后移除所有的`fit`,看单元测试是否都成功通过。成功通过则说明我们当前的功能完善未对其它的组件造成影响。
## 总结
本节中我们使用了`TDD`测试驱动开发的思想,先写了单元测试用例,最后补功的功能代码。这特别适用于某些输入与输出都比较简单,但逻辑实现稍微复杂的方法。
其实所有的方法都不是万能的,TDD的开发思想虽然好,但却并不适合于新手。但如若一直把自己当前新手来看待,将可能永远也用不到TDD的开发思想。所以我们建议是,在单元测试这里,看自己的能力能写多少写多少,在写的过程中,如果感觉特别难就换一种方式。写单元测试代码时间应该不大于写功能代码的2倍,如果时间超过了2倍,则应该考虑减小单元测试的难度。对于是先书功能代码还是单元测试代码的问题,则应该:哪个容易写哪个。
| 链接 | 名称 |
| ------------------------------------------------------------ | --------------- |
| [https://github.com/mengyunzhi/angular11-guild/archive/step7.4.5.zip](https://github.com/mengyunzhi/angular11-guild/archive/step7.4.5.zip) | 本节源码 |
| [https://baike.baidu.com/item/TDD/9064369](https://baike.baidu.com/item/TDD/9064369) ---- 注意:它的视频错了 | TDD测试驱动开发 |
| [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测试驱动开发 |
- 序言
- 第一章 Hello World
- 1.1 环境安装
- 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 请求后台数据
- 2.2.1 HttpClient
- 2.2.2 请求数据
- 2.2.3 模块与依赖注入
- 2.2.4 异步与回调函数
- 2.2.5 集成测试
- 2.2.6 本章小节
- 2.3 新增教师
- 2.3.1 组件初始化
- 2.3.2 [(ngModel)]
- 2.3.3 对接后台
- 2.3.4 路由
- 2.4 编辑教师
- 2.4.1 组件初始化
- 2.4.2 获取路由参数
- 2.4.3 插值与模板表达式
- 2.4.4 初识泛型
- 2.4.5 更新教师
- 2.4.6 测试中的路由
- 2.5 删除教师
- 2.6 收尾工作
- 2.6.1 RouterLink
- 2.6.2 fontawesome图标库
- 2.6.3 firefox
- 2.7 总结
- 第三章 用户登录
- 3.1 初识单元测试
- 3.2 http概述
- 3.3 Basic access authentication
- 3.4 着陆组件
- 3.5 @Output
- 3.6 TypeScript 类
- 3.7 浏览器缓存
- 3.8 总结
- 第四章 个人中心
- 4.1 原型
- 4.2 管道
- 4.3 对接后台
- 4.4 x-auth-token认证
- 4.5 拦截器
- 4.6 小结
- 第五章 系统菜单
- 5.1 延迟及测试
- 5.2 手动创建组件
- 5.3 隐藏测试信息
- 5.4 规划路由
- 5.5 定义菜单
- 5.6 注销
- 5.7 小结
- 第六章 班级管理
- 6.1 新增班级
- 6.1.1 组件初始化
- 6.1.2 MockApi 新建班级
- 6.1.3 ApiInterceptor
- 6.1.4 数据验证
- 6.1.5 教师选择列表
- 6.1.6 MockApi 教师列表
- 6.1.7 代码重构
- 6.1.8 小结
- 6.2 教师列表组件
- 6.2.1 初始化
- 6.2.2 响应式表单
- 6.2.3 getTestScheduler()
- 6.2.4 应用组件
- 6.2.5 小结
- 6.3 班级列表
- 6.3.1 原型设计
- 6.3.2 初始化分页
- 6.3.3 MockApi
- 6.3.4 静态分页
- 6.3.5 动态分页
- 6.3.6 @Input()
- 6.4 编辑班级
- 6.4.1 测试模块
- 6.4.2 响应式表单验证
- 6.4.3 @Input()
- 6.4.4 FormGroup
- 6.4.5 自定义FormControl
- 6.4.6 代码重构
- 6.4.7 小结
- 6.5 删除班级
- 6.6 集成测试
- 6.6.1 惰性加载
- 6.6.2 API拦截器
- 6.6.3 路由与跳转
- 6.6.4 ngStyle
- 6.7 初识Service
- 6.7.1 catchError
- 6.7.2 单例服务
- 6.7.3 单元测试
- 6.8 小结
- 第七章 学生管理
- 7.1 班级列表组件
- 7.2 新增学生
- 7.2.1 exports
- 7.2.2 自定义验证器
- 7.2.3 异步验证器
- 7.2.4 再识DI
- 7.2.5 属性型指令
- 7.2.6 完成功能
- 7.2.7 小结
- 7.3 单元测试进阶
- 7.4 学生列表
- 7.4.1 JSON对象与对象
- 7.4.2 单元测试
- 7.4.3 分页模块
- 7.4.4 子组件测试
- 7.4.5 重构分页
- 7.5 删除学生
- 7.5.1 第三方dialog
- 7.5.2 批量删除
- 7.5.3 面向对象
- 7.6 集成测试
- 7.7 编辑学生
- 7.7.1 初始化
- 7.7.2 自定义provider
- 7.7.3 更新学生
- 7.7.4 集成测试
- 7.7.5 可订阅的路由参数
- 7.7.6 小结
- 7.8 总结
- 第八章 其它
- 8.1 打包构建
- 8.2 发布部署
- 第九章 总结