由于在上个小节中启用了**数据源**机制,当用户的登录状态发生改变时。负责记录用户登录状态的数据源能够及时的将登录状态的最新值发送给所有的订阅者。所以注销功能的实现只需要向该数据源发送false值即可。
# 原型
找到nav组件,并在V层中参考bootstrap的示例文档加入注销按如下:
src/app/nav/nav.component.html
```html
...
<form class="form-inline">
<button class="btn btn-success" type="button" (click)="onLogout()">注销</button>
</form>
</nav>
```
对应在C层中增加onLogout方法:
src/app/nav/nav.component.ts
```html
onLogout() {
console.log('onLogout');
}
```
## 单元测试
使用单元测试以确认用户点击"注销"按钮时成功的触发了C层的onLogout方法:
src/app/nav/nav.component.spec.ts
```javascript
fit('点击注销按钮', () => {
spyOn(component, 'onLogout');
FormTest.clickButton(fixture, 'button');
expect(component.onLogout).toHaveBeenCalled();
});
```
# 功能开发
在点击注销按钮时向TeacherService中发送**登录状态**为false即可:
src/app/nav/nav.component.ts
```javascript
onLogout() {
this.teacherService.setIsLogin(false);
}
```
## 单元测试
当被测组件nav中声明依赖于TeacherService时,按前面已掌握的测试方法,有两种解决方面来做测试准备:
第一种方案:由于TeacherService声明的注入范围为root,所以当前的测试模块可以直接获取到TeacherService。同时又由于TeacherService声明依赖于HttpClient等服务,所以要使得测试模块成功的启用TeacherService,那么直接在测试模块中注入TeacherService所依赖服务所在的模块即可,比如TeacherService依赖于HttpClient,则当前测试模块的准备代码如下:
src/app/nav/nav.component.spec.ts
```javascript
imports: [RouterTestingModule,
HttpClientTestingModule ✚
]
```
这种方案最大的缺点在于:1. 当前测试的为nav组件,却需要考虑TeacherService的依赖,这脱离了测试主线,跑题了。在测试nav组件时我们希望仅考虑该组件的依赖,而不希望再去考虑依赖的依赖。 2. 当依赖的服务的依赖链较短时还行,在如果nav组件依赖于a,然后a又依赖于b,b又依赖于c,等等等等。这在测试的构建中无疑走向一个无底洞。3. 这极不易于维护。以TeacherService为例。当前项目中Login及nav组件分别依赖于TeacherService,同时TeacherService组件又声明依赖于HttpClient。那么在构建login及nav的测试模块时,则需要分别声明依赖于HttpClient。一旦TeacherService的依赖发生变更,比如TeacherService又声明依赖于Router,那么此时login及nav的测试模块便需要分别增加对Router的依赖支持。不止如此,在这种测试模式下,也会给维护人员带来一定的思想负担:本来仅是给TeacherService中增加了一个依赖,为何nav及Login组件报错呢?
第二种方案:为了避免测试模块在获取TeacherService时使用真实的TeacherService而带来的第一点带来的种种问题。可以在测试模块中使用provide为TeacherService指定提供者TeacherStubService。这样一来当测试模块需要TeacherService时,将直接使用该TeacherStubService。
src/app/nav/nav.component.spec.ts
```javascript
providers: [
{provide: TeacherService, useClass: TeacherStubService}
]
```
这种方式规避了方案一中的弊端。但当组件与服务的依赖关系增加多时会为书写增加一些负担。比如Login及nav组件同时声明依赖于TeacherService及Router,则需要于两个组件测试模块中分别为TeacherService及Router声明提供者TeacherStubService及RouterStub。
本节提供的另一种新的解决方案将完美的规避上述两种方案的弊端,从而使构建单元测试模块变成一件非常轻松的事情。
# TestModule
如下图首先新建一个TeacherService的替身TeacherServiceStub
![](https://img.kancloud.cn/52/be/52be7885992474d681f06fa812e287d5_518x105.png)
接着新建一个TestModule,并在该模块中使用声明:使用TeacherStubService来代替TeacherService
![](https://img.kancloud.cn/a0/cf/a0cfb3f5fdbeaae909c15e6440469d0d_539x341.png)
接下来在相应的动态测试模块中引入该TestModule
![](https://img.kancloud.cn/e9/11/e911d1a273318b26c486ff6c0776515e_603x649.png)
则相应的动态测试模块将读取providers中的信息,从而将TeacherService做为依赖TeacherService注入对应的测试模块:
![](https://img.kancloud.cn/df/6e/df6e82296885af9c3aff2536df1491f3_713x646.png)
同时,还可以在测试模块声明其它真实服务的替身。如此一来,可以首先在TestModule中声明项目中所用服务的替身,然后在对应的测试模块中声明`import TestModule`,则无论测试组件依赖于哪些服务都可以被TestModule中的替身所满足。下图展示了TestModule是如何同时提供TeacherService及Router替身的。
![](https://img.kancloud.cn/ff/79/ff79024af3e9247c7120915dc0de63f8_820x685.png)
## 初始化
```
panjiedeMac-Pro:web-app panjie$ cd src/app/
panjiedeMac-Pro:app panjie$ ng g module test
CREATE src/app/test/test.module.ts (190 bytes)
```
## 创建TeacherStubService
进入test文件夹并创建service文件夹,接着进行service文件夹,使用`ng g s teacher-stub --skip-tests`命令来创建一个没有单元测试文件的服务。
```
panjiedeMac-Pro:service panjie$ ng g s teacher-stub --skip-tests ➊
CREATE src/app/test/service/teacher-stub.service.ts (140 bytes)
```
* ➊ 加入`--skip-tests`以避免创建冗余的单元测试文件
打开`src/app/test/service/teacher-stub.service.ts`,删除`@Injectable`的部分以防止误将其做为非测试的依赖被添加。
```javascript
@Injectable({ ✘
providedIn: 'root' ✘
}) ✘
export class TeacherStubService {
constructor() { }
}
```
* @Injectable的作用是设置注入范围,即声明在某个范围以内该服务可以被做为依赖自动注入。
* 去除@Injectable,以避免在开发过程中**误**注入此测试服务。
## 加入providers
```
@NgModule({
declarations: [],
imports: [
CommonModule
],
providers: [
{provide: TeacherService➊, useClass: TeacherStubService➋}
]
})
export class TestModule { }
```
* ➊➋ 使用TeacherStubService来代替TeacherService
## 测试
src/app/nav/nav.component.spec.ts
```javascript
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [NavComponent],
imports: [RouterTestingModule,
TestModule ✚
],
})
.compileComponents();
}));
fit('测试依赖注入', () => {
const service = TestBed.get(TeacherService);
console.log(service);
});
```
查看控制台打印信息如下:
![](https://img.kancloud.cn/d5/a4/d5a4ad5908de8b07805787e361a6cfe7_426x199.png)
控制台的打印信息印证了TestModule中声明的provide对于此测试模块是生效的,当获取测试模块中被注入的TeacherService时,实际上获取到的是TeacherStubService
基于此测试方案,继续完成nav组件中logout方法的单元测试
# logout单元测试
src/app/nav/nav.component.spec.ts
```javascript
fit('onLogout', () => {
const service = TestBed.get(TeacherService) as➊ TeacherService;
spyOn(service, 'setIsLogin');
component.onLogout();
expect(service.setIsLogin).toHaveBeenCalledWith(false);
});
```
* ➊ 使用as进行类型转换,以便在进一步操作service变量时能够能到编辑器的进一步提示
![](https://img.kancloud.cn/10/f7/10f71bc95f51f9698bebb5ecc22111c2_396x109.png)
提示说setisLogin方法并不存在。这个原因是由于使用了TeacherStubService替换了真实的TeacherService,但TeacherStubService尚未添加setIsLogin方法。解决方案如下:
src/app/test/service/teacher-stub.service.ts
```javascript
export class TeacherStubService {
setIsLogin(isLogin: boolean): void {
return;
}
}
```
再次运行单元测试通过。
# 修正遗漏问题
上个小节中进行了功能修正后并未及时的修正单元测试,当所有的`fit`恢复为`it`后做全局单元测试:
![](https://img.kancloud.cn/1d/84/1d84fee6e3fe24e255cd16a638915e04_618x304.png)
## LoginComponent
此单元测试的具体错误信息为:
```
Error: Expected spy log to have been called with [ true ] but it was never called.
at <Jasmine>
at UserContext.<anonymous> (http://localhost:9876/_karma_webpack_/src/app/login/login.component.spec.ts:70:25)
```
说在login.component.spec.ts的第70行的断言发生错误,断言spy log被调用,但是其根本没有被调用过。
```javascript
expect(console.log).toHaveBeenCalledWith(true);
```
查看对应C层的onSubmit方法后,确认该方法的逻辑已经由在控制台打印日志改为:返回的结果为真,则继续调用teacherService.setIsLogin方法;为假则在控制台打印"用户名密码错误"
修正如下:
src/app/login/login.component.spec.ts
```javascript
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [LoginComponent],
imports: [
ReactiveFormsModule,
HttpClientTestingModule ✘
TestModule ✚
]
})
.compileComponents();
}));
fit('onSubmit', () => {
// 获取teacherService实例,并为其login方法设置替身
const teacherService = TestBed.get(TeacherService) as TeacherService;
spyOn(teacherService, 'login').and.returnValue(of(true));
spyOn(teacherService, 'setIsLogin');
spyOn(console, 'log');
// 添加测试数据并调用
component.formGroup.get('username').setValue('testUsername');
component.formGroup.get('password').setValue('testPassword');
component.onSubmit();
// 断言成功调用teacherService的login方法
expect(teacherService.login).toHaveBeenCalledWith('testUsername', 'testPassword');
expect(teacherService.setIsLogin).toHaveBeenCalledWith(true);
});
```
测试完返回真以后,继续测试返回假:
src/app/login/login.component.spec.ts
```javascript
spyOn(teacherService, 'login').and.returnValue(of(true)); ✘
spyOn(teacherService, 'login').and.returnValues(of(true), of(false)); ➊
...
expect(teacherService.setIsLogin).toHaveBeenCalledWith(true);
// teacherService.login返回假时
component.onSubmit();
expect(console.log).toHaveBeenCalledWith('用户名密码错误');
});
```
* ➊ 对teacherService.log进行多次调用时应该使用returnValues方法,该方法接收的值将按teacherService.log被调用的顺序,每次被调用时返回1个。
对应为TeacherStubService添加login方法
src/app/test/service/teacher-stub.service.ts
```javascript
login(username: string, password: string): Observable<boolean> {
return null;
}
```
单元测试通过。
## AppComponent > should create the app
报错信息如下:
```
'app-login' is not a known element:
1. If 'app-login' is an Angular component, then verify that it is part of this module.
2. If 'app-login' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message. ("<app-nav *ngIf="isLogin"></app-nav>
```
这是由于在app组件中使用了login组件,但却没有将login组件声明在declarations中的原因,当前学习过的解决方案为两个。
第一种解决方案是引入真实的login组件,然后再引入真实的login组件所依赖的其它依赖,比如HttpClientTestingModule。这种依次向上解决依赖的方案的弊端在本节稍前的部分已有所阐述。
第二种方法是在本测试模块中声明一个与login组件具有相同selector的测试组件,并将此组件加入到测试模块中。此方案解决了第一种方案依次向上依赖的问题,在可维护性上也比较出色。但login组件同时被多个组件引用时,则需要在引用其的多个组件的测试模块中对应加入相同的login组件替身,而这必将违反不造相同的轮子的原则。
在此给出第三方案:在TestModule中声明与login组件相同selector的同名组件。然后供所有import TestModule的测试模块使用。
### 初始化
在进行src/app/test文件夹,新建component文件夹并进入:
```
panjiedeMac-Pro:component panjie$ ng g c login --skip-tests
CREATE src/app/test/component/login/login.component.sass (0 bytes)
CREATE src/app/test/component/login/login.component.html (20 bytes)
CREATE src/app/test/component/login/login.component.ts (266 bytes)
UPDATE src/app/test/test.module.ts (478 bytes)
```
### export
与提供服务替身的思想不同,若想在import TestModule的测试模块中认识LoginComponent,只需要将其声明为在exports中即可:
src/app/test/test.module.ts
```javascript
@NgModule({
declarations: [LoginComponent],
imports: [
CommonModule
],
exports➊: [
LoginComponent
],
providers: [
{provide: TeacherService, useClass: TeacherStubService}
]
})
export class TestModule { }
```
* ➊ exports来声明模块中的**公有**组件,即该组件可被import TestModule的其它模块识别被使用
### 引入TestModule
src/app/app.component.spec.ts
```javascript
imports: [
RouterTestingModule,
TestModule ✚
],
```
加入TestModule后,单元测试通过。此时并不需要像第一种方案一样去处理复杂的依赖关系,也可以在其它组件引入login组件时引用TestModule从而达到快速的构建测试模块的目的。
# 集成测试
重启后台后数据表可能已被自动清空,此时若想实现登录功能,则需要你自己想办法先在数据表中添加一名测试教师。具体测试过程略。
![](https://img.kancloud.cn/7f/be/7fbe4c7ee34d7b7edcff7892eeb95b58_1425x380.gif)
# 参考文档
| 名称 | 链接 | 预计学习时长(分) |
| --- | --- | --- |
| 源码地址 | [https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step5.1.5](https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step5.1.5) | - |
- 序言
- 第一章: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
- 总结
- 开发规范
- 备用