首页 > 解决方案 > 使用 RendererFactory2 和没有 TestBed 的私有 Renderer2 对 Angular 服务进行单元测试

问题描述

我创建了一个服务

  1. 开放模态
  2. 模糊背景(只要至少有一个打开的模态)

服务看起来像这样:

@Injectable({
  providedIn: 'root',
})
export class ModalService {
  private readonly _renderer: Renderer2;
  // Overlaying hierarchy
  private _modalCount = 0;

  // Functional references
  private _viewContainerRef: ViewContainerRef;

  constructor(
    @Inject(DOCUMENT) private readonly _document: Document,
    private readonly _injector: Injector,
    private readonly _rendererFactory: RendererFactory2,
  ) {
    this._renderer = this._rendererFactory.createRenderer(null, null);
  }

  /**
   * Set the point where to insert the Modal in the DOM. This will generally be the
   * AppComponent.
   * @param viewContainerRef
   */
  public setViewContainerRef(viewContainerRef: ViewContainerRef): void {
    this._viewContainerRef = viewContainerRef;
  }

  /**
   * Adds a modal to the DOM (opens it) and returns a promise. the promise is resolved when the modal
   * is removed from the DOM (is closed)
   * @param componentFactory the modal component factory
   * @param data the data that should be passed to the modal
   */
  public open<S, T>(componentFactory: ComponentFactory<AbstractModalComponent<S, T>>, data?: S): Promise<T> {
    return new Promise(resolve => {
      const component = this._viewContainerRef.createComponent(componentFactory, null, this._injector);
      if (data) {
        component.instance.data = data;
      }
      const body = this._document.body as HTMLBodyElement;
      // we are adding a modal so we uppen the count by 1
      this._modalCount++;
      this._lockPage(body);
      component.instance.depth = this._modalCount;
      component.instance.close = (result: T) => {
        component.destroy();
        // we are removing a modal so we lessen the count by 1
        this._modalCount--;
        this._unlockPage(body);
        resolve(result);
      };
      component.changeDetectorRef.detectChanges();
    });
  }

  /**
   * Prevents the page behind modals from scrolling and blurs it.
   */
  private _lockPage(body: HTMLBodyElement): void {
    this._renderer.addClass(body, 'page-lock');
  }

  /**
   * Removes the `page-lock` class from the `body`
   * to enable the page behind modals to scroll again and be clearly visible.
   */
  private _unlockPage(body: HTMLBodyElement): void {
    // If at least one modal is still open the page needs to remain locked
    if (this._modalCount > 0) {
      return;
    }
    this._renderer.removeClass(body, 'page-lock');
  }
}

由于 Angular 禁止将 Renderer2 作为依赖项直接注入到服务中,因此我必须注入 RendererFactory2 并createRenderer在构造函数中调用。

现在我想对服务进行单元测试并检查是否_modalCount正确递增,然后将其传递给每个打开的模式作为depth输入来管理它们的 CSS z-index

我的单元测试看起来像这样(我注释掉了一些expects 以查明问题:

describe('ModalService', () => {
  let modalService: ModalService;
  let injectorSpy;
  let rendererSpyFactory;
  let documentSpy;
  let viewContainerRefSpy;

  @Component({ selector: 'ww-modal', template: '' })
  class ModalStubComponent {
    instance = {
      data: {},
      depth: 1,
      close: _ => {
        return;
      },
    };
    changeDetectorRef = {
      detectChanges: () => {},
    };
    destroy = () => {};
  }

  const rendererStub = ({
    addClass: jasmine.createSpy('addClass'),
    removeClass: jasmine.createSpy('removeClass'),
  } as unknown) as Renderer2;

  beforeEach(() => {
    rendererSpyFactory = jasmine.createSpyObj('RendererFactory2', ['createRenderer']);
    viewContainerRefSpy = jasmine.createSpyObj('ViewContainerRef', ['createComponent']);
    documentSpy = {
      body: ({} as unknown) as HTMLBodyElement,
      querySelectorAll: () => [],
    };
    modalService = new ModalService(documentSpy, injectorSpy, rendererSpyFactory);
    rendererSpyFactory.createRenderer.and.returnValue(rendererStub);
    modalService.setViewContainerRef(viewContainerRefSpy);
  });

  it('#open should create a modal component each time and keep track of locking the page in the background', done => {
    function openModal(depth: number) {
      const stub = new ModalStubComponent();
      viewContainerRefSpy.createComponent.and.returnValue(stub);
      const currentModal = modalService.open(null, null);

      expect(viewContainerRefSpy.createComponent).toHaveBeenCalledWith(null, null, injectorSpy);
      expect(rendererSpyFactory.createRenderer).toHaveBeenCalledWith(null, null);
      // expect(stub.instance.depth).toBe(depth);
      // expect(rendererStub.addClass).toHaveBeenCalledWith(documentSpy.body, pageLockClass);

      return { currentModal, stub };
    }
    openModal(1); // Open first modal; `modalCount` has `0` as default, so first modal always gets counted as `1`
    const { currentModal, stub } = openModal(2); // Open second modal; `modalCount` should count up to `2`

    currentModal.then(_ => {
      // expect(rendererStub.removeClass).toHaveBeenCalledWith(documentSpy.body, pageLockClass);
      openModal(2); // If we close a modal, `modalCount` decreases by `1` and opening a new modal after that should return `2` again
      done();
    });
    stub.instance.close(null);
  });
});

如您所见,我没有使用 Angulars TestBed,因为它添加了更多隐藏功能。运行测试抛出

Unhandled promise rejection: TypeError: Cannot read property 'addClass' of undefined

显然,_renderer即使createRenderer已被调用,也没有定义。我该如何解决这个问题?

标签: angularunit-testingjasmine

解决方案


我认为您的订单不正确,请在创建之前尝试存根new ModalService

// do this first before creating a new ModalService
rendererSpyFactory.createRenderer.and.returnValue(rendererStub);
modalService = new ModalService(documentSpy, injectorSpy, rendererSpyFactory);

推荐阅读