angular提供十分敌对的单元测试,特地是对于组件。应用ng g
命令即可生成一个高度能够测试的组件。而指令的单元测试样板代码便显得有些简陋了。
面临问题
angular对指令测试的样板代码更靠近于service的样板测试代码:
describe('TestDirective', () => { it('should create an instance', () => { const directive = new TestDirective(); expect(directive).toBeTruthy(); });});
这如同是在说:你应该像测试service一样来测试指令。而指令的作用更多的是对宿主进行一些变更,所以咱们在单元测试时须要一个获取到这么一个宿主。
当查阅angular在官网文档预在指令测试上获取更多信息时却提到以下提示信息:
那么问题来了,在进行指令测试时,是应该听从ng g
生成的样本代码进行指令实例的测试呢?还是应该听从官网文档构建人造测试组件,由对组件的测试间接实现对指令的测试呢。
剖析问题
官网文档只所以提出创立人造测试组件,其起因是:只有构建一个实在的组件,才可能获取到指令要操作的DOM。
比方有如下指令:
@Directive({ selector: '[appQuestion]'})export class QuestionDirective { @Input() set appQuestion(question: any) { this.setQuestion(question); } constructor(private elementRef: ElementRef) { }}
构造函数中须要的elementRef
是个DOM的援用,尽管咱们能够手动创立一个ElementRef
对象进去,但却无奈间接的感触指令对其产生的影响。
解决方案
咱们以后两个根本的诉求:
- 获取一个可用可察看的
ElementRef
。 - 实例化被测指令
指令依赖于ElementRef
,ElementRef
的获取形式有多种:
间接创立
能够应用document
对象间接创立一个dom
对象进去,而后再实例化出一个ElementRef
供指令实例化:
fit('手动创立', () => { expect(component).toBeTruthy(); const nativeElement = document.createElement('div'); nativeElement.innerHTML = '<h1>Hello World</h1>'; const elementRef = new ElementRef(nativeElement); const directive = new QuestionDirective(elementRef); expect(directive).toBeTruthy(); });
而后比方咱们为指令增加一些办法:
export class QuestionDirective implements OnInit { @Input() set appQuestion(question: any) { } constructor(private elementRef: ElementRef) { } ngOnInit(): void { this.elementRef.nativeElement.style.color = 'red'; }}
而后便能够进行相应的测试了:
fit('手动创立', () => { expect(component).toBeTruthy(); const nativeElement = document.createElement('div'); nativeElement.innerHTML = '<h1>Hello World</h1>'; const elementRef = new ElementRef(nativeElement); const directive = new QuestionDirective(elementRef); document.body.append(nativeElement); expect(directive).toBeTruthy(); // 测试ngOnInit办法 directive.ngOnInit(); expect(nativeElement.style.color).toEqual('red'); });
尽管本办法能够起到测试指令的作用,但间接由document创立的组件显著的脱离的angular,除非咱们对angular有相当的理解,否则应该躲避这样做。
@ViewChild
angular
提供的@ViewChild
能够疾速的获取到相干的援用,所以咱们也能够创立一个测试组件,而后应用@ViewChild
来获取到ElementRef
。
@Component({ template: `<div #elementRef>Hello World</div>`})class MockComponent implements AfterViewInit { @ViewChild('elementRef') elementRef: ElementRef; ngAfterViewInit(): void { console.log(this.elementRef); }}describe('QuestionDirective', () => { let fixture: ComponentFixture<MockComponent>; let component: MockComponent; beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [MockComponent], imports: [ CommonModule ] }) .compileComponents(); }); beforeEach(() => { fixture = TestBed.createComponent(MockComponent); component = fixture.componentInstance; fixture.detectChanges(); }); fit('依赖于angular创立', () => { });});
此时便能够由被测组件中获取到ElementRef
了:
fit('依赖于angular创立', () => { const directive = new QuestionDirective(component.elementRef); // 调用onInit办法 directive.ngOnInit(); expect(component.elementRef.nativeElement.style.color).toEqual('red'); });
此种办法看起来确实有些麻烦,但却能够应用ComponentFixture
等一系列特有性能,更重要是这更加贴近于组件测试。使指令与组件测试看起来差不多。
此计划尽管能够获取到一个由angular为咱们创立的ElementRef
,但并没有齐全模仿实在的环境,在实在的环境中,咱们往往要这么用 ---- <div [appQuestion]="question" #elementRef>Hello World!</div>
。
上面咱们将持续探讨如何获取这个有宿主的指令实例。
综合计划
咱们略微修改一测试组件,使其成为QuestionDirective
的宿主:
@Component({ template: ` <div [appQuestion]="question" #elementRef>Hello World!</div>`})class MockComponent implements AfterViewInit { @ViewChild('elementRef') elementRef: ElementRef; question = {}; ngAfterViewInit(): void { console.log(this.elementRef); }}
而后咱们在单元测试中减少测试方法:
fit('综合形式', () => { });
尽管咱们未书写一行代码,然而因为为Mock组件绑定了QuestionDirective
指令,所以该指令曾经失效。
此时咱们便能够像官网的参考文档一样,对Mock组件调用相干办法来实现相应成果的测试:
fit('综合形式', () => { expect(fixture.debugElement.query(By.css('#root0 > div')) .nativeElement.textContent).toEqual('Hello World!');});
同时同样能够应用ElementRef来构建指令实例来实现相应性能的测试:
fit('综合形式', () => { expect(fixture.debugElement.query(By.css('#root0 > div')).nativeElement.textContent).toEqual('Hello World!'); const directive = new QuestionDirective(component.elementRef); directive.ngOnInit(); expect(component.elementRef.nativeElement.style.color).toEqual('red'); });
动静组件(指令)
动静组件的结构依赖于ViewContainerRef
。尽管stackoverflow上的一篇文章提出了能够手动写一个测试专用的ViewContainerRef
,但并不是一个好主见。因为一旦这样做,假的ViewContainerRef
无奈提供实在的DOM性能,从而无奈在单元测试直观地察看DOM的变动,变成睁眼瞎。
因为ViewContainerRef
是个抽像类,所以咱们无奈向ElementRef一样手动地实例化一个。这时候便要细说下这个@ViewChild
的作用了。
其实@ViewChild
的作用并不是绑定ElementRef
,而是绑定Ref
,即援用,参阅官网文档可知这仅只其应用的办法之一:
The following selectors are supported.
- Any class with the @Component or @Directive decorator
- A template reference variable as a string (e.g. query <my-component #cmp></my-component> with @ViewChild('cmp'))
- Any provider defined in the child component tree of the current component (e.g. @ViewChild(SomeService) someService: SomeService)
- Any provider defined through a string token (e.g. @ViewChild('someToken') someTokenVal: any)
- A TemplateRef (e.g. query <ng-template></ng-template> with @ViewChild(TemplateRef) template;)
由上文可知@ViewChild不仅可能依据字符串失去一个template的援用,还能够获取到组件、指令、provider以及TemplateRef。
在构建动静组件时,有时候咱们须要一个ViewContainerRef
,做为装载组件的容器:
constructor(private elementRef: ElementRef, private viewContainerRef: ViewContainerRef) { }
此时咱们能够在@ViewChild
上退出{read: ViewContainerRef}
来获取:
@Component({ template: ` <ng-template #viewContainerRef></ng-template> `})class MockComponent implements AfterViewInit { @ViewChild('viewContainerRef', {read: ViewContainerRef}) viewContainerRef: ViewContainerRef;
有了ViewContainerRef,便能够轻松的实例化出指令了:
const directive = new QuestionDirective(component.elementRef, component.viewContainerRef);
依赖服务
如果咱们的指令须要依赖于某些服务:
constructor(private elementRef: ElementRef, private viewContainerRef: ViewContainerRef, private testService: TestService) { }
此时若要实例化指令,则还须要服务相干的实例,除了能够手动实例化服务实例以下,还能够应用TestBed.get()
来获取:
const testService = TestBed.inject(TestService);const directive = new QuestionDirective(component.elementRef, component.viewContainerRef, testService);
留神:angular10中弃用了TestBed.get
,改为了TestBed.inject()
总结
angular只所以提供了弱小的单元测试性能,是因为它有用。尽管咱们确实须要在单元测试上破费更多的功夫,但一旦把握了单元测试的技能。你将可能情绪的享受不依赖于后盾的前台开发、自定义依赖的前台开发,这使得咱们独自开发某一个组件成为了事实,使咱们在开发过程中齐全的脱离后盾、脱离数据库、脱离ng serve
,仅仅须要ng t
。
在整个测试过程中指令与组件联合的十分严密,官网的文档与ng g d xx
生成的样本代码又不对立,获取一些组件主动注入的对象又不是那么间接,这都为指令的测试减少了少许难度。
不过一旦咱们把握了它,便会有一种一通百通的感觉。整个过程过后,你将感叹不虚此行。