Angular CDK Overlay - 在单元测试中模拟覆盖组件

Angular CDK Overlay - Mocking overlay component in unit test

我已经创建了一个 offcanvas component for angular,但是我无法让我的单元测试工作。

这是 failing unit test (offcanvas-host.component):

describe('BsOffcanvasHostComponent', () => {
  let component: BsOffcanvasTestComponent;
  let fixture: ComponentFixture<BsOffcanvasTestComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [ CommonModule, OverlayModule ],
      declarations: [
        // Unit to test
        BsOffcanvasHostComponent,
      
        // Mock dependencies
        BsOffcanvasMockComponent,
        BsOffcanvasHeaderMockComponent,
        BsOffcanvasBodyMockComponent,
        BsOffcanvasContentMockDirective,

        // Testbench
        BsOffcanvasTestComponent,
      ]
    })
    .compileComponents();
  });

  beforeEach(() => {
    fixture = TestBed.createComponent(BsOffcanvasTestComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });
});

type OffcanvasPosition = 'top' | 'bottom' | 'start' | 'end';

@Component({
  selector: 'bs-offcanvas-test',
  template: `
    <bs-offcanvas [(show)]="isOffcanvasVisible" [position]="position" [hasBackdrop]="true" (backdropClick)="isOffcanvasVisible = false">
        <div *bsOffcanvasContent>
            <bs-offcanvas-header>
                <h5>Offcanvas</h5>
            </bs-offcanvas-header>
            <bs-offcanvas-body>
                <span>Content</span>
            </bs-offcanvas-body>
        </div>
    </bs-offcanvas>`
})
class BsOffcanvasTestComponent {
  isOffcanvasVisible = false;
  position: OffcanvasPosition = 'start';
}

@Directive({ selector: '[bsOffcanvasContent]' })
class BsOffcanvasContentMockDirective {
  constructor(offcanvasHost: BsOffcanvasHostComponent, template: TemplateRef<any>) {
    offcanvasHost.content = template;
  }
}

@Component({
  selector: 'bs-offcanvas-holder',
  template: `
    <div>
      <ng-container *ngTemplateOutlet="contentTemplate"></ng-container>
    </div>`,
  providers: [
    { provide: BsOffcanvasComponent, useExisting: BsOffcanvasMockComponent }
  ]
})
class BsOffcanvasMockComponent {
  constructor(@Inject(OFFCANVAS_CONTENT) contentTemplate: TemplateRef<any>) {
    this.contentTemplate = contentTemplate;
  }

  contentTemplate: TemplateRef<any>;
}

@Component({
  selector: 'bs-offcanvas-header',
  template: `
    <div class="offcanvas-header">
      <ng-content></ng-content>
    </div>`
})
class BsOffcanvasHeaderMockComponent {}

@Component({
  selector: 'bs-offcanvas-body',
  template: `
    <div class="offcanvas-body">
      <ng-content></ng-content>
    </div>`
})
class BsOffcanvasBodyMockComponent {}

测试 following component:

ngAfterViewInit() {
    const injector = Injector.create({
      providers: [
        { provide: OFFCANVAS_CONTENT, useValue: this.content },
      ],
      parent: this.rootInjector,
    });
    const portal = new ComponentPortal(BsOffcanvasComponent, null, injector);
    const overlayRef = this.overlayService.create({
      scrollStrategy: this.overlayService.scrollStrategies.block(),
      positionStrategy: this.overlayService.position().global()
        .top('0').left('0').bottom('0').right('0'),
      hasBackdrop: false
    });

    this.component = overlayRef.attach<BsOffcanvasComponent>(portal); // <-- The test fails here

    this.component.instance.backdropClick
      .pipe(takeUntil(this.destroyed$))
      .subscribe((ev) => {
        this.backdropClick.emit(ev);
      });

    this.viewInited$.next(true);
}

错误信息是

Error: NG0302: The pipe 'async' could not be found in the 'BsOffcanvasComponent' component!. Find more at https://angular.io/errors/NG0302

Here's a minimal reproduction of the issue

如何告诉 angular TestingModule 在调用时使用模拟组件而不是初始组件类型

overlayRef.attach<BsOffcanvasComponent>(portal)

单元测试中的命令?

编辑

遗憾的是,我仍然无法正常工作。通常在angular进行单元测试主要有3种情况:

通过在 UTT(测试单元)模板中使用标记名引用的组件

这可以通过创建具有相同标记名和 inputs/outputs 的 MockComponent 来解决。

UTT 中注入的祖先组件

你可以通过在装饰器

上创建一个带有 provider 的 MockComponent 来解决这个问题
@Component({
  selector: 'bs-offcanvas-holder',
  template: ``,
  providers: [
    { provide: BsOffcanvasComponent, useExisting: BsOffcanvasMockComponent }
  ]
})
class BsOffcanvasMockComponent {}

直接从 UTT 调用的组件类型

this.component = overlayRef.attach<BsOffcanvasComponent>(portal);

这里我应该可以使用 BsOffcanvasMockComponent 代替,而无需单元测试将其他文件拖到 TestBed 中。那么我该如何解决呢?当然我可以模拟 CDK Overlay 服务,但这仍然让我在我的 UTT 中留下上面的代码行,其中 BsOffcanvasComponent 被乱扔到测试平台中。

我认为您必须模拟 overlayService 创建方法,该方法必须有一个模拟 attach,其中包含 instancebackdropClick

类似这样的内容(后面有 !!):

describe('BsOffcanvasHostComponent', () => {
  let component: BsOffcanvasTestComponent;
  let fixture: ComponentFixture<BsOffcanvasTestComponent>;

  let mockOverlayService: jasmine.SpyObj<OverlayService>;

  beforeEach(async () => {
    // mock overlay attach
    mockOverlayAttach = {
        attach: () => {
          return {
             instance: {
               backdropClick: of(true),
             }
          };
        }
    };
    mockOverlayService = jasmine.createSpyObj<OverlayService>('OverlayService', ['create']);
    mockOverlayService.create.and.returnValue(mockOverlayAttach);
    await TestBed.configureTestingModule({
      imports: [ CommonModule, OverlayModule ],
      declarations: [
        // Unit to test
        BsOffcanvasHostComponent,
      
        // Mock dependencies
        BsOffcanvasMockComponent,
        BsOffcanvasHeaderMockComponent,
        BsOffcanvasBodyMockComponent,
        BsOffcanvasContentMockDirective,

        // Testbench
        BsOffcanvasTestComponent,
      ],
       // provide mock for OverlayService
      providers: [
        { provide: OverlayService, useValue: mockOverlayService },
      ]
    })
    .compileComponents();
  });

  beforeEach(() => {
    fixture = TestBed.createComponent(BsOffcanvasTestComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });
});

我能够通过在我的运行时模块 (OffcanvasModule) 和我的 TestingModule 中提供工厂来解决问题。这样就去掉了testing模块中BsOffcanvasComponent的import

components.module.ts

providers: [{
  provide: 'PORTAL_FACTORY',
  useValue: (injector: Injector) => {
    return new ComponentPortal(BsOffcanvasComponent, null, injector);
  }
}]

offcanvas-host.component.spec.ts

providers: [
  {
    provide: 'PORTAL_FACTORY',
    useValue: (injector: Injector) => {
      return new ComponentPortal(BsOffcanvasComponent, null, injector);
    }
  }
]

这解决了以下错误

The pipe 'async' could not be found in the 'BsOffcanvasComponent' component