本节完成用户登录组件的功能设计,与前面开发的其它组件相同:在C层中完成与V层的交互工作,在M层中完成与后台的对接部分,并借助单元测试最终完成开发。 # CV层交互 V层中有两项交互的内容:表单信息及提交按钮。 ## 表单信息 表单信息的交互采用更加面向对象的formGroup,代码如下: src/app/login/login.component.ts ```javascript formGroup: FormGroup; constructor() { } ngOnInit() { this.formGroup = new FormGroup({ username: new FormControl(''), password: new FormControl('') }); } ``` src/app/login/login.component.html ```html <form [formGroup]="formGroup"✚> <input type="text" class="form-control" id="username" aria-describedby="usernameHelp" placeholder="用户名" formControlName="username"✚> <input type="password" class="form-control" id="password" placeholder="密码" formControlName="password"✚> ``` ### 测试 src/app/login/login.component.spec.ts ```javascript fit('表单绑定', () => { // 设置C层的值后重新渲染V层 component.formGroup.get('username').setValue('testUsername'); component.formGroup.get('password').setValue('testPassword'); fixture.detectChanges(); // 获取V层的值 const usernameValue = FormTest.getInputValueByFixtureAndCss(fixture, '#username'); const passwordValue = FormTest.getInputValueByFixtureAndCss(fixture, '#password'); // 断言CV两层的值相等 expect(usernameValue).toEqual('testUsername'); expect(passwordValue).toEqual('testPassword'); }); ``` 测试结果: ``` Failed: Template parse errors: Can't bind to 'formGroup' since it isn't a known property of 'form'. ("<div class="row justify-content-center"> <div class="col-4"> <form [ERROR ->][formGroup]="formGroup"> <div class="form-group"> <label for="username">用户名</label> "): ng:///DynamicTestModule/LoginComponent.html@2:10 ``` 提示说:不认识formGroup,这是由于formFroup存在于ReactiveFormsModule中,修正如下: src/app/login/login.component.spec.ts ```javascript beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [ LoginComponent ], imports: [ ✚ ReactiveFormsModule ✚ ] ✚ }) .compileComponents(); })); ``` 再次运行测试通过,且用户名密码被填充至相应的input中: ![](https://img.kancloud.cn/4f/6a/4f6a5d88cc5a571b005a117d1f935d86_446x250.png) ## 提交按钮 点击提交按钮时,应该触发C层相应的方法,设定方法名为:onSubmit。 src/app/login/login.component.html ```html <form [formGroup]="formGroup" (ngSubmit)="onSubmit()"> ``` src/app/login/login.component.ts ```javascript onSubmit() { } ``` ### 单元测试 src/app/login/login.component.spec.ts ```javascript fit('点击提交按钮', () => { spyOn(component, 'onSubmit'); FormTest.clickButton(fixture, 'button'); expect(component.onSubmit).toHaveBeenCalled(); }); ``` 组件的CV层对接测试完毕后,开始进行CM层的对接测试。 # CM层对接 由于M层最终需要对后台的数据进行请求,所以到了制定前台后对接规范的时候了。用户提交用户名密码时,如果用户名密码正确,则返回true;如果用户名密码不正确,则返回false。定制接口规范如下: ``` POST /Teacher/login ``` #### 参数 Parameters | type | name | Description | Schema | | --- | --- | --- | --- | | **Body** | **用户名密码** <br> *requried* | 登录教师 | {username: 用户名, password: 密码} | #### 返回值 Responses | HTTP Code | Description | Schema | | --- | --- | --- | | **200** | Ok | 用户密码是否正确:正确,true; 不正确, false | 定制相应的时序图如下: ![](https://img.kancloud.cn/b1/a7/b1a73eac38cffb5b79f084c6ed11cefa_377x101.png) ## M层开发 按由后向前的顺序,先进行M层的开发。来到src/app/service文件夹,新建service及对应的单元测试文件: ``` panjiedeMac-Pro:service panjie$ ng g s teacher CREATE src/app/service/teacher.service.spec.ts (338 bytes) CREATE src/app/service/teacher.service.ts (136 bytes) ``` 添加login方法: src/app/service/teacher.service.ts ```javascript import { Injectable } from '@angular/core'; import {Observable} from 'rxjs'; @Injectable({ providedIn: 'root' }) export class TeacherService { constructor() { } /** * 用户登录 * @param username 用户名 * @param password 密码 * @return 登录成功:true; 登录失败: false。 */ login(username: string, password: string): Observable<boolean> { return null; } } ``` 完善功能 src/app/service/teacher.service.ts ```javascript export class TeacherService { constructor(private httpClient: HttpClient) { } /** * 用户登录 * @param username 用户名 * @param password 密码 * @return 登录成功:true; 登录失败: false。 */ login(username: string, password: string): Observable<boolean> { const url = 'http://localhost:8080/Teacher/login'; return this.httpClient.post<boolean>({username, password}); } } ``` ### 单元测试 此类的单元测试已经做过很多遍了: src/app/service/teacher.service.spec.ts ```javascript beforeEach(() => TestBed.configureTestingModule({ imports: [ HttpClientTestingModule ] })); ... fit('login', () => { // 获取service实例 const service: TeacherService = TestBed.get(TeacherService); // 准备接收值,调用login方法并订阅以使其发起请求 let result: boolean; service.login('username', 'password').subscribe(value => { result = value; }); // 获取请求信息,并断言请求地址、方法、请求的值符合预期 const httpTestingController: HttpTestingController = TestBed.get(HttpTestingController); const req = httpTestingController.expectOne('http://localhost:8080/Teacher/login'); expect(req.request.method).toEqual('POST'); const usernameAndPassword: { username: string, password: string } = req.request.body.valueOf(); ➊ expect(usernameAndPassword.username).toEqual('username'); expect(usernameAndPassword.password).toEqual('password'); // 模拟返回请求值,断言在订阅中接收到了该值 req.flush('true'); ➊ expect(result).toBeTruthy(); }); ``` * ➊ 使用valueOf()时,应该规定获取后的变量类型。比如此处为: `{ username: string, password: string }` * ➋ 此处应该使用`req.flush('true');`,而非`req.flush(true);` ## C层开发 C层的功能可以描述为:获取用户输入的用户名、密码信息,然后将用户名密码信息发送到M层,对应代码如下: src/app/login/login.component.ts ```javascript /** * 点击提交按钮后进行用户登录 */ onSubmit() { const username = this.formGroup.get('username').value; const password = this.formGroup.get('password').value; this.teacherService.login(username, password).subscribe(result => { console.log(result); }); } ``` ### 单元测试 src/app/login/login.component.spec.ts ```javascript imports: [ ReactiveFormsModule, HttpClientTestingModule ✚ ] fit('onSubmit', () => { // 获取teacherService实例,并为其login方法设置替身 const teacherService = TestBed.get(TeacherService) as TeacherService; spyOn(teacherService, 'login').and.returnValue(of(true)); // 添加测试数据并调用 component.formGroup.get('username').setValue('testUsername'); component.formGroup.get('password').setValue('testPassword'); component.onSubmit(); // 断言成功调用teacherService的login方法 expect(teacherService.login).toHaveBeenCalledWith('testUsername', 'testPassword'); }); ``` 测试通过 ### 测试console.log 刚刚的测试虽然断言了调用的`teacherService.login`时传入的参数是符合预期的,但是并没有对`onSubmit`是否成功的接收到了`teacherService.login`的返回值进行断言。在`login.component.ts`中使用了`console.log(result);`在控制台打印了返回值,如何该断言`console.log`语句成功执行了并且是以`teacher.login`发送的值执行的呢?若要实现断言`console.log`,则需要为其制作替身,而制作替身的方法我们已经熟练使用了,代码为:`spyOn(对象, 方法)`,按此思想为`console`对象制作替身如下: src/app/login/login.component.spec.ts ```javascript fit('onSubmit', () => { // 获取teacherService实例,并为其login方法设置替身 const teacherService = TestBed.get(TeacherService) as TeacherService; spyOn(teacherService, 'login').and.returnValue(of(true)); 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(console.log).toHaveBeenCalledWith(true); ✚ }); ``` 单元测试通过。 # 参考文档 | 名称 | 链接 | 预计学习时长(分) | | --- | --- | --- | | 源码地址 | [https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step5.1.2](https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step5.1.2) | - |