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 {}
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 中注入的祖先组件
你可以通过在装饰器
上创建一个带有 provide
r 的 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
,其中包含 instance
和 backdropClick
。
类似这样的内容(后面有 !!):
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
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
我已经创建了一个 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 {}
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 中注入的祖先组件
你可以通过在装饰器
上创建一个带有provide
r 的 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
,其中包含 instance
和 backdropClick
。
类似这样的内容(后面有 !!):
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
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