Skip to content

Test implementation guidelines for Ignite UI for Angular

Plamena Miteva edited this page Apr 13, 2020 · 35 revisions
Version User Date Notes
0.1 Zdravko Kolev June 10, 2019 Initial version
0.2 Nikolay Alipiev January 17, 2020 Angular component unit testing
0.3 Nikolay Alipiev / Plamena Miteva March 30, 2020 Define new Guidelines
0.4 Plamena Miteva April 08, 2020 Editing test guidelines
  • Radoslav Karaivanov | Date:
  • Konstantin Dinev | Date:

Prerequisites

Examples

Here are some good examples of how to write new tests and how some of the old ones are refactored using the guide below.

New tests

Refactored tests

Test implementation guidelines

Note: those guidelines should be applied only if reasonable

Globals

  • Define constants to hold css selector class names, default values, debounce time etc., at the top of the tests:
const CSS_CLASS_DRAG_ROW = 'igx-grid__tr--drag';
const DEFAULT_ITEM_HEIGHT = 40;
const DEBOUNCE_TIME = 16;
  • Define types for all variables and parameters:
let grid: IgxGridComponent;
  • For better performance, use NoopAnimationsModule to disable animations in tests if they are not the subject of testing:
import { NoopAnimationsModule } from '@angular/platform-browser/animations';

Testing without TestBed

One of the techniques for unit testing is to create a component/service/directive and inject its dependencies by hand while calling its constructor as described in Angular tutorials.

select = new IgxSelectComponent(null, mockCdr, mockSelection, null, mockInjector);

There are several approaches to create a dependency:

  • calling its constructor
const selectionService = new IgxSelectionAPIService();
combo = new IgxComboComponent({ nativeElement: null }, mockCdr, selectionService, mockComboService, null, mockInjector);

However, creating the real component or service might be a rather difficult task sometimes as it may rely on several other dependencies which also have to be created and injected.

  • mock the dependency with fake class
  • use a dummy value
elementRef = { nativeElement: { value: '20/02/2019 23:15:12' } };
dateTimeEditor = new IgxDateTimeEditorDirective(elementRef, maskParsingService, renderer2, DOCUMENT);
  • create a spy
const mockSelection: {
            [key: string]: jasmine.Spy
        } = jasmine.createSpyObj('IgxSelectionAPIService', ['get', 'set', 'add_items', 'select_items']);
 combo = new IgxComboComponent({ nativeElement: null }, mockCdr, mockSelection as any, mockComboService, null, mockInjector);

Testing with TestBed

Setup

  • Group tests together by features/functionalities using a describe block
  • Create one TestBed for each describe using beforeAll
  • In the TestBed declarations put only components which are used in the tests bellow and import necessary dependencies
describe('Initialization and rendering tests: ', () => {
    configureTestSuite();
    beforeAll(async(() => {
        TestBed.configureTestingModule({
            declarations: [
                IgxComboSampleComponent
            ],
            imports: [
                IgxComboModule,
                NoopAnimationsModule,
                IgxToggleModule
            ]
        }).compileComponents();
    }));
    ...
  • Call the configureTestSuite() method before the TestBed setup.
describe('Initialization and rendering tests: ', () => {
    configureTestSuite();
  • Use beforeEach in cases when all the tests use only one TestBed component rather than duplicate the TestBed initialization Example:
describe('IgxGrid - Multi Cell selection', () => {
    configureTestSuite();
    beforeEach(async(() => {
        TestBed.configureTestingModule({
            declarations: [
                SelectionWithScrollsComponent,
                SelectionWithTransactionsComponent
            ],
            imports: [NoopAnimationsModule, IgxGridModule]
        }).compileComponents();
    }));
    describe('Base', () => {
        ...
        beforeEach(fakeAsync(/** height/width setter rAF */() => {
           fixture = TestBed.createComponent(TestComponentX);
           ...
        }));

        it('Should be able to select a range with mouse dragging', () => {
...
  • When creating tests for the grids (igxGrid, igxTreeGrid, igxHieraticalGrid) try to use or extend one of the TestBeds from the files grid-base-components.spec, grid-samples.spec and tree-grid-components.spec.ts. If none of them is suitable for your scenario add the new test component to one of these files so it can be easily reused later.
  • Variables used in all/several tests are better to be defined in the scope of the describe and then reassigned rather than repeat their declaration in each test:
describe('Custom ghost template tests', () => {
     let grid: IgxGridComponent;
     let rows: IgxGridRowComponent[];
     let dragRows: DebugElement[];
     configureTestSuite();
     beforeAll(async(() => {
     ...

Helper utils

The test-utils folder holds a great number of helper functions, data samples etc. that can be reused in tests to prevent code duplication.

Testing Asynchronous Code

When testing asynchronous code avoid using whenStable, done and setTimeout functions. Try to use fakeAsync and tick instead. The tick() function blocks execution and simulates the passage of time until all pending asynchronous activities complete.

it('should toggle drop down on open/close methods call', fakeAsync(() => {
                ...
                dropdown.open();
                tick();
                fixture.detectChanges();
                ...
));

If the above approach is not possible use async and await wait

 it('should properly call dropdown navigateNext with virtual items', (async () => {
                ...
                combo.toggle();
                await wait(30);
                fixture.detectChanges(); 
                ...
});

When testing virtualization you will need to use async since the code runs out of the Angular zone. It is also recommended to use the igxFor scroll methods like scrollTo instead of key interactions

 it('should preserve selection on scrolling', (async () => {
                ...
                combo.virtualScrollContainer.scrollTo(16);
                await wait(30);
                fixture.detectChanges();
                ...
});

UI Interactions & Events

To simulate an event in a test use the Angular DebugElement instance method triggerEventHandler(), instead of dispatchEvent().

selectedItem.triggerEventHandler('click', UIInteractions.clickEvent);
fixture.detectChanges();

The UIInteractions file (ui-interactions.spec) already has lots of functions covering keyboard and mouse events that can be used

`UIInteractions.triggerKeyDownEvtUponElem('tab', cell.nativeElement, true)`, 

Another option is to directly call the API methods:

rowDragDirective = dragRows[1].injector.get(IgxRowDragDirective);
rowDragDirective.onPointerDown(UIInteractions.createPointerEvent('pointerdown', startPoint));

Spies

Use Jasmine spies to stub any function and track calls to it and all arguments

    it('should not trigger onRemove event when ..', () => {
        ...
        const firstChipComp = fix.componentInstance.chips.toArray()[0];
        spyOn(firstChipComp.onRemove, 'emit');

This is especially useful in cases when we do not want the actual method to be called. The spy will replace the method with a stub that tracks if the method got called

    // Spy the saveBlobToFile method so the files are not really created
    spyOn(ExportUtilities as any, 'saveBlobToFile');

Pending Specs

Call the pending function in the spec's body to mark a test as pending instead of declaring it with xit.

it('should correctly handle ngControl validity', () => {
    pending('Convert existing form test here');
});

Refactor

In the end always do a self-review, to fix unclear test names, syntax errors, etc.

Future improvements

  • Move e2e test outside unit test.
  • Avoid defining test components in the test files. Define them in a separate file instead (for example, grid-samples.spec.ts) so they can be easily reused in other test files.
Clone this wiki locally