Accelerate Your UI Development

How to Unit Test A Custom AG Grid Renderer Component in Angular with Karma and Jasmine

TLDR;

This is an advanced article that shows you how to properly test custom renderer AG Grid components in Angular. You will need to perform integrated, black-box unit tests to successfully validate the custom work done by the renderer. The component is rendered using the TestBed using Jasmine and Karma, then validated by traversing the generated DOM. You will need to test the user interface behavior of the component since AG Grid renderers modify the presentation of data cell content. Do not perform white-box testing.

If you wish to view a video of the content described in this article, click on the embedded video below. Otherwise, continue reading.

Contents

  1. Prerequisites
  2. Instructions
  3. Considerations
  4. Avoid
  5. Conclusion
  6. Resources

Prerequisites

  • Angular v12
  • Karma / Jasmine unit testing setup
  • AG Grid Community Core v26
  • Intermediate level experience with Angular

Instructions

We are going to be testing a custom renderer that transforms a numeric value into a US Dollar currency. The custom renderer takes a general numeric value and transforms it into a Dollar currency.

Currency custom renderer in action inside an AG Grid data table.

Let’s go over the Currency custom renderer code down below.

import { Component } from '@angular/core';
import { ICellRendererAngularComp } from '@ag-grid-community/angular';
import { ICellRendererParams } from '@ag-grid-community/core';

@Component({
  template: ` {{ value | currency:'USD' }} `
})
export class CurrencyRendererComponent implements AgRendererComponent{
  /** The cell value. */
  value: any;

  agInit(params: ICellRendererParams): void {
    this.value = params.value;
  }

  refresh(params: ICellRendererParams): boolean {
    this.value = params.value;
    return true;
  }
}

On line 6, we can see that our component’s renderer view template is taking the entered value in the cell and running it through Angular’s Currency Pipe. We set the currency code to “USD” in order for our code sample to be consistent regardless of the country the end-user views the currency figure.

On line 8, we designate this component to be a custom renderer consumable by AG Grid by implementing the AgRendererComponent interface. We won’t go into much more detail than this. You can read more about building custom renderers here.

Moving on, let’s talk about the approach for testing the renderer component. Given that the renderer’s main job is to transform value content into a particular format, our unit tests need to focus on testing the behavior of the component only. Proper behavior testing requires black-box testing instead of white-box testing. So instead of our spec verifying class methods and properties, we will validate that the component renders HTML content correctly with different inputs.

Let’s talk about how to structure our spec file. We know that the renderer is a consumed component, meaning that it cannot exist independently. It will need to be called upon by an AG Grid component. Our unit test will require us to stub out a parent AG Grid component to present our custom renderer in the test runner (Karma) to properly query its DOM properties. Therefore, our spec file will have two parts; the stub parent component that will invoke the custom renderer and the main Jasmine describe block where the TestBed instantiates both the parent and renderer components.

Let’s start by reviewing the code for the stub parent component.

import { Component } from '@angular/core';
import { CurrencyRendererComponent } from './currency-renderer.component';
import { ColDef, Module } from '@ag-grid-community/core';
import { ClientSideRowModelModule } from '@ag-grid-community/all-modules';

@Component({
  template: `
    <div>
      <ag-grid-angular
        class="ag-theme-alpine"
        [rowData]="rowData"
        [columnDefs]="columnDefs"
        [modules]="modules"
        [frameworkComponents]="frameworkComponents"
      >
      </ag-grid-angular>
    </div>
  `,
  styles: [
    `
      ag-grid-angular {
        height: 100px;
        width: 100%;
      }
    `
  ]
})
class TestHostCurrencyRendererComponent {
  modules: Module[] = [ClientSideRowModelModule];

  columnDefs: ColDef[] = [
    {
      headerName: 'Customer Name',
      field: 'name',
      filter: false
    },
    {
      headerName: 'Account No',
      field: 'accountNumber',
      filter: false
    },
    {
      headerName: 'Total',
      field: 'total',
      editable: true,
      cellRenderer: 'currencyRenderer'
    }
  ];

