我们在上个小节中首次使用单元测试来进行组件的单独开发,本节继续使用单元测试来自动完成C层的交互。 # 新建测试用例 在上一个测试用例的下方,模仿建一个新的测试用例。 klass/index/index.component.spec.ts ```js it('测试V层的交互操作', () => { }); ``` * 使用`it(string ,function)`来声明一个测试用例。`string` 是对测试用例的描述,`function`来定义该测试用例的功能。 增加了测试用例后,我们在终端中使用`ng test`来执行测试。 ![](https://img.kancloud.cn/7c/3c/7c3c585475cef99d256c65a03085863a_784x176.png) 测试结果页显示我们的当前的测试用例没有`预测(断言)`信息。 > 断言:我`断`定`说`,一定会发生什么,如果结果与我的断言不相符则报错。 ## 验证双向数据绑定 在班级index组件的V层中,我们使用了这样的代码想达到将C层数据传给(绑定)V层使用的目的: klass/index/index.component.html ``` <form (ngSubmit)="onQuery()"> <label>名称:<input type="text" name="params.name"/></label> ① <button>查询</button> </form> ``` 下面我们使用单元测试来验证数据是否被绑定成功了。同时,为了防止测试用例的执行顺序,我们将当前测试用由`it`变更为`fit`,表示:只执行本测试用例。 klass/index/index.component.spec.ts ``` it('测试V层的交互操作', () => { ✘ fit('测试V层的交互操作', () => { ✚ component.params.name = 'test'; ➊ fixture.detectChanges(); ➋ }); ``` * ➊ 设置查询参数`name`的值为`test`。 * ➋ 当组件发生变化时,重新渲染组件。 #### 测试 ![](https://img.kancloud.cn/81/1b/811b3f0a7e0b533af37bca4e03c90158_352x179.png) V层中的input表单并没有如我们所期望的一般变为`test`。由此可见,我们的数据绑定并没有成功。相信当我们初始化V层的时候,你已经发现了这个前台绑定的错误,在前台进行数据绑定时,我们应该这样: ``` <label>名称:<input type="text" name="params.name"/></label> ✘ <label>名称:<input type="text" name="name" [(ngModel)]="params.name"/></label> ✚ ``` 保存文件后得到如下测试提醒: ``` Failed: Template parse errors: Can't bind to 'ngModel' since it isn't a known property of 'input'. ("<form (ngSubmit)="onQuery()"> <label>名称:<input type="text" name="name" [ERROR ->][(ngModel)]="params.name"/></label> <button>查询</button> </form> "): ng:///DynamicTestModule/IndexComponent.html@1:43 ``` 看到该错误,相信你一定有种似曾相识的感觉,这是因为该错误已经不是首次出现了。错误产生的原因是:`NgModel`指令属于`FormModule`,而我们当前测试未引入`FormModule`,进而报错。其实该错误与前面的`no provider for HttpClient 未找到httpClient的提供者`的错误性质是一样的。在C中引入`HttpClient`则需要引用`HttpClient`所在的模块`HttpModule`,在V层中使用`ngModel`指令,则需要引用`ngModel`指令所在的模块`FormsModule`。修正的方法如下: klass/index/index.component.spec.ts ``` import {FormsModule} from '@angular/forms'; beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [IndexComponent], imports: [HttpClientTestingModule, FormsModule] ① }) .compileComponents(); })); ``` 此时再查看运行的结果: ![](https://img.kancloud.cn/ba/b9/bab9f478df9ca4c5017d8c59340aeb03_369x163.png) ### 验证V层向C层绑定数据 刚刚我们验证了C层向V层的数据绑定,接下来继续验证V层向C层的数据绑定。 我们将上个用例的`fit`修改为`it`,然后新建一个`fit`测试用例方法。 klass/index/index.component.spec.ts ``` fit('测试V层向C层绑定', () => { fixture.whenStable().then(() => { ➊ }); }); ``` * ➊ 由于V层的渲染需要的一定的时间(很短,肉眼感受不到),所以在进行V层的一些操作时,我们使用`whenStable().then(function)`来等待V层渲染完毕。 在Angular测试中,我们使用如下方法设置`input`的值: klass/index/index.component.spec.ts ``` import {By} from '@angular/platform-browser'; ① fit('测试V层向C层绑定', () => { fixture.whenStable().then(() => { const debugElement = fixture.debugElement; ➊ const nameInputElement = debugElement.query(By.css('input[name="name"]')); ➋ const nameInput: HTMLInputElement = nameInputElement.nativeElement; ➌ nameInput.value = 'test1'; ➍ nameInput.dispatchEvent(new Event('input')); ➎ console.log(component.params.name); ➏ }); }); ``` * ➊ 获取**所有**供测试的V层元素。 * ➋ 通过CSS选择器来查询出具体的V层的元素。 * ➌ 将获取到的元素转换为`HTMLInputElement`。 * ➍ 设置预输入表单的值。 * ➎ Angular 不知道我们设置了这个 <input> 元素的 value 属性。 在通过调用 dispatchEvent() 触发了该输入框的 input 事件之前,它不能读到那个值。 * ➏ 查看是否成功的绑定到C层。 > 只所以要经过➊➋➌三步操作是由于:Angular 依赖于`[DebugElement](https://www.angular.cn/api/core/DebugElement)`这个抽象层,就可以安全的横跨*其支持的所有平台*。 Angular 不再创建 HTML 元素树,而是创建`[DebugElement](https://www.angular.cn/api/core/DebugElement)`树,其中包裹着相应运行平台上的*原生元素*。`nativeElement`属性会解开`[DebugElement](https://www.angular.cn/api/core/DebugElement)`,并返回平台相关的元素对象。简单的原因就是:由于angular要安全的支持所有平台。 测试结果: ``` Chrome 78.0.3904 (Mac OS X 10.13.6): Executed 0 of 6 SUCCESS (0 secs / 0 secs) ERROR: 'Spec 'IndexComponent 测试V层向C层绑定' has no expectations.' Chrome 78.0.3904 (Mac OS X 10.13.6): Executed 0 of 6 SUCCESS (0 secs / 0 secs) LOG: 'test1' Chrome 78.0.3904 (Mac OS X 10.13.6): Executed 1 of 6 SUCCESS (0 secs / 0.114 secs) Chrome 78.0.3904 (Mac OS X 10.13.6): Executed 1 of 6 (skipped 5) SUCCESS (0.137 secs / 0.114 secs) TOTAL: 1 SUCCESS TOTAL: 1 SUCCESS ``` ![](https://img.kancloud.cn/e9/ad/e9adcb8a1438fbab3b63cfda9aed7d75_952x676.png) ## 断言 我们刚刚在控制台中确认了数据被成功的绑定。但这种确认的方法并不可靠,随着项目推进、需求变更、团队人员增加等因素,我们需要越来越多的主观判断来保证历史的功能仍在正常运行。试想下,我们当前的项目有了3个测试用例,如果人为的来确认这3个测试用例是正确的,则需要逐个将`it`方法变更为`fit`方法,然后观察界面或控制台来确认组件的正确性。在团队开发中,往往是由于成员A变更了自己的功能而导致成员B的功能出现错误。也就是说如果想保证项目的质量,那么成员A在变更了自己的任意代码后都应该把项目中所有的单元测试跑一遍,用眼睛来确认一便来确保自己的变更没有对其它的模块产生影响。这是不现实的。 单元测试的断言很好的解决了此问题,每个预期的结果我们都写在单元测试的代码中,当其它的成员进行增量开发或是修改时,如果不小心影响了我们的功能,那么此时单元测试会自动告知他。这样就使得在大型的团队合作开发的项目中,不会因个别代码的变动而影响其它模块的功能了。 `断言` = `断定说`,还可以理解为`我预测`、`我期望..`等。下面,我们来逐步增加断言进而完成前面的单元测试。 klass/index/index.component.spec.ts ``` fit('测试V层向C层绑定', () => { expect(component).toBeTruthy(); ① ✚ fixture.whenStable().then(() => { const debugElement: DebugElement = fixture.debugElement; const nameInputElement = debugElement.query(By.css('input[name="name"]')); const nameInput: HTMLInputElement = nameInputElement.nativeElement; nameInput.value = 'test1'; nameInput.dispatchEvent(new Event('input')); console.log(component.params.name); ✘ expect(component.params.name).toBe('test1'); ➊ ``` * ① 防止报没有任何期望的错误 * ➊ 期望`component.params.name`的值为`test1` 如果我们将`expect(component.params.name).toBe('test1');`变更为`expect(component.params.name).toBe('test2');`,则会得到如下错误:·· ``` Expected 'test1' to be 'test2'. ``` 它是在说:我们期望的是`test2`,但是得到的是`test1`,这与我们规定的期望不致。有了这个期望后,当其它的成员或几天后的自己不小心犯错后单元测试便会自动告知我们了。 ### 修正其它的断言 将此测试用例方法由`fit`变更为`it`后,我们为其它的两个测试用例增加期望。 ``` it('should create', () => { expect(component).toBeTruthy(); const req = httpTestingController.expectOne('http://localhost:8080/Klass?name='); const klasses = [ new Klass(1, '计科1901班', new Teacher(1, 'zhagnsan', '张三')), new Klass(2, '软件1902班', new Teacher(2, 'lisi', '李四')) ]; req.flush(klasses); fixture.detectChanges(); fixture.whenStable().then(() => { const debugElement: DebugElement = fixture.debugElement; const tableElement = debugElement.query(By.css('table')); ➊ const nameInput: HTMLTableElement = tableElement.nativeElement; ➋ expect(nameInput.rows.length).toBe(3); ➌ expect(nameInput.rows.item(1).cells.item(1).innerText).toBe('计科1901班'); ➍ expect(nameInput.rows.item(1).cells.item(2).innerText).toBe('张三'); ➎ }); }); it('测试V层的交互操作', () => { component.params.name = 'test'; fixture.detectChanges(); fixture.whenStable().then(() => { const debugElement: DebugElement = fixture.debugElement; const nameInputElement = debugElement.query(By.css('input[name="name"]')); const nameInput: HTMLInputElement = nameInputElement.nativeElement; expect(nameInput.value).toBe('test'); }); }); ``` * ➊➋ 查找table元素。 * ➌ 断言table中有3行信息:表头1行+数据2行。 * ➍ 断言第1行第1列的值为`计科1901班`。 * ➎ 断言第1行第2列的值为`张三`。 > 在实际的开发中,我们可能不会对数据绑定这种较常规的点进行测试。此小节对双向数据绑定进行了测试,目的在抛砖引玉为以后较复杂的组件测试做基础积累。 # 参考文档 | 名称 | 链接 | 预计学习时长(分) | | --- | --- | --- | | 源码地址 | [https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step3.2.6](https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step3.2.6) | - | | 组件DOM测试 | [https://www.angular.cn/guide/testing#component-dom-testing](https://www.angular.cn/guide/testing#component-dom-testing) | 15 | | CSS选择器 | [https://www.w3school.com.cn/cssref/css\_selectors.asp](https://www.w3school.com.cn/cssref/css_selectors.asp) | 10 | | DebugElement | [https://www.angular.cn/api/core/DebugElement](https://www.angular.cn/api/core/DebugElement) | 5 | | HTMLTableElement | [https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLTableElement](https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLTableElement) | 5 | |HTMLInputElement | [https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLInputElement](https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLInputElement) | 5 | | 使用`[dispatchEvent()](https://www.angular.cn/)`修改输入值 | [https://www.angular.cn/guide/testing#change-an-input-value-with-dispatchevent](https://www.angular.cn/guide/testing#change-an-input-value-with-dispatchevent) | 5 |