# 初始单元测试
原型永远是第一位的!本节中我们快速的对登录组件进行初始化,并尝试使用代码来测试登录按钮的绑定状态。
## 初始化
打开shell并进行`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/)对原型初始化如下:
```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">我们不会分享你的登录信息</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('点击了登录按钮');
+ }
}
```
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">我们不会分享你的登录信息</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>
```
## 测试
软件工程相比于交通、土木工程等其它实体工程有着先天的优势 ---- 几乎可以忽略不计的测试成本。所以我们在开发中,要摒弃**我认为**、**应该**等字眼,当不太清楚自己的代码是否正确运行时,最简单的方法就是测试一下。
加入测试代码:
```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>
```
在单元测试中:
1. 加入FormModule以支持`[(ngModel)]`
2. 启用自动检测变更以便捷观察数据的时实变更情况
```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 `快速启动组件:
![image-20210303081750151](https://img.kancloud.cn/fd/1e/fd1e172d6eb549ff6eda09185197f3a9_1624x782.png)
- 测试一:数据绑定成功
- 测试二:按钮绑定生效
## 单元测试
在一般的项目中,用人眼来对代码进行测试是不可靠的。它的不可靠主要体现在两个方面:
* 随着组件功能的增多,人眼同时检测多种测试信息,免不了顾此失彼。开发了一个新功能同时,也可能破坏了一个原有的正常的功能。
* 由于**应该看什么**并没有形成文档。和合作开发中,张三在接手了李四的组件后,完全不知道应该看什么,哪是对的,哪又是错的。
鉴于此,我们可以采用使用代码来测试代码的方法,由于这种方法是针对功能点的某个小的功能单元进行测试,所以又被称为**单元测试**,英文关键字为**Unit Test**。
在此,我们简单介绍下如何使用单元测试来验证登录按钮与C层的`onSubmit`方法是否绑定成功。
### 流程
此测试在思想上大概分为以下几步:
1. 获取V层中的登录按钮
2. 使用代码来点击这个按钮
3. 查看C层中的方法是否被触发
接下来,我们分别介绍上述步骤的实现方法。
### 获取V层的登录按钮
在单元测试中,我们可以使用`fixture`来获取组件V层相关的数据,比如可以使用如下代码来获取当前V层对应的`dom`节点:
```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);
});
});
```
来到控制台点击对应的日志内容,对应当前组件的`dom`。
![image-20210303083131752](https://img.kancloud.cn/0b/13/0b137c31425cbba5a85a4298e598ad1c_2566x904.png)
HTML中的每个元素都对应一个对象,该对象被称为**文档对象模型(Document Object Model)**。我们使用JS来操作HTML页面的便是通过操作这个**文档对象模型**。这样一来,相较于传统的直接编写html代码,网页的生成便又多了一种方法:javascript 操作 dom。
获取到组件dom后,我们在根据`dom`知识来获取当前`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);
});
});
```
- 根据实际情况,使用`as`来为变量指定一个类型。 👈
## 使用代码点击登录按钮
使用代码对按钮进行点击非常简单,仅仅需要调用该对象的`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();
});
});
```
此时在控制台中成功的打印了相关日志,说明C层的`onSubmit`方法被成功的触发了,从而证明了绑定是成功的。
![image-20210303084535974](https://img.kancloud.cn/c1/1f/c11f9c292a8c75d8323a60d695312a93_998x204.png)
上述过程中,我们成功的实现了:使用代码来点击**登录按钮**,但在最后的验证环节仍然是使用人眼进行观察的,这仍然没有消除人眼观察的**不可靠性**。
### 验证C层方法被触发
遗憾的是,除了观察,我们是没有办法直接验证某个C层的方法是否被成功的调用的。为了实现这种验证,我们采用:建造模拟C层的方法来间接达到这个目的。
所谓的模拟C层,就是在根据当前的C层,建立一个外表看起来一模一样的C层。原C层有什么方法,我们的模拟C层就会有什么方法;原C层的方法中有什么样的参数,我们的模拟C层的方法中也会有什么参数。
如果我们仅仅是为了验证某一个方法,则还可以在这个方法上安排一个间谍。这像极了我们在电视剧中看到的谍战片。为了获取一手的情况,我们在在敌方的情报部门安排一个间谍。此时,敌方在我方间谍发送信息时,实际的信息却被我方获取了。
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();
});
});
```
此时再次运行单元测试代码,发现控制台的日志不见了:
![image-20210303085859717](https://img.kancloud.cn/47/a7/47a7dd291a2285d71b538b60990ecbad_1024x220.png)
为了近一步确认的确是间谍方法被调用了,我们还可以补充下`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('间谍方法被调用'));
submitButtonElement.click();
});
});
```
![image-20210303090642492](https://img.kancloud.cn/a1/d1/a1d1cc75da6ae3d8da4bc421096a2492_1012x186.png)
最后,我们加入以下验证代码,来间谍来替我们验证`onSubmit`方法的确是被调用了。
```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('间谍方法被调用'));
+ // 点击按钮以前,onSubmit方法应该被调用了0次。
+ expect👈(component.onSubmit).toHaveBeenCalledTimes(0);
submitButtonElement.click();
+ // 点击按钮以后,onSubmit方法应该被调用了1次。
+ expect👈(component.onSubmit).toHaveBeenCalledTimes(1);
});
});
```
**expect**👈代表期望,就是说我们预计组件的方法是被调用了0次或是1次,如果实际的情况与我们预计的相同,则该代码将正确运行;如果实际情况与我们预计的不同,则该处代码将会触发异常。
![image-20210303091229639](https://img.kancloud.cn/0c/c1/0cc147be04505a559a30b4e7716566c8_1244x344.png)
此时,如果我们删除V层中关于触发C层的代码,则会得到如下异常。
![image-20210303091548090](https://img.kancloud.cn/7a/71/7a716ba791bb73fcb318b05a1a771510_1032x152.png)
该异常提示我们:点击了V层的登录按钮后,并没有调用C层的`onSubmit`方法。而这种情况是不正确的。
## 本节作业
尝试删除V层中关于触发C层的代码来触发异常。
| 名称 | 地址 | 备注 |
| ----------------------- | ------------------------------------------------------------ | --------------------------- |
| 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) | 建议将语言切换为English查看 |
| HtmlButtonElement | [https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLButtonElement](https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLButtonElement) | 建议将语言切换为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) | 看不太懂时,再切回中文查看 |
| 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) | |
| 本节源码 | [https://github.com/mengyunzhi/angular11-guild/archive/step3.1.zip](https://github.com/mengyunzhi/angular11-guild/archive/step3.1.zip) | |
- 序言
- 第一章 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 发布部署
- 第九章 总结