How to test the usage of AG Grid in Angular applications
AG Grid displays tabular data.
To test a component that uses ag-grid-angular, mock the grid with ng-mocks,
verify its input and output bindings, and test any custom cell renderers separately.
Let's assume that a component uses ag-grid-angular like this:
<ag-grid-angular
[rowData]="rowData"
[columnDefs]="columnDefs"
[defaultColDef]="defaultColDef"
[gridOptions]="gridOptions"
(gridReady)="onGridReady($event)"
(rowClicked)="selectedRow = $event.data"
></ag-grid-angular>
<span>{{ selectedRow?.make }}</span>
A test of such a template requires us to:
- mock
ag-grid-angular - assert passed inputs
- assert listeners on outputs
- provide the Grid API methods used by the component
- assert custom cell renderers if the grid uses them
Spec file
With MockBuilder, our spec file needs a single line to provide mocks:
beforeEach(() => MockBuilder(TargetComponent, TargetModule));
Where TargetComponent is a component which uses ag-grid-angular,
and TargetModule is its module, which imports AgGridModule.
The complete component, its Row type, and its module are in
test.spec.ts.
Testing inputs of ag-grid-angular
In this test we need to verify that the grid receives the parent's rows, columns, default column settings, and grid options. We also check that replacing the rows updates the binding.
The tools from ng-mocks we need:
MockRender: to renderTargetComponentand get its instancengMocks.reveal: to find a debug element ofAgGridAngularngMocks.input: to get an input's value
After directly assigning a field, mark the parent for checking so the example
also works with OnPush change detection.
it('binds inputs', () => {
// Rendering TargetComponent and accessing its instance.
const fixture = MockRender(TargetComponent);
const targetComponent = fixture.point.componentInstance;
// Looking for a debug element of `AgGridAngular`.
const gridEl = ngMocks.reveal<AgGridAngular<Row>>(AgGridAngular);
// Asserting bound properties.
expect(ngMocks.input(gridEl, 'rowData')).toBe(
targetComponent.rowData,
);
expect(ngMocks.input(gridEl, 'columnDefs')).toBe(
targetComponent.columnDefs,
);
expect(ngMocks.input(gridEl, 'defaultColDef')).toBe(
targetComponent.defaultColDef,
);
expect(ngMocks.input(gridEl, 'gridOptions')).toBe(
targetComponent.gridOptions,
);
// Checking that the mock is available through ViewChild and has no grid artifacts.
expect(isMockOf(gridEl.componentInstance, AgGridAngular)).toBe(
true,
);
expect(targetComponent.grid).toBe(gridEl.componentInstance);
expect(targetComponent.grid!.api).toBeUndefined();
expect(ngMocks.formatHtml(gridEl)).toEqual('');
// Updating an input and checking its binding again.
targetComponent.rowData = [{ make: 'Ford', price: 32_000 }];
fixture.point.injector.get(ChangeDetectorRef).markForCheck();
fixture.detectChanges();
expect(ngMocks.input(gridEl, 'rowData')).toBe(
targetComponent.rowData,
);
});
Testing outputs of ag-grid-angular
The component listens to rowClicked and displays the selected row's make.
To test the binding, emit a row through the mocked output and assert its effect.
The tools from ng-mocks we need:
MockRender: to renderTargetComponentand get its instancengMocks.reveal: to find a debug element ofAgGridAngularngMocks.output: to get an output'sEventEmitter
it('binds outputs', () => {
// Rendering TargetComponent and accessing its instance.
const fixture = MockRender(TargetComponent);
const targetComponent = fixture.point.componentInstance;
const gridEl = ngMocks.reveal(AgGridAngular);
// Simulating an emit.
const data = targetComponent.rowData[0];
expect(targetComponent.selectedRow).toBeUndefined();
ngMocks
.output(gridEl, 'rowClicked')
.emit({ data } as RowClickedEvent<Row>);
fixture.detectChanges();
// Asserting the effect of the emit.
expect(targetComponent.selectedRow).toBe(data);
expect(ngMocks.formatText(fixture)).toEqual('Toyota');
});
Testing gridReady
The approach to test gridReady is the same as above.
In this example, onGridReady calls event.api.sizeColumnsToFit().
Supply that method in the event and assert that the handler calls it.
The GridApi<Row> and GridReadyEvent<Row> types come from ag-grid-community.
The type assertions describe partial test fixtures; the mock does not create a real Grid API.
it('handles gridReady', () => {
// Rendering TargetComponent and looking for the grid.
MockRender(TargetComponent);
const gridEl = ngMocks.reveal(AgGridAngular);
// Providing the API method used by the gridReady handler.
let calls = 0;
const api = {
sizeColumnsToFit: () => {
calls += 1;
},
} as GridApi<Row>;
// Simulating an emit.
expect(calls).toBe(0);
ngMocks
.output(gridEl, 'gridReady')
.emit({ api } as GridReadyEvent<Row>);
// Asserting the effect of the emit.
expect(calls).toBe(1);
});
Testing Grid API access through ViewChild
If the component accesses the grid through ViewChild, it might use these members:
@ViewChild(AgGridAngular) public grid?: AgGridAngular<Row>;
public getSelectedRows(): Row[] {
return this.grid!.api.getSelectedRows();
}
The tools from ng-mocks we need:
MockInstance: to initialize the mock'sapiproperty before renderingMockRender: to renderTargetComponentand get its instance
Call MockInstance.scope() in the suite to restore the customization after each test:
MockInstance.scope();
it('provides a Grid API for ViewChild', () => {
// Customizing the mock before rendering TargetComponent.
const selectedRows: Row[] = [{ make: 'Ford', price: 32_000 }];
const api = {
getSelectedRows: () => selectedRows,
} as GridApi<Row>;
MockInstance(AgGridAngular, 'api', api);
// Rendering TargetComponent and accessing its instance.
const targetComponent =
MockRender(TargetComponent).point.componentInstance;
// Asserting access to the API through ViewChild.
expect(targetComponent.grid!.api).toBe(api);
expect(targetComponent.getSelectedRows()).toBe(selectedRows);
});
Testing custom cell renderers
AG Grid creates Angular cell components referenced by a column's cellRenderer.
For example, a price column can use this definition:
{ field: 'price', cellRenderer: PriceCellComponent }
A mocked grid receives the column definition but does not create the cell component. Test the cell component separately, supplying the parameters that AG Grid passes to it.
agInit and refresh are called by AG Grid. Angular does not invoke them as lifecycle hooks.
Cell components are created dynamically, so use MockRender
to test their templates. The ngMocks.render helper is for projected templates.
For example, PriceCellComponent uses PriceService to format the supplied value:
@Injectable({ providedIn: 'root' })
class PriceService {
public format(value: number): string {
return `€${value}`;
}
}
@Component({
selector: 'ag-grid-price-cell',
standalone: true,
template: '<strong>{{ price }}</strong>',
})
class PriceCellComponent implements ICellRendererAngularComp {
public price = '';
public constructor(private readonly priceService: PriceService) {}
public agInit(params: ICellRendererParams): void {
this.price = this.priceService.format(params.value);
}
public refresh(params: ICellRendererParams): boolean {
this.price = this.priceService.format(params.value);
return true;
}
}
The tools from ng-mocks we need:
MockBuilder: to keep the cell component and mock its serviceMockRender: to render the cell component and get its instancengMocks.formatText: to read the rendered value
The setup provides a predictable result from the formatting service:
beforeEach(() =>
MockBuilder(PriceCellComponent).mock(PriceService, {
format: value => `price: ${value}`,
}),
);
Call agInit with the initial value, then refresh with a new value,
and assert the rendered text after each call:
it('refreshes the value without replacing the renderer', () => {
// Rendering the cell component and accessing its instance.
const fixture = MockRender(PriceCellComponent);
const renderer = fixture.point.componentInstance;
// Initializing the cell as AG Grid would.
renderer.agInit({ value: 35_000 } as ICellRendererParams);
fixture.point.injector.get(ChangeDetectorRef).markForCheck();
fixture.detectChanges();
expect(ngMocks.formatText(fixture)).toEqual('price: 35000');
// Refreshing the cell with a new value.
expect(
renderer.refresh({ value: 32_000 } as ICellRendererParams),
).toBe(true);
fixture.point.injector.get(ChangeDetectorRef).markForCheck();
fixture.detectChanges();
// Asserting that the same renderer displays the new value.
expect(fixture.point.componentInstance).toBe(renderer);
expect(ngMocks.formatText(fixture)).toEqual('price: 32000');
});
cell-renderer.spec.ts
also checks initialization on its own and verifies that a mocked grid preserves
the renderer configuration without creating cells.