使用 RendererFactory2 和私有 Renderer2 的单元测试 Angular 服务,不带 TestBed

Unit-Testing Angular service with RendererFactory2 and private Renderer2 without TestBed

我为

创建了一个服务
  1. 打开模式
  2. 模糊背景(只要至少有一个打开的模态)

Servie 看起来像这样:

@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.

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

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

很明显,即使调用了 createRenderer_renderer 也没有定义。 我该如何解决这个问题?

我认为您的顺序不正确,请在创建 new ModalService

之前尝试存根
// do this first before creating a new ModalService
rendererSpyFactory.createRenderer.and.returnValue(rendererStub);
modalService = new ModalService(documentSpy, injectorSpy, rendererSpyFactory);