如何对AG Grid gridApi.forEachNode内部逻辑进行单元测试?
问题描述
在Angular 15、Jest 29.5.0、ag-grid-angular 25.3.0环境下,需要为以下组件方法实现单元测试覆盖:
processUpdates(input) { let request = []; this.gridApi.forEachNode(function (node) { // 需要mock/spy此方法的内部逻辑 if (input.has(data.id)) { request.push(this.createRequestRowUpdate(node, input)); } this.someCoolService.saveMychanges(request).pipe(map(res => res as any)).subscribe(res => { // 成功逻辑 }, (error: HttpErrorResponse) => { // 失败逻辑 }); }); }
目前已mock gridApi,但forEachNode的mock未完善,需要编写测试用例对forEachNode内部调用的createRequestRowUpdate、saveMychanges方法进行断言(例如验证saveMychanges是否被调用一次)。当前测试代码如下:
const mGridApi = { // 其他已mock方法 forEachNode: function () { // 需完善此mock }, // 其他已mock方法 }; it('should call processUpdate', async () => { component['gridApi'] = mGridApi; jest.spyOn(component, 'processUpdate'); jest.spyOn(component, 'createRequestRowUpdate'); jest.spyOn(service, 'saveMychanges'); component['processUpdate']('test data in here'); expect(component['createRequestRowUpdate']).hasBeenCalledOnce() });
请教如何设置测试以实现上述断言?
解决方案
1. 完善forEachNode的Mock,执行传入的回调
forEachNode的核心逻辑是调用传入的回调函数,所以mock时要让它主动执行这个回调,同时要处理回调里的this指向问题——原代码用了普通函数,this默认指向gridApi,得手动绑定组件实例才能正确调用createRequestRowUpdate和someCoolService。
修改后的mGridApi:
const mGridApi = { // 其他已mock方法 forEachNode: jest.fn((callback) => { // 模拟ag-grid的node数据,根据测试场景自定义结构 const mockNode = { data: { id: 'test-id' } }; // 把组件实例绑定到回调的this上 callback.call(component, mockNode); }), // 其他已mock方法 };
2. 构造符合条件的测试输入
原代码里有input.has(data.id)的判断,测试时要构造能触发(或不触发)这个判断的输入,比如用Set模拟:
// 包含mockNode的id,会触发createRequestRowUpdate调用 const validInput = new Set(['test-id']); // 不包含mockNode的id,不会触发createRequestRowUpdate调用 const invalidInput = new Set(['non-exist-id']);
3. 处理异步Observable的订阅
saveMychanges返回的是Observable,测试时要让它立即完成,否则断言会在订阅执行前触发。可以让spy返回一个立即发出值的Observable:
// 导入rxjs的of操作符 import { of, throwError } from 'rxjs'; // 让saveMychanges返回一个成功的Observable jest.spyOn(service, 'saveMychanges').mockReturnValue(of({})); // 如果要测试错误场景,返回错误Observable // jest.spyOn(service, 'saveMychanges').mockReturnValue(throwError(() => new HttpErrorResponse({})));
4. 完整测试用例示例
整合以上步骤,完整测试代码如下:
import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; import { of, throwError } from 'rxjs'; import { HttpErrorResponse } from '@angular/common/http'; import { YourComponent } from './your.component'; import { SomeCoolService } from './some-cool.service'; describe('YourComponent - processUpdates', () => { let component: YourComponent; let service: SomeCoolService; const mGridApi = { forEachNode: jest.fn((callback) => { const mockNode = { data: { id: 'test-id' } }; callback.call(component, mockNode); }), // 其他已mock的gridApi方法 }; beforeEach(() => { TestBed.configureTestingModule({ declarations: [YourComponent], providers: [SomeCoolService] }); component = TestBed.inject(YourComponent); service = TestBed.inject(SomeCoolService); // 绑定mock的gridApi到组件 component['gridApi'] = mGridApi; }); it('should call createRequestRowUpdate and saveMychanges when input contains node id', () => { // 创建方法spy const createSpy = jest.spyOn(component, 'createRequestRowUpdate').mockReturnValue({ mock: 'data' }); const saveSpy = jest.spyOn(service, 'saveMychanges').mockReturnValue(of({})); // 调用目标方法 component['processUpdates'](new Set(['test-id'])); // 断言逻辑 expect(mGridApi.forEachNode).toHaveBeenCalled(); expect(createSpy).toHaveBeenCalledTimes(1); expect(createSpy).toHaveBeenCalledWith(mGridApi.forEachNode.mock.calls[0][0].arguments[0], new Set(['test-id'])); expect(saveSpy).toHaveBeenCalledTimes(1); expect(saveSpy).toHaveBeenCalledWith([{ mock: 'data' }]); }); it('should NOT call createRequestRowUpdate when input does not contain node id', () => { const createSpy = jest.spyOn(component, 'createRequestRowUpdate'); const saveSpy = jest.spyOn(service, 'saveMychanges').mockReturnValue(of({})); component['processUpdates'](new Set(['non-exist-id'])); expect(createSpy).not.toHaveBeenCalled(); expect(saveSpy).toHaveBeenCalledTimes(1); expect(saveSpy).toHaveBeenCalledWith([]); // 此时request是空数组 }); it('should handle save failure correctly', fakeAsync(() => { // 模拟错误场景 const errorSpy = jest.spyOn(console, 'error').mockImplementation(); jest.spyOn(service, 'saveMychanges').mockReturnValue(throwError(() => new HttpErrorResponse({ status: 400 }))); component['processUpdates'](new Set(['test-id'])); tick(); // 触发Observable的错误回调 expect(errorSpy).toHaveBeenCalled(); errorSpy.mockRestore(); })); });
5. 额外注意事项
- 原代码里的
data.id应该是node.data.id(看起来像是笔误),要确保mockNode的结构和业务代码匹配。 - 如果测试涉及异步逻辑,建议用Angular的
fakeAsync和tick()来确保订阅完成后再执行断言。
内容的提问来源于stack exchange,提问作者null-point-exceptional
相关产品推荐
相关产品推荐

