乐趣区

关于angular:angular指令中带有ViewContainerRef以及其它服务时该如何进行单元测试

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 生成的样本代码又不对立,获取一些组件主动注入的对象又不是那么间接,这都为指令的测试减少了少许难度。

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

退出移动版