如何检查内部组件的方法在测试期间被调用?
How to check inner component's method is called during tests?
我有一个通过 HexesService 调用外部 API 的组件 (MapComponent)。此 API 调用的数据结果加载到某些按钮(查看地图、编辑地图)。我有另一个内部组件 (HexesComponent),单击“查看”按钮会调用此内部组件(这又会进行另一个 API 调用并显示 HexesComponent 内的数据)。
我创建了一个模拟外部服务的测试。我的测试检查在加载主要组件后,它会调用模拟服务并正确加载数据(所有按钮都已填充)。这工作得很好。
接下来我要检查单击按钮是否会调用 HexesComponent 的方法。
我的问题是我的内部组件总是未定义。
我已经为我的内部组件创建了一个存根,但这没有帮助:即使是存根也是空的。
问题 #1:
我做错了什么?我已经尝试对 'beforeEach' 中的方法使用异步,但这没有帮助。
不用说 'tests' 之外,该功能运行得非常好。
问题 #2:
如何验证单击按钮会导致调用 HexComponent 的 'loadMap' 方法?
这是我的测试代码:
class MockHexesService extends HexesService {
getDataForButtons(){
return of(...);
}
...
}
@Component({ selector: 'app-hex', template: '' })
class HexStubComponent {
public loadMap = jasmine.createSpy('loadMap');
}
describe('MapsComponent', () => {
let component: MapsComponent;
let hexComponent: HexStubComponent;
let fixture: ComponentFixture<MapsComponent>;
let componentService: HexesService;
let hexComponentSpy: any;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientModule],
declarations: [MapsComponent, HexStubComponent],
providers: [HexesService],
}).compileComponents();
TestBed.overrideComponent(MapsComponent, {
set: {
providers: [{ provide: HexesService, useClass: MockHexesService }],
},
});
fixture = TestBed.createComponent(MapsComponent);
component = fixture.componentInstance;
componentService = fixture.debugElement.injector.get(HexesService);
fixture.detectChanges();
hexComponent = fixture.debugElement.injector.get(HexStubComponent);
hexComponentSpy = spyOn(hexComponent, 'loadMap');
});
it('Loaded map descriptions should load data for buttons', () => {
const tblRows =
fixture.debugElement.nativeElement.querySelectorAll('table tr');
const cells0 = tblRows[0].querySelectorAll('td');
expect(cells0[0].innerHTML).toBe('Map 1');
... checking other buttons are populated properly
expect(component.hexComponent).toBeTruthy(); // - this fails here
});
it('Click on "View" button should load the map', async(() => {
//spyOn(component.hexComponent.loadMap)
const btns = fixture.debugElement.nativeElement.querySelectorAll('button');
expect(btns.length).toBe(6);
const btnView = btns[0];
expect(btnView.innerHTML).toBe('View Map');
btnView.click();
fixture.whenStable().then(() => {
expect(component.hexComponent).toBeTruthy(); // - this fails
// and how do I check that 'loadMap' method of hexComponent is called with proper data?
});
}));
});
maps.component.html的重要部分:
...
<tr *ngFor="let d1 of data">
...
<button (click)="loadMap(d1.mapId)">View Map</button>
...
</tr>
<app-hex id="hexComponent"></app-hex>
maps.component.ts的重要部分:
export class MapsComponent implements OnInit {
@ViewChild(HexComponent) hexComponent: HexComponent;
constructor(public ngZone: NgZone, private hexesService: HexesService) {}
ngOnInit(): void {
this.hexesService
.getDataForButtons()
.subscribe((data: any) => this.populateButtons(data));
}
loadMap(mapId): void {
this.hexComponent.loadMap(mapId);
}
P.S。我发现了一个类似的问题 (How can I use a fake/mock/stub child component when testing a component that has ViewChildren in Angular 10+?),它建议使用 ng-mocks,但无法使它们工作。
P.P.S。我试过使用
@Component({ selector: 'app-hex', template: '' })
class HexStubComponent {
//public loadMap(mapId: number) {}
public loadMap = jasmine.createSpy('loadMap');
}
并在 'TestBed.configureTestingModule' 的声明中使用 HexStubComponent,但我的测试给出了错误:
NullInjectorError: R3InjectorError(DynamicTestModule)[HexStubComponent
-> HexStubComponent]: NullInjectorError: No provider for HexStubComponent!
不知道如何解决。
我决定再试一次,让它与 ng-mocks 一起工作,而不是推动使用 Stabs 的选项。
显然,我的问题接近
好的指导是 https://www.npmjs.com/package/ng-mocks/v/11.1.4
不同的是在我的情况下,我还需要确保调用子组件的一些方法。
为了帮助其他人(也许将来我自己),这里列出了我在途中遇到的问题。
使用“npm install ng-mocks --save-dev”命令安装 ng-mocks
添加导入行(很明显,但 VS Code 没有自动建议这些,我努力了解如何访问我需要的确切 类):
import { MockComponent, MockInstance, ngMocks } from 'ng-mocks';
在 TestBed.configureTestingModule 的异步版本中添加您的组件模拟:
declarations: [MapsComponent, MockComponent(HexComponent)],
在您需要检查的组件和方法上创建间谍:
beforeEach(() => MockInstance(HexComponent, 'loadMap', jasmine.createSpy()));
验证:
it('Click on "View" button should load the map', async(() => {
const hexComponent = ngMocks.findInstance(HexComponent);
expect(hexComponent).toBeDefined();
expect(hexComponent.loadMap).not.toHaveBeenCalled();
const btns = fixture.debugElement.nativeElement.querySelectorAll('button');
expect(btns.length).toBe(6);
const btnView = btns[0];
expect(btnView.innerHTML).toBe('View Map');
btnView.click();
fixture.whenStable().then(() => {
expect(hexComponent.loadMap).toHaveBeenCalled();
});
}));
我唯一没有解决的问题是模拟 HexComponent 使用的一些服务。我稍后再试。
Angular @ViewChild
和其他查询基于 id
或 ctor
.
因为 HexStubComponent
不是 HexComponent
,查询无法找到它。
如您所说,为了修复它,您可以使用 ng-mocks and MockComponent or MockBuilder,然后库将完成所有设置。
或者你可以告诉Angular当请求HexComponent
时应该提供HexStubComponent
:
@Component({ selector: 'app-hex', template: '',
// this is the change
providers: [
{
provide: HexComponent,
useExisting: HexStubComponent,
},
],
// the end of the change
})
class HexStubComponent {
public loadMap = jasmine.createSpy('loadMap');
}
这就是 ng-mocks 在幕后所做的,应该可以帮助您实现所需的行为。
关于第二个问题。这听起来像是对 HexComponent
本身的测试,因为期望的期望是确保 HexComponent
行为如何,而这里我们有它的模拟。
在当前测试中,我建议覆盖 getDataForButtons
已在点击时被调用,但不是它的作用。它所做的应该是一个不同的测试套件。
我有一个通过 HexesService 调用外部 API 的组件 (MapComponent)。此 API 调用的数据结果加载到某些按钮(查看地图、编辑地图)。我有另一个内部组件 (HexesComponent),单击“查看”按钮会调用此内部组件(这又会进行另一个 API 调用并显示 HexesComponent 内的数据)。
我创建了一个模拟外部服务的测试。我的测试检查在加载主要组件后,它会调用模拟服务并正确加载数据(所有按钮都已填充)。这工作得很好。
接下来我要检查单击按钮是否会调用 HexesComponent 的方法。
我的问题是我的内部组件总是未定义。 我已经为我的内部组件创建了一个存根,但这没有帮助:即使是存根也是空的。
问题 #1: 我做错了什么?我已经尝试对 'beforeEach' 中的方法使用异步,但这没有帮助。
不用说 'tests' 之外,该功能运行得非常好。
问题 #2: 如何验证单击按钮会导致调用 HexComponent 的 'loadMap' 方法?
这是我的测试代码:
class MockHexesService extends HexesService {
getDataForButtons(){
return of(...);
}
...
}
@Component({ selector: 'app-hex', template: '' })
class HexStubComponent {
public loadMap = jasmine.createSpy('loadMap');
}
describe('MapsComponent', () => {
let component: MapsComponent;
let hexComponent: HexStubComponent;
let fixture: ComponentFixture<MapsComponent>;
let componentService: HexesService;
let hexComponentSpy: any;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientModule],
declarations: [MapsComponent, HexStubComponent],
providers: [HexesService],
}).compileComponents();
TestBed.overrideComponent(MapsComponent, {
set: {
providers: [{ provide: HexesService, useClass: MockHexesService }],
},
});
fixture = TestBed.createComponent(MapsComponent);
component = fixture.componentInstance;
componentService = fixture.debugElement.injector.get(HexesService);
fixture.detectChanges();
hexComponent = fixture.debugElement.injector.get(HexStubComponent);
hexComponentSpy = spyOn(hexComponent, 'loadMap');
});
it('Loaded map descriptions should load data for buttons', () => {
const tblRows =
fixture.debugElement.nativeElement.querySelectorAll('table tr');
const cells0 = tblRows[0].querySelectorAll('td');
expect(cells0[0].innerHTML).toBe('Map 1');
... checking other buttons are populated properly
expect(component.hexComponent).toBeTruthy(); // - this fails here
});
it('Click on "View" button should load the map', async(() => {
//spyOn(component.hexComponent.loadMap)
const btns = fixture.debugElement.nativeElement.querySelectorAll('button');
expect(btns.length).toBe(6);
const btnView = btns[0];
expect(btnView.innerHTML).toBe('View Map');
btnView.click();
fixture.whenStable().then(() => {
expect(component.hexComponent).toBeTruthy(); // - this fails
// and how do I check that 'loadMap' method of hexComponent is called with proper data?
});
}));
});
maps.component.html的重要部分:
...
<tr *ngFor="let d1 of data">
...
<button (click)="loadMap(d1.mapId)">View Map</button>
...
</tr>
<app-hex id="hexComponent"></app-hex>
maps.component.ts的重要部分:
export class MapsComponent implements OnInit {
@ViewChild(HexComponent) hexComponent: HexComponent;
constructor(public ngZone: NgZone, private hexesService: HexesService) {}
ngOnInit(): void {
this.hexesService
.getDataForButtons()
.subscribe((data: any) => this.populateButtons(data));
}
loadMap(mapId): void {
this.hexComponent.loadMap(mapId);
}
P.S。我发现了一个类似的问题 (How can I use a fake/mock/stub child component when testing a component that has ViewChildren in Angular 10+?),它建议使用 ng-mocks,但无法使它们工作。
P.P.S。我试过使用
@Component({ selector: 'app-hex', template: '' })
class HexStubComponent {
//public loadMap(mapId: number) {}
public loadMap = jasmine.createSpy('loadMap');
}
并在 'TestBed.configureTestingModule' 的声明中使用 HexStubComponent,但我的测试给出了错误:
NullInjectorError: R3InjectorError(DynamicTestModule)[HexStubComponent -> HexStubComponent]: NullInjectorError: No provider for HexStubComponent!
不知道如何解决。
我决定再试一次,让它与 ng-mocks 一起工作,而不是推动使用 Stabs 的选项。
显然,我的问题接近
为了帮助其他人(也许将来我自己),这里列出了我在途中遇到的问题。
使用“npm install ng-mocks --save-dev”命令安装 ng-mocks
添加导入行(很明显,但 VS Code 没有自动建议这些,我努力了解如何访问我需要的确切 类):
import { MockComponent, MockInstance, ngMocks } from 'ng-mocks';
在 TestBed.configureTestingModule 的异步版本中添加您的组件模拟:
declarations: [MapsComponent, MockComponent(HexComponent)],
在您需要检查的组件和方法上创建间谍:
beforeEach(() => MockInstance(HexComponent, 'loadMap', jasmine.createSpy()));
验证:
it('Click on "View" button should load the map', async(() => { const hexComponent = ngMocks.findInstance(HexComponent); expect(hexComponent).toBeDefined(); expect(hexComponent.loadMap).not.toHaveBeenCalled(); const btns = fixture.debugElement.nativeElement.querySelectorAll('button'); expect(btns.length).toBe(6); const btnView = btns[0]; expect(btnView.innerHTML).toBe('View Map'); btnView.click(); fixture.whenStable().then(() => { expect(hexComponent.loadMap).toHaveBeenCalled(); }); }));
我唯一没有解决的问题是模拟 HexComponent 使用的一些服务。我稍后再试。
Angular @ViewChild
和其他查询基于 id
或 ctor
.
因为 HexStubComponent
不是 HexComponent
,查询无法找到它。
如您所说,为了修复它,您可以使用 ng-mocks and MockComponent or MockBuilder,然后库将完成所有设置。
或者你可以告诉Angular当请求HexComponent
时应该提供HexStubComponent
:
@Component({ selector: 'app-hex', template: '',
// this is the change
providers: [
{
provide: HexComponent,
useExisting: HexStubComponent,
},
],
// the end of the change
})
class HexStubComponent {
public loadMap = jasmine.createSpy('loadMap');
}
这就是 ng-mocks 在幕后所做的,应该可以帮助您实现所需的行为。
关于第二个问题。这听起来像是对 HexComponent
本身的测试,因为期望的期望是确保 HexComponent
行为如何,而这里我们有它的模拟。
在当前测试中,我建议覆盖 getDataForButtons
已在点击时被调用,但不是它的作用。它所做的应该是一个不同的测试套件。