同其它组件的开发步骤一致,我们首先按原型进行前台的初始化。然后结合单元测试完成各个功能点的开发。待后台接口准备后(当然也可能是早早的就已经准备好了),对接后台接口以进行集成测试来对功能进行验证。并按验证的结果进行相应前(后)台修正。 打开shell,并来到学生模块,执行`ng g c edit`来生成对应的编辑组件: src/app/student ``` panjiedeMac-Pro:student panjie$ ng g c edit CREATE src/app/student/edit/edit.component.sass (0 bytes) CREATE src/app/student/edit/edit.component.html (19 bytes) CREATE src/app/student/edit/edit.component.spec.ts (614 bytes) CREATE src/app/student/edit/edit.component.ts (262 bytes) UPDATE src/app/student/student.module.ts (754 bytes) ``` angular cli自动生成组件的同时,将该组件同步添加到`student.module.ts`中。 ## V层初始化 V层的初始化与添加学生大同小异: src/app/student/edit/edit.component.html ``` <h2>编辑学生</h2> <form (ngSubmit)="onSubmit()" [formGroup]="formGroup"> <label>姓名:<input name="name" formControlName="name"/></label> <label>学号:<input name="sno" formControlName="sno"/></label> <label>班级:<app-klass-select [klass]="student.klass" (selected)="onSelectKlass($event)"></app-klass-select></label> <button type="submit">保存</button> </form> <a style="display: none" routerLink="./../" #linkToIndex>返回学生列表页</a> ``` > 由于添加与编辑组件具有高度的重合性,所以在有些小的项目中我们也会尝试将`新增`、`编辑`两个功能融合到一个组件中。 ## 单元测试及C层初始化 打开`src/app/student/edit/edit.component.spec.ts`,添加`f`并修正单元测试的描述。 ``` fdescribe('student -> EditComponent', () => { ``` 打开控制台,并使用`ng test`来启动单元测试,接下来我们按单元测试的提示来完善单元测试文件或C层代码 ### Failed: Template parse errors: Can't bind to 'formGroup' since it isn't a known property of 'form'. 译文:失败的:模板解析错误:不能在`form`上绑定`formGroup`,`formGroup`不是`form`元素的已知属性。 原因:formGroup是响应式表单为form添加的属性,单元测试未引入响应式表单,所以产生上述错误。 解决方案: src/app/student/edit/edit.component.spec.ts ``` beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [ EditComponent ], imports: [ ReactiveFormsModule ✚ ] }) .compileComponents(); })); ``` ### Can't bind to 'klass' since it isn't a known property of 'app-klass-select'. 这错误虽然在描述上与上一个相同,但产生的原因却不尽相同。在解析`<app-klass-select [klass]="student.klass"`时, angular把`<app-klass-select`做为普通的dom元素而进行解析,而普通的dom元素并没有`[klass]`属性,因而产生了错误。 解决方案:声明`app-klass-select`对应的组件,这样在模板解析时遇到`<app-klass-select`便会当成组件来进行处理。 src/app/student/edit/edit.component.spec.ts ``` beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [ EditComponent, KlassSelectComponent✚ ], imports: [ ReactiveFormsModule ] }) .compileComponents(); })); ``` ### Can't bind to 'url' since it isn't a known property of 'app-select'. 我们在V层中并没有引用`app-select`,出现该错语的原因是由于`KlassSelectComponent`中引用了`app-select`。但当前测试模块却找到对应解析`app-select`的组件。 解决方案:引入解析`app-select`的组件。该组件被声明于`CoreModule`并由该模块抛出,所以直接`import CoreModule`即拥有了`app-select`正确的解析器: SelectComponent src/app/student/edit/edit.component.spec.ts ``` beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [ EditComponent, KlassSelectComponent ], imports: [ ReactiveFormsModule, CoreModule ✚ ] }) .compileComponents(); })); ``` ### NullInjectorError: StaticInjectorError(DynamicTestModule)[SelectComponent -> HttpClient]: 译为:在SelectComponent中注入HttpClient时发生空注入器错误:静态注入器错误(当前测试模块即:动态的测试模块) 也就是说SelectComponent需要一个能够提供HttpClient的服务,但却没有找到。在非测试环境下,HttpClientModule起到了提供HttpClient的作用,在测试环境下,我们使用HttpClientTestingModule来提供HttpClient以防止其发起真实的HTTP请求。 src/app/student/edit/edit.component.spec.ts ``` beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [ EditComponent, KlassSelectComponent ], imports: [ ReactiveFormsModule, CoreModule, HttpClientTestingModule ✚ ] }) .compileComponents(); })); ``` ### Error: formGroup expects a FormGroup instance 这个错误是说:在V层中用到了formGroup,但该formGroup没有被实例化。该错误报的很详细,不但提醒我们哪错了,还给出了解决问题的示例代码: ``` Error: formGroup expects a FormGroup instance. Please pass one in. Example: <div [formGroup]="myGroup"> <input formControlName="firstName"> </div> In your class: this.myGroup = new FormGroup({ firstName: new FormControl() }); error propert ``` 的确,我们虽然在V层中使用了一些变量、方法,但在C层中却并没有实例化它们。而单元测试则可以协助我们来完成这些变量的实例化,以避免我们经常犯的"不小落掉一个"的错误。 src/app/student/edit/edit.component.ts ```javascript export class EditComponent implements OnInit { formGroup: FormGroup; constructor() { } ngOnInit() { this.formGroup = new FormGroup({ name: new FormControl(''), sno: new FormControl('') }); } } ``` ### TypeError: Cannot read property 'klass' of undefined 但凡出现此类错误的,必然是我们调用了`xxx.klass`而此时xxx的值是undefined。观察V层发现语句:`<app-klass-select [klass]="student.klass"`符合刚才的结论。避免该错误的方法也很简单:初始化student,使其不为undefined src/app/student/edit/edit.component.ts ``` export class EditComponent implements OnInit { formGroup: FormGroup; student: Student = new Student(); ✚ constructor() { } ``` 至此,我们一步步的打造了一个可以测试`编辑学生组件`的如下模块: ![](https://img.kancloud.cn/1e/1a/1e1a36dc506a3fa4b4fa38de474668f0_1046x558.png) 该测试模块`DynamicTestModule`的目标在于调用`createComponent`方法来创建一个可供测试的`编辑学生组件`。在创建`编辑学生组件`的过程中,再依次地创建其依赖的其它组件或服务。而在创建依赖过程中,一旦在本测试模块找不到应的依赖项,则会报相应的注入错误。我们在单元测试模块中,通过使用declarations及imports的方式为测试模块添加`编辑学生组件`所有的依赖项后,单元测试顺利通过,初始化工作完成。 根据上图,我们大胆猜测总结出以下规律: 1. 单元测试模块在创建测试组件时,需要提前准备好该组件所依赖的其它组件及服务。 2. 单元测试模块`DynamicTestModule`是一个模块,学生模块`StudentModule`也是一个模块,它们都是模块的个例,那么推广开来应该能够得出:模块在创建相应的组件时,需要提前准备好该组件所依赖的其它组件及服务。如果未准备好,则会引发相应的依赖错误。 3. 如果某个模块拥有某个组件所依赖的其它组件及服务,则该模块必然拥有创建该组件的能力。 有了以下的总结,让我们再次阅读4.5.10集成测试小节,相信此时再次阅读便轻松多了。只所以出现了`Can't bind to 'formGroup' since it isn't a known property of 'form'.`的错误,是由于`formGroup`指令存在于`ReactiveFormsModule`,而此时的`StudentModule`并没有引用`ReactiveFormsModule`。 # 练一练 按我们总结的规律的第3点:'如果某个模块拥有某个组件所依赖的其它组件及服务,则该模块必然拥有创建该组件的能力。',当前的测试模块不但拥有了创建`编辑学生组件`的能力,同时还拥有了创建`选择班级组件`及`选择组件`的能力。请在单元测试中尝试创建`选择班级组件`及`选择组件`两个组件。并断言其创建成功。 # 参考文档 | 名称 | 链接 | 预计学习时长(分) | | --- | --- | --- | | 源码地址 | [https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step4.7.1](https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step4.7.1) | - |