前后台的基本完成后,进行集成测试来验证其对接情况。按老的步骤:启动数据库 -> 启动后台 -> 启动前台 -> 添加用于支持测评编辑学生功能的基础数据 -> 添加路由以使得可以通过URL或跳转的方式来查看编辑页面 -> 按控制台报错提示添加编辑组件的依赖 -> 进行测试并按完善代码或修正BUG。
启动数据库的步骤略过。
# 启动后台
方法一:直接在idea中启动
![](https://img.kancloud.cn/62/82/62822a715cfcc1ce455e6b63057068c9_523x69.png)
方法二:使用命令行启动
![](https://img.kancloud.cn/b4/a6/b4a6b536dcd401602d9585199f189f11_781x117.png)
# 启动前台
使用webstorm打开前台项目,并使用`ng serve`来启动项目:
![](https://img.kancloud.cn/f6/ba/f6baacc170487b415db153199addf411_1080x165.png)
# 添加基础数据
打开[http://localhost:4200/](http://localhost:4200/)。在教师管理中添加一个教师,在班级管理中添加不少于2个班级,在学生管理中添加不小于2个学生,各个学生应该有所区别。
![](https://img.kancloud.cn/cf/29/cf2967fc1885095baa4653d523a61f50_1078x228.png)
![](https://img.kancloud.cn/a6/cf/a6cf12ef0b09ed8a8ea1efd2c236fd9b_1048x178.png)
# 添加路由
如果你和我一样早就忘记了如何添加路中,则可以打开前面已经开发过的`src/app/app-routing.module.ts`参考。然后打开学生模块对应的路由配置文件:`src/app/student/student-routing.module.ts`
```
import {EditComponent} from './edit/edit.component'; ✚
const routes: Routes = [
{
path: 'add',
component: AddComponent
},
{
path: '',
component: IndexComponent
}, ➊
{ ✚
path: 'edit/:id', ✚
component: EditComponent ✚
} ✚
];
```
* ➊ 注意这里应该多加一个`,`。在数组中,各个项用`,`进行分离。
# 添加依赖
打开chrome的控制台。然后按刚刚添加的路由信息,在地址栏中对应的输入相应地址。继而查看控制台报错信息,最后按报错提示添加对应依赖。
![](https://img.kancloud.cn/cc/21/cc2145e783204c90116fa32d32999c5e_901x246.png)
控制台信息:
![](https://img.kancloud.cn/f2/e0/f2e03879231d15d834ad75248849c98e_591x179.png)
控制台无报错,看来当前StudentModule有提供编辑组件中所需全部依赖的能力。
# 测试并修正
要想更好的测试编辑功能,就需要在学生列表中加入相应的链接,使用点击链接的方式进行测试。
## 加入链接
src/app/student/index/index.component.html
```
<td>{{student.klass.name}}</td>
<td><a routerLink="./edit/{{student.id}}">编辑</a> </td> ✚
</tr>
</table>
```
## 点击测试
![](https://img.kancloud.cn/eb/ad/ebadd2c3472e91a4efccc81882d31e0d_1125x312.gif)
为增强些视觉的交互效果,为`编辑`加几个样式,使它看起来更顺眼些:
```
<td><a routerLink="./edit/{{student.id}}" class="btn btn-sm btn-info">编辑</a> </td>
```
![](https://img.kancloud.cn/52/26/5226d52926642b0fdd73bfe803c1df22_1086x59.png)
## 测试问题一
编辑时班级未选中的问题:
![](https://img.kancloud.cn/fb/95/fb95cc072c035bf2fc739d7ceae3bee3_777x89.png)
解决该问题相对较复杂,将在下一小节中单元进行讲解。
## 测试问题二
选择班级后报错:
![](https://img.kancloud.cn/72/43/7243015162425f3c2b26e6c7844ad371_848x264.png)
原则上这个问题应该在单元测试时被测试出来,但单元测试中由于当前测试思路(在思想上,总相按原来的方法获取这个班级选择组件的替身,但又没有找到获取该组件的方法)尚无法进行嵌入式组件的输入、输出测试,所以单元测试跳过了该部分。
按错误提示:在学生编辑组件的V层的第5行:
src/app/student/edit/edit.component.html
```
<label>班级:<app-klass-select [klass]="student.klass" (selected)="onSelectKlass($event)"></app-klass-select></label>
```
发生错误:onSelectKlass不是一个函数。
原来在V层声明了要调用C层的onSelectKlass,但却忘记在C层中写相应的方法了,补充如下:
src/app/student/edit/edit.component.ts
```
/**
* 选择班级
* @param $event 班级
*/
onSelectKlass($event: Klass) {
this.student.klass = $event;
}
```
再次测试错语消失。
## 测试问题三
点击"保存"按钮未触发网络请求。
![](https://img.kancloud.cn/ec/ff/ecffacb58c7751b9b9a201171c04caef_1137x391.gif)
但我们很确信在单元测试中测试了这个功能。找到对应的单元测试代码来排查问题:
src/app/student/edit/edit.component.spec.ts
```
it('点击保存按钮', () => {
spyOn(component, 'onSubmit');
const button: HTMLButtonElement = fixture.debugElement.query(By.css('button')).nativeElement;
button.click();
expect(component.onSubmit).toHaveBeenCalled();
});
it('onSubmit', () => {
// 设置formGroup的值
const name = Math.random().toString(36).slice(-10);
const sno = Math.random().toString(36).slice(-10);
component.formGroup.get('name').setValue(name);
component.formGroup.get('sno').setValue(sno);
// 设置班级选择组件的值
// todo: 在官方文档中暂未找到相应的示例代码
// 调用onSubmit方法
component.onSubmit();
// 断言已使用formGroup及班级选择组件的值更新了学生信息
expect(component.student.name).toBe(name);
expect(component.student.sno).toBe(sno);
// expect(component.student.klass).toBe(null); todo: 原则上这里要测试发射值
// 断言调用 向M层传入更新的学生ID及更新的学生信息 方法 ★
});
```
* ★ 貌似找到问题了,在单元测试只写了注释却没有完成相应的代码。
为此将此测试用例的`it`暂时修改为`fit`,启用`ng test`后修正代码如下:
src/app/student/edit/edit.component.spec.ts
```
fit('onSubmit', () => {
// 设置formGroup的值
const name = Math.random().toString(36).slice(-10);
const sno = Math.random().toString(36).slice(-10);
component.formGroup.get('name').setValue(name);
component.formGroup.get('sno').setValue(sno);
// 设置班级选择组件的值
// todo: 在官方文档中暂未找到相应的示例代码
// 设置update方法替身
spyOn(component, 'update'); ✚
// 调用onSubmit方法
component.onSubmit();
// 断言已使用formGroup及班级选择组件的值更新了学生信息
expect(component.student.name).toBe(name);
expect(component.student.sno).toBe(sno);
// expect(component.student.klass).toBe(null); todo: 原则上这里要测试发射值
// 断言调用 向M层传入更新的学生ID及更新的学生信息 方法
expect(component.update).toHaveBeenCalledWith(component.student); ✚
});
```
![](https://img.kancloud.cn/6b/9d/6b9d1867c135dc7abd9d7b1e022691e9_2348x130.png)
报错:期望调用 componet.update方法,但根本就没调用过。
补充C层代码如下:
src/app/student/edit/edit.component.ts
```
onSubmit() {
this.student.name = this.formGroup.get('name').value;
this.student.sno = this.formGroup.get('sno').value;
this.student.klass = this.student.klass;
this.update(this.student); ✚
}
```
测试顺序通过。此时正常发起了网络请求且成功更新了数据。
![](https://img.kancloud.cn/75/74/75746db417019fb393fd1690e960401a_1141x370.gif)
## 测试问题四
点击保存按钮后未自动跳转至index。
实现跳转的方法有两个:第一种是直接在C层中写跳转的代码;第二种是在V层中增加一个"隐藏"的跳转链接,然后在C层中进行模拟点击,进行达到跳转的效果。由于第二种方法支持相对路径,可以适应变化的路由,在此仍然采用第二种方案。具体可参考学生模块的添加组件:
src/app/student/edit/edit.component.ts
```
/**
* 更新学生
* @param student 学生
*/
update(student: Student) {
this.studentService.update(student.id, student)
.subscribe((result) => {
this.student = result;
this.linkToIndex.nativeElement.click(); ✚
});
}
```
但在测试时点击保存按钮后却在控制台打印了如下错误信息:
![](https://img.kancloud.cn/1c/b2/1cb2c12a584ad4eba75a944d7664ae73_892x121.png)
它在说:当点击保存按钮时,尝试跳转到`student/edit`路由,但该路由未能匹配任何路由规则,所以当前路由无法进行跳转。
分析这个问题还要从两方面入手:
1. 当前路径
2. 相对路径的设置
当前路径为:`http://localhost:4200/student/edit/1`, V层中相对路径的设置为:`<a style="display: none" routerLink="./../" #linkToIndex>返回学生列表页</a>`中的`./../`。其中`./`代表当前路径即:`http://localhost:4200/student/edit/1` ;`./../`则代表当前路径的上级路径,则对应的路径为:`http://localhost:4200/student/edit`。而这个路径的确未对应任何的路由,所以得到了上面的错误。而实际上此处跳转的目标是:`http://localhost:4200/student`,所以此时的的相对路径应该修正为:
```
routerLink="./../../"
```
修正后测试正常。
> 请思索:为何在添加组件中写`./../`而不是`./../../`呢?
至此,除编辑时未选中当前klass以外,其它的编辑功能正常。集成测试完成。
## 验证测试
很长时间没有验证其它单元测试是否还在正确的运行了。下面请把单元测试中所有的`fdescribe`变更为`describe`,所有的`fit`变更为`it`。跑一遍单元测试来查看是否由于添加新的功能而对历史功能造成了影响。
![](https://img.kancloud.cn/62/27/622788b7f5dedb32825f57bf2b3084f1_644x158.png)
测试结果:60的测试用例,失败了28个。看到这样的数字后不必进行惊慌,由于单元测试是一点点的补充过来的,所以不应该出现大量的单元测试报错的情况。如果有,那么出现错误的原因也必然集中到一个点上。把一个解决了,其它的也就是随着解决了。
```
Student -> IndexComponent > 测试页码号
Failed: Template parse errors:
Can't bind to 'routerLink' since it isn't a known property of 'a'. ("
```
错误信息:不认识`routerLink`。`routeLink`由`路由模块`提供。此错误的原因应该在测试模块中未引入相应的路由模块。
src/app/student/index/index.component.spec.ts
```
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [IndexComponent, KlassSelectComponent],
imports: [
ReactiveFormsModule,
FormsModule,
CoreModule,
HttpClientTestingModule,
RouterTestingModule ✚ ],
providers: [
{provide: StudentService, useClass: StudentStubService}
]
})
.compileComponents();
}));
```
果然加入此信息后,失败的单元测试用例由28个锐减到了2个。
![](https://img.kancloud.cn/e6/3f/e63f5dd6fab861bf77115e4f286a7a86_890x130.png)
继续修正:
```
Student -> IndexComponent > 当前页、总页数、每页大小
Expected ' 第1/100页 每页2条 ' to contain '第0/100页'.
Error: Expected ' 第1/100页 每页2条 ' to contain '第0/100页'.
```
看到此测试突然想起来了:当初修正过分页的当前页码(由0变成1),也修改过页码的提示,但却未对应修正单元测试的内容。看来以后再进行功能修正的时候,一定要同步修正单元测试。否则时间一长,可能就判断不出是单元测试用例写的有问题还是功能性代码写的有问题了。
src/app/student/index/index.component.spec.ts
```
it('当前页、总页数、每页大小', () => {
/* 获取分页信息 */
const debugElement = fixture.debugElement.query(By.css('#pageInfo'));
const pageInfoDiv: HTMLDivElement = debugElement.nativeElement;
const text = pageInfoDiv.textContent;
console.log(text);
/* 断言绑定了C层的分页值 */
expect(text).toContain(`第${component.params.page}/${component.pageStudent.totalPages}页`); ✘
expect(text).toContain(`第${component.params.page + 1}/${component.pageStudent.totalPages}页`); ✚
expect(text).toContain(`每页${component.params.size}条`); ✘
expect(text).toContain(`第${component.params.page + 1}/${component.pageStudent.totalPages}页 每页${component.params.size}条`); ✚
});
```
继续修正:
```
Expected '编辑' to be ''.
Error: Expected '编辑' to be ''.
at <Jasmine>
at UserContext.<anonymous> (http://localhost:9876/_karma_webpack_/src/app/student/index/index.component.spec.ts:73:62)
```
提示说:`src/app/student/index/index.component.spec.ts`第73行62列出现错误:期望是空字符串,但实现却是编辑。
这是由于当前在index中增加了编辑按钮造成的。找到对应的测试代码,修正为:
src/app/student/index/index.component.spec.ts
```
expect(table.rows.item(row).cells.item(col++).innerText).toBe('testKlass');
expect(table.rows.item(row).cells.item(col++).innerText).toBe(''); ✘
expect(table.rows.item(row).cells.item(col++).innerText).toBe('编辑'); ✚
});
```
最终单元测试全部通过:
![](https://img.kancloud.cn/49/5e/495e06db45e220ff9ee52aa58c2fb948_445x75.png)
# 写在最后
在后期的修正或是递增开发时,往往会忽略掉集成测试,或是在集成测试中仅测试部分功能。因为相对于可以被自动执行的单元测试,集成测试的确是太耗时且充满着不确定性了。做集成测试需要对整个模块或是整个功能进行全方位的了解。比如此处的集成测试就需要先准备好教师数据,再准备好班级数据,最后还要准备好学生数据。现在数据库中才仅仅有3个数据表便需如此,数据表稍微一多便无法想像。这也是教程伊始便带领大家走向这难学、难懂、抽象、难掌握的单元测试的原因。在单元测试中,可以大胆的忽略到这些问题,可以不考虑其它的组件的工作方法,把精力集中到单一的组件开发中来;不仅如此,一理建立的正确的单元测试代码,但可以在任何时候来保障功能的正确性,从长期来看这的确是一笔相当划算的买卖。
如果你对此已小有感触,那么是否考虑到要把点击"保存"扭钮后正常跳转到列表页写入单元测试呢?如果没有单元测试的保障,假设有一天将编辑的路由path由`edit/:id`变更为`edit/:id/:username `时,是否能够快速的发现跳转功能已失效的错误呢?请继续阅读后续章节。
# 参考文档
| 名称 | 链接 | 预计学习时长(分) |
| --- | --- | --- |
| 源码地址 | [https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step4.7.6](https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step4.7.6) | - |
- 序言
- 第一章:Hello World
- 第一节:Angular准备工作
- 1 Node.js
- 2 npm
- 3 WebStorm
- 第二节:Hello Angular
- 第三节:Spring Boot准备工作
- 1 JDK
- 2 MAVEN
- 3 IDEA
- 第四节:Hello Spring Boot
- 1 Spring Initializr
- 2 Hello Spring Boot!
- 3 maven国内源配置
- 4 package与import
- 第五节:Hello Spring Boot + Angular
- 1 依赖注入【前】
- 2 HttpClient获取数据【前】
- 3 数据绑定【前】
- 4 回调函数【选学】
- 第二章 教师管理
- 第一节 数据库初始化
- 第二节 CRUD之R查数据
- 1 原型初始化【前】
- 2 连接数据库【后】
- 3 使用JDBC读取数据【后】
- 4 前后台对接
- 5 ng-if【前】
- 6 日期管道【前】
- 第三节 CRUD之C增数据
- 1 新建组件并映射路由【前】
- 2 模板驱动表单【前】
- 3 httpClient post请求【前】
- 4 保存数据【后】
- 5 组件间调用【前】
- 第四节 CRUD之U改数据
- 1 路由参数【前】
- 2 请求映射【后】
- 3 前后台对接【前】
- 4 更新数据【前】
- 5 更新某个教师【后】
- 6 路由器链接【前】
- 7 观察者模式【前】
- 第五节 CRUD之D删数据
- 1 绑定到用户输入事件【前】
- 2 删除某个教师【后】
- 第六节 代码重构
- 1 文件夹化【前】
- 2 优化交互体验【前】
- 3 相对与绝对地址【前】
- 第三章 班级管理
- 第一节 JPA初始化数据表
- 第二节 班级列表
- 1 新建模块【前】
- 2 初识单元测试【前】
- 3 初始化原型【前】
- 4 面向对象【前】
- 5 测试HTTP请求【前】
- 6 测试INPUT【前】
- 7 测试BUTTON【前】
- 8 @RequestParam【后】
- 9 Repository【后】
- 10 前后台对接【前】
- 第三节 新增班级
- 1 初始化【前】
- 2 响应式表单【前】
- 3 测试POST请求【前】
- 4 JPA插入数据【后】
- 5 单元测试【后】
- 6 惰性加载【前】
- 7 对接【前】
- 第四节 编辑班级
- 1 FormGroup【前】
- 2 x、[x]、{{x}}与(x)【前】
- 3 模拟路由服务【前】
- 4 测试间谍spy【前】
- 5 使用JPA更新数据【后】
- 6 分层开发【后】
- 7 前后台对接
- 8 深入imports【前】
- 9 深入exports【前】
- 第五节 选择教师组件
- 1 初始化【前】
- 2 动态数据绑定【前】
- 3 初识泛型
- 4 @Output()【前】
- 5 @Input()【前】
- 6 再识单元测试【前】
- 7 其它问题
- 第六节 删除班级
- 1 TDD【前】
- 2 TDD【后】
- 3 前后台对接
- 第四章 学生管理
- 第一节 引入Bootstrap【前】
- 第二节 NAV导航组件【前】
- 1 初始化
- 2 Bootstrap格式化
- 3 RouterLinkActive
- 第三节 footer组件【前】
- 第四节 欢迎界面【前】
- 第五节 新增学生
- 1 初始化【前】
- 2 选择班级组件【前】
- 3 复用选择组件【前】
- 4 完善功能【前】
- 5 MVC【前】
- 6 非NULL校验【后】
- 7 唯一性校验【后】
- 8 @PrePersist【后】
- 9 CM层开发【后】
- 10 集成测试
- 第六节 学生列表
- 1 分页【后】
- 2 HashMap与LinkedHashMap
- 3 初识综合查询【后】
- 4 综合查询进阶【后】
- 5 小试综合查询【后】
- 6 初始化【前】
- 7 M层【前】
- 8 单元测试与分页【前】
- 9 单选与多选【前】
- 10 集成测试
- 第七节 编辑学生
- 1 初始化【前】
- 2 嵌套组件测试【前】
- 3 功能开发【前】
- 4 JsonPath【后】
- 5 spyOn【后】
- 6 集成测试
- 7 @Input 异步传值【前】
- 8 值传递与引入传递
- 9 @PreUpdate【后】
- 10 表单验证【前】
- 第八节 删除学生
- 1 CSS选择器【前】
- 2 confirm【前】
- 3 功能开发与测试【后】
- 4 集成测试
- 5 定制提示框【前】
- 6 引入图标库【前】
- 第九节 集成测试
- 第五章 登录与注销
- 第一节:普通登录
- 1 原型【前】
- 2 功能设计【前】
- 3 功能设计【后】
- 4 应用登录组件【前】
- 5 注销【前】
- 6 保留登录状态【前】
- 第二节:你是谁
- 1 过滤器【后】
- 2 令牌机制【后】
- 3 装饰器模式【后】
- 4 拦截器【前】
- 5 RxJS操作符【前】
- 6 用户登录与注销【后】
- 7 个人中心【前】
- 8 拦截器【后】
- 9 集成测试
- 10 单例模式
- 第六章 课程管理
- 第一节 新增课程
- 1 初始化【前】
- 2 嵌套组件测试【前】
- 3 async管道【前】
- 4 优雅的测试【前】
- 5 功能开发【前】
- 6 实体监听器【后】
- 7 @ManyToMany【后】
- 8 集成测试【前】
- 9 异步验证器【前】
- 10 详解CORS【前】
- 第二节 课程列表
- 第三节 果断
- 1 初始化【前】
- 2 分页组件【前】
- 2 分页组件【前】
- 3 综合查询【前】
- 4 综合查询【后】
- 4 综合查询【后】
- 第节 班级列表
- 第节 教师列表
- 第节 编辑课程
- TODO返回机制【前】
- 4 弹出框组件【前】
- 5 多路由出口【前】
- 第节 删除课程
- 第七章 权限管理
- 第一节 AOP
- 总结
- 开发规范
- 备用