本节继续前一小节的单元测试,完成提交数据的单元测试部分。 # 初始化 在编写代码前先写清自己编写代码的功能以及实现功能的步骤是个非常好的开发习惯,这将使你在处理复杂的问题的时候显得得心应手: klass/edit/edit.component.spec.ts ``` /** * 数据更新测试,步骤: * 1. 设置路由参数 * 2. 输入input的值 * 3. 点击提交扭钮:断言向预期的地址以对应的方法提交了表单中的数据 * 4. 断言跳转到''路由地址 */ fit('更新数据测试', () => { expect(component).toBeTruthy(); }); ``` ## 造轮子 测试方法需要设置input的值以值点击对应的提交按钮,由于此方法前面我们使用过,所以确定为重复的方法,故在testing/FormTest.ts中新造轮子如下: ``` /** * 设置input的值 * @param fixture 夹具 * @param cssSelector CSS选择器 * @param value 要设置的值 * @return 成功true 失败false */ static setInputValue(fixture: ComponentFixture<any>, cssSelector: string, value: string): boolean { const selectorElement = this.getSelectorElement(fixture, cssSelector); if (isNull(selectorElement)) { return false; } const htmlInputElement: HTMLInputElement = selectorElement.nativeElement; htmlInputElement.value = value; htmlInputElement.dispatchEvent(new Event('input')); return true; } /** * 获取button按钮,并点击 * @param fixture 夹具 * @param cssSelector CSS选择器 * @return 成功true 失败false */ static clickButton(fixture: ComponentFixture<any>, cssSelector: string): boolean { const selectorElement = this.getSelectorElement(fixture, cssSelector); if (isNull(selectorElement)) { return false; } const htmlButtonElement: HTMLButtonElement = selectorElement.nativeElement; htmlButtonElement.click(); return true; } /** * 根据CSS选择器来获取夹具中Debug元素 * @param fixture 夹具 * @param cssSelector CSS选择器 * @return Debug元素 */ static getSelectorElement(fixture: ComponentFixture<any>, cssSelector: string): DebugElement { const debugElement: DebugElement = fixture.debugElement; return debugElement.query(By.css(cssSelector)); } ``` 在造上述轮子的时候我们发现两个轮子中也存在同样的方法,于是在轮子中又剥离中一个新轮子`getSelectorElement`。 ## 完善测试 我们添加一个onSubmitTest方法用于测试更新操作,由于更新操作依赖于数据获取操作,所以该方法在数据成功获取后调用: ``` const testGetHttp = (id: number) => { ... fixture.whenStable().then(() => { ... onSubmitTest(1); ① }); }; /** * 数据更新测试,步骤: * 1. 设置路由参数 * 2. 输入input的值 * 3. 点击提交扭钮:断言向预期的地址以对应的方法提交了表单中的数据 * 4. 断言跳转到''路由地址 */ const onSubmitTest = (id: number) => { FormTest.setInputValue(fixture, '#name', '测试更新班级'); FormTest.setInputValue(fixture, '#teacherId', '100'); fixture.whenStable().then(() => { FormTest.clickButton(fixture, 'button'); const httpTestController: HttpTestingController = TestBed.get(HttpTestingController); const req = httpTestController.expectOne(`http://localhost:8080/Klass/${id}`); expect(req.request.method).toEqual('PUT'); const klass: Klass = req.request.body.valueOf(); expect(klass.name).toEqual('测试更新班级'); expect(klass.teacher.id).toEqual(100); req.flush(null, {status: 204, statusText: 'No Content'}); httpTestController.verify(); ➊ }); }; ``` * ① 获取要编辑的数据后,开始测试编辑数据 * ➊ httpTestController.verify(); 来保证所有的http请求都是符合我们的预期的。在执行此方法时如果还有其它的http请求,则会在控制台中打印异常信息。 由于一些异常仅仅会打印在控制台中而未反馈到测试界面上,所以我们在测试的过程中应该:**轻界面重控制台** # 测试间谍SPY 我们看似完成了所有的操作,但来到klass/edit/edit.component.ts中却发现我们并不能够确认这行代码是否成功的被执行了: klass/edit/edit.component.ts ``` /** * 用户提交时执行的操作 */ onSubmit(): void { const data = { name: this.formGroup.value.name, teacher: {id: this.formGroup.value.teacherId} }; this.httpClient.put(this.getUrl(), data) .subscribe(() => { this.router.navigateByUrl('', {relativeTo: this.route}); ① }, () => { console.error(`在${this.getUrl()}上的PUT请求发生错误`); }); } ``` * ① 此行代码是否成功的触发我们并不清楚 要测试该方法是否成功的被执行了,我们使用上个小节刚刚讲过的方法。即:模拟一个和this.route一样具有`navigateByUrl`方法的服务,然后在`providers`中声明使用该模拟服务来替换原服务,最终我们去测试模拟服务中该方法是否被触发了。比如: router-stub.ts ``` import {ActivatedRoute} from '@angular/router'; export class RouterStub { path: string; config: {relativeTo: ActivatedRoute}; navigateByUrl(path: string, config: {relativeTo: ActivatedRoute}): void { this.path = path; this.config = config; } } ``` 然后我们使用`providers`来注入此`RouterStud`来替换原来的`Router`。最终我们来断言`RouterStud`中的`path`是否为`''`。为`''`则说明执行了此方法;仍然是`undefined`则说明未执行此方法。 klass/edit/edit.component.ts ``` providers: [ {provide: ActivatedRoute, useClass: ActivatedRouteStub}, {provide: Router, useClass: RouterStub} ] ... req.flush(null, {status: 204, statusText: 'No Content'}); httpTestController.verify(); const router: RouterStub = TestBed.get(Router); ✚ expect(router.path).toEqual(''); ✚ }); ``` 除此以外angular来为我们提供了更为简单的测试方法 ---- 测试间谍 ## 生成间谍 间谍的作用同我们刚刚建立的`stub`模拟服务一样,都是向测试的组件提供一个覆盖原服务的测试服务。但测试间谍的生成方法更加的简单,比如我们在使用`spy`方法来生成一个Router测试服务,则只需要:`const routerSpy = jasmine.createSpyObj<Router>('Router', ['navigateByUrl']);`即可。 它表示:创建一个拥有`navigateByUrl`方法的模拟`Router`的间谍服务。有了这个服务,我们直接将其添加到`providers`中即可: klass/edit/edit.component.ts ``` beforeEach(async(() => { const routerSpy = jasmine.createSpyObj<Router➋>('Router', ['navigateByUrl']); TestBed.configureTestingModule({ declarations: [EditComponent], imports: [ ReactiveFormsModule, HttpClientTestingModule, RouterTestingModule ], providers: [ {provide: ActivatedRoute, useClass: ActivatedRouteStub}, {provide: Router, useValue➊: routerSpy} ] }) .compileComponents(); })); ``` * ➊ 在使用RouterStub时,我们使用关键字`useClass`来表示:当组件需要具有Router功能的对象(服务)时,使用RouterStub`类`来实例化一个对象给它。 * ➊ 在使用routerSpy时,我们我们使用关键字`useValue`来表示 :当组件需要具有Router功能的对象(服务)时,将routerSpy的`值`给它。 * ➋ 使用`<Router>`表示该间谍的根据`Router`生成的并用于模拟`Router`功能的。 ## 使用间谍断言 klass/edit/edit.component.ts ``` /** * 数据更新测试,步骤: * 1. 设置路由参数 * 2. 输入input的值 * 3. 点击提交扭钮:断言向预期的地址以对应的方法提交了表单中的数据 * 4. 断言跳转到''路由地址 */ const onSubmitTest = (id: number) => { FormTest.setInputValue(fixture, '#name', '测试更新班级'); FormTest.setInputValue(fixture, '#teacherId', '100'); fixture.whenStable().then(() => { FormTest.clickButton(fixture, 'button'); const httpTestController: HttpTestingController = TestBed.get(HttpTestingController); const req = httpTestController.expectOne(`http://localhost:8080/Klass/${id}`); expect(req.request.method).toEqual('PUT'); const klass: Klass = req.request.body.valueOf(); expect(klass.name).toEqual('测试更新班级'); expect(klass.teacher.id).toEqual(100); const routerSpy: SpyObj<Router> = TestBed.get(Router); ➊ expect(routerSpy.navigateByUrl.calls.any()).toBe(false); ➋ req.flush(null, {status: 204, statusText: 'No Content'}); expect(routerSpy.navigateByUrl.calls.any()).toBe(true); ➌ httpTestController.verify(); }); }; ``` * ➊ 获取我们在providers中声明的spy对象。 * ➋ 断言:该间谍服务的navigateByUrl从未调用过。 * ➌ 断言:该间谍服务的navigateByUrl已经被调用过了。 至此,我们在间谍的帮助下快速的完成了编辑组件提交的测试工作。 # 参考文档 | 名称 | 链接 | 预计学习时长(分) | | --- | --- | --- | | 源码地址 | [https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step3.4.4](https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step3.4.4) | - | | 使用间谍(spy)进行测试 | [https://www.angular.cn/guide/testing#testing-with-a-spy](https://www.angular.cn/guide/testing#testing-with-a-spy) | | Spy | [https://jasmine.github.io/api/3.3/Spy.html](https://jasmine.github.io/api/3.3/Spy.html) | | | Spy calls | [https://jasmine.github.io/api/3.3/Spy\_calls.html](https://jasmine.github.io/api/3.3/Spy_calls.html) | | 依赖提供商 | [https://www.angular.cn/guide/dependency-injection-providers#dependency-providers](https://www.angular.cn/guide/dependency-injection-providers#dependency-providers) | 15 |