Skip to main content

Command Palette

Search for a command to run...

Angular directive testing

Updated

In this article, I'll show how to test Angular directive. To test it I'll create appCloneElement directive. Its purpose is to duplicate HTML elements given n times.

Setup

If unsure how to set up Angular with Jest please refer to the article: https://barcioch.pro/angular-with-jest-setup

Clone element directive

The directive works as follows (everything happens in ngOnChanges method):

  • clear the view of created elements
this.viewContainer.clear();
  • check if the passed value is a valid number
if (this.appCloneElement == null) {
  return;
}

if (!Number.isFinite(this.appCloneElement)) {
  return;
}

if (this.appCloneElement < 1) {
  return;
}
  • render the elements
for (let i = 1; i <= this.appCloneElement; i++) {
  this.viewContainer.createEmbeddedView(this.templateRef)
}

Full implementation:

import { Directive, Input, OnChanges, SimpleChanges, TemplateRef, ViewContainerRef } from '@angular/core';

@Directive({
  selector: '[appCloneElement]'
})
export class CloneElementDirective implements OnChanges {
  @Input() appCloneElement: number | null | undefined;

  constructor(
    private templateRef: TemplateRef<any>,
    private viewContainer: ViewContainerRef
  ) {
  }

  ngOnChanges(changes: SimpleChanges): void {
    this.viewContainer.clear();

    if (this.appCloneElement == null) {
      return;
    }

    if (!Number.isFinite(this.appCloneElement)) {
      return;
    }

    if (this.appCloneElement < 1) {
      return;
    }

    for (let i = 1; i <= this.appCloneElement; i++) {
      this.viewContainer.createEmbeddedView(this.templateRef)
    }
  }
}

Testing

To test a directive you need a component that passes properties to the directive. In the following example, it's TestComponent. The directive itself is declared in a separate module CloneElementModule so it has to be imported. The testing scenarios are quite simple:

  • pass the value to the component

  • detect changes

  • validate the number of displayed, cloned elements

The first scenario to check is the default view - without passing any values.

describe('when component with directive is initialized', () => {

it('should not render any element', () => {
  expect(getClones().length).toBe(0);
});
...

The directive takes the number of elements to clone. If the number is less that 1 it shouldn't render any element. Since you can pass some values that are not valid numbers but the compiler won't complain about them, we're also going to test them. These values are: null, undefined, NaN, Infinity.

describe.each([
  {input: -1, inputText: '-1'},
  {input: -581, inputText: '-581'},
  {input: 0, inputText: '0'},
  {input: null, inputText: 'null'},
  {input: undefined, inputText: 'undefined'},
  {input: Infinity, inputText: 'Infinity'},
  {input: NaN, inputText: 'NaN'},
])('and $inputText value is passed', ({input}) => {
  beforeEach(() => {
    component.cloneNumber = input;
    fixture.detectChanges();
  });

  it('should not render any element', () => {
    expect(getClones().length).toBe(0);
  });
});

Also, you have to verify that multiple changes to the directive input will produce a valid number of cloned elements. The scenario will look like this:

  • pass 5 number

  • detect changes

  • validate there are 5 elements

  • pass 9 number

  • detect changes

  • validate there are 9 elements

  • pass undefined value

  • detect changes

  • validate there are no elements

describe('and 5 value is passed', () => {
  beforeEach(() => {
    component.cloneNumber = 5;
    fixture.detectChanges();
  });

  it(`should render 5 elements`, () => {
    expect(getClones().length).toBe(5);
  });

  describe('and 9 value is passed', () => {
    beforeEach(() => {
      component.cloneNumber = 9;
      fixture.detectChanges();
    });

    it(`should render 9 elements`, () => {
      expect(getClones().length).toBe(9);
    });
  });

  describe('and undefined value is passed', () => {
    beforeEach(() => {
      component.cloneNumber = undefined;
      fixture.detectChanges();
    });

    it(`should not render any element`, () => {
      expect(getClones().length).toBe(0);
    });
  });
});

Full implementation:

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Component, DebugElement } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CloneElementModule } from './clone-element.module';
import { By } from '@angular/platform-browser';