  frameworkComponents = {
    currencyRenderer: CurrencyRendererComponent
  };

  rowData = [
    {
      name: 'Guillermo',
      accountNumber: 123,
      total: 100
    }
  ];
}

The entire stub looks like a standard Angular component. We have the @Component decorator on line 6 with the inline template and styles. The template (lines 8-17) effectively declares our use of AG Grid. The class (lines 28-61) define the configuration for AG Grid.

Of particular importance is the frameworkComponents object on line 50. This object truly configures AG Grid to consume our custom Currency renderer. Otherwise, AG Grid has no idea of the custom renderer’s existence.

Okay, so the second part of the spec file is our actual Jasmine tests. Here is the code to review. We’ll only highlight the important pieces.

import {
  ComponentFixture,
  fakeAsync,
  flush,
  TestBed,
  waitForAsync
} from '@angular/core/testing';
import { CurrencyRendererComponent } from './currency-renderer.component';
import { AgGridModule } from '@ag-grid-community/angular';

describe('CurrencyRendererComponent', () => {
  let component: TestHostCurrencyRendererComponent;
  let fixture: ComponentFixture<TestHostCurrencyRendererComponent>;

  beforeEach(
    waitForAsync(() => {
      TestBed.configureTestingModule({
        imports: [AgGridModule],
        declarations: [
          TestHostCurrencyRendererComponent,
          CurrencyRendererComponent
        ]
      }).compileComponents();
    })
  );

  beforeEach(fakeAsync(
    () => {
      fixture = TestBed.createComponent(TestHostCurrencyRendererComponent);
      component = fixture.componentInstance;
      fixture.detectChanges();
      flush();
    })
  );

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

  it('should match USD currency in the produced cell render', () => {
    const elemInnerText = fixture.nativeElement.querySelector(
      'ag-grid-angular .ag-row>div:nth-last-child(1)'
    ).innerText;

    expect(elemInnerText).toBe('$100.00');

  });

  it('should round up USD currency in the produced cell render',
    fakeAsync(() => {
      component.rowData = [
        {
          name: 'Guillermo',
          accountNumber: 123,
          total: 5.9999
        }
      ];

      fixture.detectChanges();
      flush();

      const elemInnerText = fixture.nativeElement.querySelector(
        'ag-grid-angular .ag-row>div:nth-last-child(1)'
      ).innerText;

      expect(elemInnerText).toBe('$6.00');

    })
  );
});

First of all, you will need to pull in the proper instances of the components under test into your TestBed. This means both the stub component (line 20) and the renderer component under test (line 21) will be part of the declarations property array.

Second, you will need to wrap any code blocks that run a component’s change detection with a fakeAsync function. As you know, running a change detection after properties are modified will refresh the visuals of the component rendered in Karma. Inside of the beforeEach section (lines 27-34), you can see the anonymous function wrapped with fakeAsync. You need it here because of the fixture.detectChanges() method on line 31 is asynchronous, and the flush() method on line 32 ensures that all pending tasks are complete before continuing. By completing all of the asynchronous activities, you will have a stable component fully rendered in the test runner ready to query and assert for correctness. Likewise, if you modify the properties in one of your tests, and want to refresh the component to test the correct behavior (line 49), you will need to wrap it in fakeAsync and execute flush().

For completeness sake, here’s the full spec file. If you are going to copy-and-paste something from this entire article, it’s this. 😉

import { Component } from '@angular/core';
import {
  ComponentFixture,
  fakeAsync,
  flush,
  TestBed,
  waitForAsync
} from '@angular/core/testing';
import { CurrencyRendererComponent } from './currency-renderer.component';
import { AgGridModule } from '@ag-grid-community/angular';
import { ColDef, Module } from '@ag-grid-community/core';
import { ClientSideRowModelModule } from '@ag-grid-community/all-modules';

