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对象进去,但却无奈间接的感触指令对其产生的影响。

解决方案

咱们以后两个根本的诉求:

  1. 获取一个可用可察看的ElementRef
  2. 实例化被测指令

指令依赖于ElementRefElementRef的获取形式有多种:

间接创立

能够应用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.

  1. Any class with the @Component or @Directive decorator
  2. A template reference variable as a string (e.g. query <my-component #cmp></my-component> with @ViewChild('cmp'))
  3. Any provider defined in the child component tree of the current component (e.g. @ViewChild(SomeService) someService: SomeService)
  4. Any provider defined through a string token (e.g. @ViewChild('someToken') someTokenVal: any)
  5. 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生成的样本代码又不对立,获取一些组件主动注入的对象又不是那么间接,这都为指令的测试减少了少许难度。

不过一旦咱们把握了它,便会有一种一通百通的感觉。整个过程过后,你将感叹不虚此行。