Angular自定义异步验证器单元测试通过 但expect断言疑似未执行
核心问题1:取值逻辑错位
你的验证器正式代码中,读取已存在域名列表的逻辑是访问验证器类实例自身的属性:
if (this.domains.indexOf(input.value) > -1) { return of({ domainTakenInList: true }); }
但你写测试的时候,把domains数组挂在了传入的FormControl对象上:
const control = { value: 'google.com', domains: ['mozilla.com', 'google.com'] };
测试执行时,验证器根本不会去读control上的domains属性,只会找自己实例上的this.domains——而你测试里初始化验证器的方式是new DomainUniqueValidator(),这个实例没有手动给domains属性赋值,走到这段判断时直接抛出Cannot read properties of undefined (reading 'indexOf')的运行时错误,Observable流直接中断,自然不会触发订阅的next回调,console.log和断言根本没有执行机会。
你第一个测试用例看起来能正常运行,本质是传的control.value为null,提前触发了if (!input.value)的分支直接返回,根本没走到读取this.domains的问题代码段。
核心问题2:异步测试未做等待处理
你的验证器返回的Observable开头内置了timer(500)的500ms延时,而你写的测试代码没有任何等待异步流执行完的逻辑:测试框架执行完同步代码后,会立刻标记测试用例通过,根本不会等500ms的延时结束、Observable推送值到订阅回调里。哪怕没有刚才说的取值错位的问题,回调也不会在测试判定通过前执行。
加上刚才提到的运行时错误是在500ms延时后才抛出的,这时候测试已经走完判定流程,框架根本捕获不到这个异步错误,最终就出现了“测试显示通过但断言没跑”的假象。
修正步骤
1. 匹配取值逻辑写测试数据
既然正式代码是从验证器实例的this.domains取值,测试时就要把测试用的域名数组手动挂到validator实例上,不要塞到control对象里:
// 手动给validator实例赋值,匹配正式代码的取值来源 validator.domains = ['mozilla.com', 'google.com'];
2. 用Angular测试工具处理异步等待
两种常用写法选一种即可:
- 用
fakeAsync+tick手动推进时间轴,跳过timer的延时
it('should return a domainTakenInList error', fakeAsync(() => { validator.domains = ['mozilla.com', 'google.com']; const control = { value: 'google.com' }; let resultValue; validator.validate(control as any).subscribe(res => resultValue = res); // 直接推进500ms跳过timer等待 tick(500); expect(resultValue).toEqual({ domainTakenInList: true }); }));
- 用
done回调显式告知测试框架异步任务完成时机
it('should return a domainTakenInList error', (done) => { validator.domains = ['mozilla.com', 'google.com']; const control = { value: 'google.com' }; validator.validate(control as any).subscribe({ next: (res) => { expect(res).toEqual({ domainTakenInList: true }); // 断言执行完再通知框架测试结束 done(); }, error: (err) => done.fail(err) }); });
长期代码风险提示
你现在把domains、domainToEdit等和具体表单实例绑定的属性,挂在providedIn: 'root'全局单例的验证器上,存在严重的状态串扰问题:如果同一个页面有多个表单实例共用这个全局验证器,后挂载的属性会覆盖前面的值,导致验证逻辑判断错误。更稳妥的实现方式是把静态校验逻辑封装成验证器工厂函数,将域名数组、编辑态id作为工厂参数,为每个表单生成独立的验证器实例,避免全局状态污染。
内容的提问来源于stack exchange,提问作者NewToAngular

