今天我们要来为我们用 Reactive Forms 所撰写的登入系统写单元测试,如果还没有相关程序码的朋友,赶快前往阅读第三天的文章: Reactive Forms 实作 - 以登入为例。
前置作业基本上都跟第六天的文章:单元测试实作 - 登入系统 by Template Driven Forms 相同,今天就不会再赘述,大家如果忘记怎麽做可以先回去复习一下。
目前的程序码:
export class AppComponent {
formGroup: FormGroup | undefined;
get accountControl(): FormControl {
return this.formGroup!.get('account') as FormControl;
}
get passwordControl(): FormControl {
return this.formGroup!.get('password') as FormControl;
}
constructor(private formBuilder: FormBuilder) {}
ngOnInit(): void {
this.formGroup = this.formBuilder.group({
account: [
'',
[
Validators.required,
Validators.pattern(/^\b[\w\.-]+@[\w\.-]+\.\w{2,4}\b$/gi)
]
],
password: [
'',
[Validators.required, Validators.minLength(8), Validators.maxLength(16)]
]
});
}
getErrorMessage(formControl: FormControl): string {
let errorMessage = '';
if (!formControl.errors || formControl.pristine) {
errorMessage = '';
} else if (formControl.errors.required) {
errorMessage = '此栏位必填';
} else if (formControl.errors.pattern) {
errorMessage = '格式有误,请重新输入';
} else if (formControl.errors.minlength) {
errorMessage = '密码长度最短不得低於8码';
} else if (formControl.errors.maxlength) {
errorMessage = '密码长度最长不得超过16码';
}
return errorMessage;
}
login(): void {
// do login...
}
}
以目前的程序码来看,基本上我们只要验 getErrorMessage
这个函式,不过我们其实也能验 ngOnInit
这个 Angular Component Lifecycle Hook 的执行结果,毕竟它也是个函式,我们一样可以写测试去验证这个函式的执行结果是否符合我们的预期。
关於 Angular Component Lifecycle Hook ,如果想知道更多可以阅读官方文件: Component Lifecycle hooks
我们一样先加一个 describe
,表明在这里面的测试案例都是在测 getErrorMessage
这个函式:
describe('AppComponent', () => {
// ...
describe('getErrorMessage', () => {
// 这里面的测试案例都是要测这个函式
});
});
接着统整一下这个 getErrorMessage
的函式里会遇到的情况:
formControl
里没有任何 error
,则会取得空字串。formControl
的属性 pristine
的值为 true
,则会取得空字串。formControl
里有必填的错误: required
,则会取得错误讯息 此栏位必填
。formControl
里有格式的错误: pattern
,则会取得错误讯息 格式有误,请重新输入
。formControl
里有最小长度的错误: minlength
,则会取得错误讯息 密码长度最短不得低於8码
。formControl
里有最大长度的错误: maxlength
,则会取得错误讯息 密码长度最长不得超过16码
。统整完之後,就可以将上述情况写成测试案例:
describe('getErrorMessage', () => {
it('should get empty string when the value is correct', () => {
// Arrange
const formControl = new FormControl('');
const expectedMessage = '';
// Act
const message = component.getErrorMessage(formControl);
// Assert
expect(message).toBe(expectedMessage);
});
it('should get empty string when the value is empty string but the form control is pristine', () => {
// Arrange
const formControl = new FormControl('', [Validators.required]);
const expectedMessage = '';
// Act
const message = component.getErrorMessage(formControl);
// Assert
expect(message).toBe(expectedMessage);
});
it('should get "此栏位必填" when the value is empty string but the form control', () => {
// Arrange
const formControl = new FormControl('', [Validators.required]);
const expectedMessage = '此栏位必填';
// Act
formControl.markAsDirty();
const message = component.getErrorMessage(formControl);
// Assert
expect(message).toBe(expectedMessage);
});
it('should get "格式有误,请重新输入" when the value is empty string but the form control', () => {
// Arrange
const formControl = new FormControl('whatever', [Validators.pattern('/^\b[\w\.-]+@[\w\.-]+\.\w{2,4}\b$/gi')]);
const expectedMessage = '格式有误,请重新输入';
// Act
formControl.markAsDirty();
const message = component.getErrorMessage(formControl);
// Assert
expect(message).toBe(expectedMessage);
});
it('should get "密码长度最短不得低於8码" when the value is empty string but the form control', () => {
// Arrange
const formControl = new FormControl('abc', [Validators.minLength(8)]);
const expectedMessage = '密码长度最短不得低於8码';
// Act
formControl.markAsDirty();
const message = component.getErrorMessage(formControl);
// Assert
expect(message).toBe(expectedMessage);
});
it('should get "密码长度最长不得超过16码" when the value is empty string but the form control', () => {
// Arrange
const formControl = new FormControl('12345678901234567', [Validators.maxLength(16)]);
const expectedMessage = '密码长度最长不得超过16码';
// Act
formControl.markAsDirty();
const message = component.getErrorMessage(formControl);
// Assert
expect(message).toBe(expectedMessage);
});
});
从上面的程序码中可以看出,我这次写单元测试的策略是:让每个案例自己配置足以验证该案例的 formControl
与其必须的 Validators
即可。
也就是说,当我需要验证 此栏位必填
的错误讯息时,我只需要配置 Validators.required
给 formControl
;当我需要验证 密码长度最短不得低於8码
的错误讯息时,我只需要配置 Validators.minlength(8)
给 formControl
,依此类推。
会这样写是因为我们只需要专注在什麽样子的 errors
会得到什麽样子的错误讯息上面,当然大家也可以每次都帮 formControl
配置最完整的 Validators
,这两个方法我觉得都可以。
此外,由於我们这次有判断 formControl
的状态: pristine
,因此在写测试的时候要特别留意,记得要先 markAsDirty
之後才能测试噢!
上一次写单元测试的文章: 单元测试实作 - 登入系统 by Template Driven Forms。
测试结果:
再来是 ngOnInit
的部份, ngOnInit
要验证的项目跟 formGroup
满相关,所以我打算用 formGroup
当测试集合的名称,具体要验证的项目有:
ngOnInit
执行之前, formGroup
是 undefined
的状况。ngOnInit
执行之後,
formGroup
是类型为 FormGroup
的实体。formGroup
里要有两个 FormControl
。
accountFormControl
passwordFormControl
程序码如下:
describe('formGroup', () => {
it('should be undefined before init', () => {
// Assert
expect(component.formGroup).toBeFalsy();
});
describe('after ngInit', () => {
beforeEach(() => {
fixture.detectChanges();
});
it('should be instance of FormGroup', () => {
// Assert
expect(component.formGroup).toBeInstanceOf(FormGroup);
});
it('should have 2 form controls', () => {
// Arrange
const formControls = component.formGroup!.controls;
const controlLength = Object.keys(formControls).length;
// Assert
expect(controlLength).toBe(2);
});
describe('accountFormControl', () => {
it('should have the required validator', () => {
// Arrange
const error = component.accountControl.errors!;
// Assert
expect(error.required).toBe(true);
});
it('should have the email pattern validator', () => {
// Arrange
component.accountControl.setValue('abc');
const error = component.accountControl.errors!;
const expectedPattern = '/^\\b[\\w\\.-]+@[\\w\\.-]+\\.\\w{2,4}\\b$/gi';
// Assert
expect(error.pattern.requiredPattern).toBe(expectedPattern);
});
});
describe('passwordFormControl', () => {
it('should have the required validator', () => {
// Arrange
const error = component.accountControl.errors!;
// Assert
expect(error.required).toBe(true);
});
it('should have the min-length validator', () => {
// Arrange
component.passwordControl.setValue('abc');
const error = component.passwordControl.errors!;
// Assert
expect(error.minlength.requiredLength).toBe(8);
});
it('should have the max-length validator', () => {
// Arrange
component.passwordControl.setValue('12345678901234567');
const error = component.passwordControl.errors!;
// Assert
expect(error.maxlength.requiredLength).toBe(16);
});
});
});
});
此处比较特别的地方是,我在 after ngInit
的 beforeEach
里是用 fixture.detectChanges()
来触发 ngOnInit()
,而不是使用 component.ngOnInit()
的方式来触发,这是因为我认为我们在写的是 Angular ,而这个 Lifecycle Hook 又是 Angular 的东西,所以使用 Angular 的机制来触发会比直接使用该函式触发来的好。
当然也是可以直接使用 component.ngOnInit()
来触发,在测试的验证结果上其实不会有什麽不同,所以用哪个方式其实都可以。
测试结果:
已经写了两次的测试,相信大家对於测试的熟悉度已经有显着地提昇,而今天的重点主要会是在使用 FormControl
markAsDirty
来改变栏位的状态,以及了解 fixture.detectChanges
与 ngOnInit
的关系,未来在写测试的时候,这两点也是非常需要多加留意的。
今日的实作程序码一样会放在 Github - Branch: day8 上供大家参考,建议大家在看我的实作之前,先按照需求规格自己做一遍,之後再跟我的对照,看看自己的实作跟我的实作不同的地方在哪里、有什麽好处与坏处,如此反覆咀嚼消化後,我相信你一定可以进步地非常快!
如果有任何的问题或是回馈,也都非常欢迎留言给我让我知道噢!
<<: [Day 15] Drone - Runner in k8s 安装设定
类别继承 定义:可以继承父类别所有的公有方法与属性,在子类别就不用重新设计 基於保护原则,外部是不...
这是一篇有用的工具文。 日常生活中,我们不可避免会碰上误删,误格式化硬碟/外接硬碟/记忆卡亦或是随身...
开始打程序前,网页设计是不可或缺的环节! 网站介面设计,称为UIUX设计 UI指使用者介面(User...
今天我们要实作 Signaling server 的部分: 建立文件 # 进入要放专案的路径 ❯ c...
今日题目:13. Roman to Integer(Easy) Roman numerals are...