@Component({
  template: `
    <div>
      <ag-grid-angular
        class="ag-theme-alpine"
        [rowData]="rowData"
        [columnDefs]="columnDefs"
        [modules]="modules"
        [frameworkComponents]="frameworkComponents"
      >
      </ag-grid-angular>
    </div>
  `,
  styles: [
    `
      ag-grid-angular {
        height: 100px;
        width: 100%;
      }
    `
  ]
})
class TestHostCurrencyRendererComponent {
  modules: Module[] = [ClientSideRowModelModule];

  columnDefs: ColDef[] = [
    {
      headerName: 'Customer Name',
      field: 'name',
      filter: false
    },
    {
      headerName: 'Account No',
      field: 'accountNumber',
      filter: false
    },
    {
      headerName: 'Total',
      field: 'total',
      editable: true,
      cellRenderer: 'currencyRenderer'
    }
  ];

  frameworkComponents = {
    currencyRenderer: CurrencyRendererComponent
  };

  rowData = [
    {
      name: 'Guillermo',
      accountNumber: 123,
      total: 100
    }
  ];
}

fdescribe('CurrencyRendererComponent', () => {
  let component: TestHostCurrencyRendererComponent;
  let fixture: ComponentFixture<TestHostCurrencyRendererComponent>;

  beforeEach(
    waitForAsync(() => {
      TestBed.configureTestingModule({
        imports: [AgGridModule],
        declarations: [
          TestHostCurrencyRendererComponent,
          CurrencyRendererComponent
        ]
      }).compileComponents();
    })
  );

  beforeEach(fakeAsync(
    () => {
      fixture = TestBed.createComponent(TestHostCurrencyRendererComponent);
      component = fixture.componentInstance;
      fixture.detectChanges();
      flush();
    })
  );

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

  it('should match USD currency in the produced cell render', () => {
    const elemInnerText = fixture.nativeElement.querySelector(
      'ag-grid-angular .ag-row>div:nth-last-child(1)'
    ).innerText;

    expect(elemInnerText).toBe('$100.00');

  });

  it('should round up USD currency in the produced cell render',
    fakeAsync(() => {
      component.rowData = [
        {
          name: 'Guillermo',
          accountNumber: 123,
          total: 5.9999
        }
      ];

      fixture.detectChanges();
      flush();

      const elemInnerText = fixture.nativeElement.querySelector(
        'ag-grid-angular .ag-row>div:nth-last-child(1)'
      ).innerText;

      expect(elemInnerText).toBe('$6.00');

    })
  );
});

Consider

Except for AG Grid custom renderers, I highly recommend that you stick with mostly white-box testing for your unit tests. White-box tests primarily focus on testing the logic of your component classes. These tests are fast and valuable. Any issue that could cause the production application to fail integration with other systems, or produce errors that impact business revenue, will be found in white-box tests. Focus on these types of tests for the majority of your code.

Avoid

Avoid a significant amount of black-box testing for your unit tests. Setting up stub parent or child components takes a significant amount of time. You’ll spend more time writing the tests than writing functioning code. That’s bad.

You also want to avoid pulling in multiple instances of components and 3rd party modules into your TestBed to generate a working user interface. Doing so can slow unit tests drastically. Typically, if you have less than 200 unit tests, you won’t notice a performance hit. But once you go over 300 unit tests, you’ll start noticing an entire battery of unit tests taking 5 minutes or more. Consider end-to-end test suites as an alternative to handle most of the functional tests dealing with interactivity.

As noted elsewhere with regards to AG Grid renderers, an exception is given to perform black-box testing for critical visual presentational components.

Conclusion

Here’s a link to all the code presented in the article; CodeSandbox.io sandbox

Please export the code into your local machine to run the tests locally. This way, you can play with the code while running Karma and perform further experimentation.

Performing unit testing of an AG Grid Editor component requires more setup than other tests. Sometimes the code for the test is longer than the component itself. The most important thing to remember when running your unit tests is to force asynchronous change detection to complete before validating the UI. In the end, if you follow these steps, you are guaranteed higher quality work and confidence in your code.

Resources