describe('CloneElementDirective', () => {
  let fixture: ComponentFixture<TestComponent>;
  let component: TestComponent;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [
        CommonModule,
        CloneElementModule
      ],
      declarations: [TestComponent],
    });

    fixture = TestBed.createComponent(TestComponent);
    component = fixture.componentInstance;
  });

  describe('when component with directive is initialized', () => {

    it('should not render any element', () => {
      expect(getClones().length).toBe(0);
    });

    describe.each([
      {input: -1, inputText: '-1'},
      {input: -581, inputText: '-581'},
      {input: 0, inputText: '0'},
      {input: null, inputText: 'null'},
      {input: undefined, inputText: 'undefined'},
      {input: Infinity, inputText: 'Infinity'},
      {input: NaN, inputText: 'NaN'},
    ])('and $inputText value is passed', ({input}) => {
      beforeEach(() => {
        component.cloneNumber = input;
        fixture.detectChanges();
      });

      it('should not render any element', () => {
        expect(getClones().length).toBe(0);
      });
    });

    describe.each([
      {input: 1},
      {input: 99},
      {input: 19},
    ])('and $input value is passed', ({input}) => {
      beforeEach(() => {
        component.cloneNumber = input;
        fixture.detectChanges();
      });

      it(`should render ${ input } element(s)`, () => {
        expect(getClones().length).toBe(input);
      });
    });

    describe('and 5 value is passed', () => {
      beforeEach(() => {
        component.cloneNumber = 5;
        fixture.detectChanges();
      });

      it(`should render 5 elements`, () => {
        expect(getClones().length).toBe(5);
      });

      describe('and 9 value is passed', () => {
        beforeEach(() => {
          component.cloneNumber = 9;
          fixture.detectChanges();
        });

        it(`should render 9 elements`, () => {
          expect(getClones().length).toBe(9);
        });
      });

      describe('and undefined value is passed', () => {
        beforeEach(() => {
          component.cloneNumber = undefined;
          fixture.detectChanges();
        });

        it(`should not render any element`, () => {
          expect(getClones().length).toBe(0);
        });
      });
    });
  });

  const getClones = (): DebugElement[] => {
    return fixture.debugElement.queryAll(By.css('.clone'));
  }
});


@Component({
  selector: 'app-test',
  template: `
    <div class="clone" *appCloneElement="cloneNumber">I'm a clone</div>
  `,
})
export class TestComponent {
  cloneNumber: number | null | undefined;
}

Run the tests and you should see the results:

 PASS  src/app/directives/clone-element.directive.spec.ts
  CloneElementDirective
    when component with directive is initialized
      ✓ should not render any element
      and -1 value is passed
        ✓ should not render any element
      and -581 value is passed
        ✓ should not render any element
      and 0 value is passed
        ✓ should not render any element 
      and null value is passed
        ✓ should not render any element 
      and undefined value is passed
        ✓ should not render any element 
      and Infinity value is passed
        ✓ should not render any element 
      and NaN value is passed
        ✓ should not render any element
      and 1 value is passed
        ✓ should render 1 element(s) 
      and 99 value is passed
        ✓ should render 99 element(s) 
      and 19 value is passed
        ✓ should render 19 element(s) 
      and 5 value is passed
        ✓ should render 5 elements 
        and 9 value is passed
          ✓ should render 9 elements 
        and undefined value is passed
          ✓ should not render any element 

Test Suites: 1 passed, 1 total
Tests:       14 passed, 14 total

Source code

https://gitlab.com/barcioch-blog-examples/series-angular-testing-06-angular-directive-testing

More from this blog

CodeCraft with Barcioch

24 posts