Document Tree
Document Properties
Kbid
L30773
Last Modified
21-Dec-2023
Added to KB
22-Feb-2023
Public Access
Everyone
Status
Online
Doc Type
Guidelines
Product
Intershop Progressive Web App
Guide - Intershop Progressive Web App - Unit Testing with Jest

Stick to General Unit Testing Rules

Single Responsibility

A test should test only one thing.
One given behavior is tested in one and only one test.

The tests should be independent of each other, i.e. there is no need to chain them together or run them in any particular order.

Test Functionality - Not Implementation

A test is incorrectly implemented or the test scenario is meaningless if changes to the HTML structure of the component destroy the test result.

Example: The test fails if an additional input field is added to the form.

⚠️ Wrong Test Scenario

it('should check if input fields are rendered on HTML', () => {
  const inputFields = element.getElementsByClassName('form-control');
  expect(inputFields.length).toBe(4);
  expect(inputFields[0]).toBeDefined();
  expect(inputFields[1]).toBeDefined();
  expect(inputFields[2]).toBeDefined();
});

Do not Comment out Tests

Instead, use the xdescribe or xit feature (just add an x before the method declaration) to exclude tests.
This way, excluded tests are still visible as skipped and can be repaired later on.

✔️

xdescribe("description", function() {
  it("description", function() {
    ...
  });
});

Always Test the Initial State of a Service/Component/Module/...

With this approach, the test itself documents the initial behavior of the unit under test.
This is especially true if you are testing whether your action triggers a change: Test for the previous state!

✔️

it('should call the cache when data is available', () => {
    // precondition
    service.getData();
    expect(cacheService.getCachedData).not.toHaveBeenCalled();

    << change cacheService mock to data available >>

    // test again
    service.getData();
    expect(cacheService.getCachedData).toHaveBeenCalled();
});

Do not Test the Obvious

Testing should not be done for the sake of having tests:

  • It is not useful to test getter and setter methods and use spy on methods that are called directly later on.
  • Do not use assertions that are logically always true.

Make Stronger Assertions

It is easy to always test with toBeTruthy or toBeFalsy when you expect something as a return value, but it is better to use stronger assertions like toBeTrue, toBeNull or toEqual(12).

⚠️

it('should cache data with encryption', () => {
  customCacheService.storeDataToCache('My task is testing', 'task', true);
  expect(customCacheService.cacheKeyExists('task')).toBeTruthy();
});

✔️

it('should cache data with encryption', () => {
  customCacheService.storeDataToCache('My task is testing', 'task', true);
  expect(customCacheService.cacheKeyExists('task')).toBeTrue();
});

Again, do not rely too much on the implementation.
If user customizations can easily break the test code, your assertions are too strong.

⚠️ Test too Close to Implementation

it('should test if tags with their text are getting rendered on the HTML', () => {
  expect(element.getElementsByTagName('h3')[0].textContent).toContain('We are sorry');
  expect(element.getElementsByTagName('p')[0].textContent).toContain(
    'The page you are looking for is currently not available'
  );
  expect(element.getElementsByTagName('h4')[0].textContent).toContain('Please try one of the following:');
  expect(element.getElementsByClassName('btn-primary')[0].textContent).toContain('Search');
});

✔️ Same Test in a more Stable Way

it('should test if tags with their text are getting rendered on the HTML', () => {
  expect(element.getElementsByClassName('error-text')).toBeTruthy();
  expect(element.getElementsByClassName('btn-primary')[0].textContent).toContain('Search');
});

Do not Meddle with the Framework

Methods like ngOnInit() are lifecycle-hook methods which are called by Angular – The test should not call them directly.
When doing component testing, you most likely use TestBed anyway, so use the detectChanges() method of your available ComponentFixture.

⚠️ Wrong Test with ngOnInit() Method Calling

it('should call ngOnInit method', () => {
  component.ngOnInit();
  expect(component.loginForm).toBeDefined();
});

✔️ Test without ngOnInit() Method Call

it('should contain the login form', () => {
  fixture.detectChanges();
  expect(component.loginForm).not.toBeNull();
});

Assure Readability of Tests

Stick to Meaningful Naming

The test name perfectly describes what the test does.

⚠️ Wrong Naming

it ('wishlist test', () => {...})

✔️ Correct Naming

it ('should add a product to an existing wishlist when the button is clicked', () => {...})

Basically, it should read like a documentation of the unit under test, not a documentation of what the test does. Jasmine has named the methods accordingly.
Read it like `I am describing , it should when/if/on <condition/trigger> (because/to )`.

This also applies to assertions.
They should be readable like meaningful sentences.

⚠️

const result = accountService.isAuthorized();
expect(result).toBeTrue();

✔️

const authorized = accountService.isAuthorized();
expect(authorized).toBeTrue();

or directly

expect(accountService.isAuthorized()).toBeTrue();

Avoid Global Variables

Tests should define variables only in the scope where they are needed.
Do not define variables before describe or respective it methods.

Avoid Code Duplication in Tests

This increases readability of test cases.

  • Common initialization code of constants or sub-elements should be located in beforeEach methods.
  • When using TestBed, you can handle injection to variables in a separate beforeEach method.

⚠️

it('should create the app', async(() => {
    const fixture = TestBed.createComponent(AppComponent);
    const component = fixture.componentInstance;
    ...
});
it('should have the title "app"', async(() => {
    const fixture = TestBed.createComponent(AppComponent);
    const component = fixture.componentInstance;
   ...
});
it('should match the text passed in Header Component', async(() => {
    const fixture = TestBed.createComponent(AppComponent);
});

✔️

describe('AppComponent', () => {
    let translate: TranslateService;
    let fixture: ComponentFixture<AppComponent>;
    let component: AppComponent;

    beforeEach(async(() => {
        TestBed.configureTestingModule({
            declarations: [ ... ] });
        fixture = TestBed.createComponent(AppComponent);
        component = fixture.componentInstance;
    })
    it('should create the app', () => { ... });
    it(\`should have as title 'app'\`, () => { ... });
    it('should match the text passed in Header Component', () => { ... });
});

Do not Use Features You Do not Need

This increases readability of test cases.

If you do not need the following features, do not use them:

  • ComponentFixture.debugElement
  • TestBed
  • async, fakeAsync
  • inject

⚠️ Wrong Test With Useless Features (TestBed, ComponentFixture.debugElement)

it('should create the app', async(() => {
  const app = fixture.debugElement.componentInstance;
  expect(app).toBeTruthy();
}));

✔️ Same Test - Works Without These Features

it('should be created', () => {
  const app = fixture.componentInstance;
  expect(app).toBeTruthy();
});

Structure Long Tests

The describe methods in Jasmine are nestable.
You can use this to group various it methods into a nested describe where you can also use an additional beforeEach initialization method.

✔️ Nested describe Methods

describe('AccountLogin Component', () => {
    it('should be created', () => { ... });
    it('should check if controls are rendered on Login page', () => { ... });
    ....
    describe('Username Field', () => {
        it('should be valid when a correct email is assigned', () => { ... });
       ....
    });
});

Avoid Having Dead Code

Declare only what you need.
Unused variables, classes and imports reduce the readability of unit tests.

Use a Mocking Framework Instead of Dealing with Stubbed Classes

This way, less code needs to be implemented, which in turn increases the readability of unit tests.
Mocks can also be stubbed on time, depending on the current method.
We decided to use ts-mockito as our test mocking framework.

Do not Change Implementation to Satisfy Tests

DOM Element Selection

Use only IDs or definite class names to select DOM elements in tests.
Try to avoid general class names.

⚠️ Wrong Selector

const selectedLanguage = element.getElementsByClassName('d-none');

✔️ Correct Selector

// by id

const selectedLanguage = element.querySelector('#language-switch');

// by class

const selectedLanguage = element.getElementsByClassName('language-switch');

DOM Changes for Tests

Use data-testing-id via attribute binding to implement an identifier used for testing purpose only.

✔️ Correct Testing ID

*.component.html

<ol class="viewer-nav">
  <li *ngFor="let section of sections" [attr.data-testing-id]="section.value">{{ section.text }}</li>
</ol>

*.spec.ts

element.querySelectorAll('[data-testing-id]')[0].innerHTML;

element.querySelectorAll("[data-testing-id='en']").length;

Warning

Do not overuse this feature!

Stick to Intershop Conventions Regarding Angular Tests

Every Component Should Have a 'should be created' Test

Every component should have a 'should be created' test like the one Angular CLI auto-generates.
This test handles runtime initialization errors.

✔️

it('should be created', () => {
  expect(component).toBeTruthy();
  expect(element).toBeTruthy();
  expect(() => fixture.detectChanges()).not.toThrow();
});

Choose the Right Level of Abstraction

  • When working mainly with stubs for specific services that mock dependencies of services under test, you should mainly use spies to check whether the correct methods of the stub are called.
  • When working mainly with fully configured services, it is best to check return values.
  • When testing complex scenarios (e.g., when the test has to handle multiple pages), it may be better to implement a Geb+Spock end-to-end test.

See Three Ways to Test Angular Components for more information.

Be Aware of Common Pitfalls

Be Careful When Using toBeDefined

Be careful when using toBeDefined, because a dynamic language like JavaScript has another meaning of defined (see: Is It Defined? toBeDefined, toBeUndefined).

Warning

Do not use toBeDefined if you really want to check for not null because technically 'null' is defined. Use toBeTruthy instead.

Be Careful With Variable Initialization

Jasmine does not automatically reset all your variables for each test like other test frameworks do.
If you initialize directly under describe, the variable is initialized only once.

Warning

Since tests should be independent of each other, do not do this.

describe('...', () => {
  let a = true; // initialized just once
  const b = true; // immutable value
  let c; // re-initialized in beforeEach

  beforeEach(() => {
    c = true;
  });

  it('test1', () => {
    a = false;
    // b = false; not possible
    c = false;
  });

  it('test2', () => {
    // a is still false
    // c is back to true
  });
});

As shown in the above example, a shows the wrong way of initializing variables in tests.

If you do not need to change the value, use a const declaration for primitive variables like b.
If you need to change the value in some tests, make sure it is reinitialized each time in the beforeEach method like c.

A const declaration like b should not be used for complex values, as the object behind b is still mutable and needs to be reinitialized properly:

describe('...', () => {
  let a: any;
  const b = { value: true };

  beforeEach(() => {
    a = { value: true };
  });

  it('test1', () => {
    a.value = false;
    b.value = false;
  });

  it('test2', () => {
    // a.value is back to true
    // b.value is still false
  });
});

Use the Right Way to Test EventEmitter

Testing EventEmitter firing can be done in several ways, each with advantages and disadvantages.
Consider the following example:

import { EventEmitter } from '@angular/core';
import { anything, capture, deepEqual, spy, verify } from 'ts-mockito';

describe('Emitter', () => {
  class Component {
    valueChange = new EventEmitter<{ val: number }>();

    do() {
      this.valueChange.emit({ val: 0 });
    }
  }

  let component: Component;

  beforeEach(() => {
    component = new Component();
  });

  it('should detect errors using spy with extract', () => {
    // *1*
    const emitter = spy(component.valueChange);

    component.do();

    verify(emitter.emit(anything())).once();
    const [arg] = capture(emitter.emit).last();
    expect(arg).toEqual({ val: 0 });
  });

  it('should detect errors using spy with deepEqual', () => {
    // *2*
    const emitter = spy(component.valueChange);

    component.do();

    verify(emitter.emit(deepEqual({ val: 0 }))).once();
  });

  it('should detect errors using subscribe', done => {
    // *3*
    component.valueChange.subscribe(data => {
      expect(data).toEqual({ val: 0 });
      done();
    });

    component.do();
  });
});

As EventEmitter is Observable, subscribing to it might be the most logical way of testing it.
We, however, would recommend using ts-mockito to increase readability.
Ways 1 and 2 illustrate two options, we would recommend using the first one.

1 (preferred) 2 3
Description Using ts-mockito spy and then verify it has fired - Then check argument for expected value Using ts-mockito spy and then verify it has fired with the expected value Using subscription and asynchronous method safeguard
Readability Capturing arguments with ts-mockito might seem tricky and therefore reduces readability, but the test is done in the right order. ✔️ Right order, fewest lines of code ⚠️ Order is reversed.
In case it does not emit ✔️ Correct line number and a missing emission is reported. ✔️ Correct line number and a missing emission is reported. ⚠️ Test runs into timeout as the asynchronous callback is not called.
In case it emits another value ✔️ Correct line number and an incorrect value is reported. ⚠️ Missing emission is reported. ✔️ Correct line number and an incorrect value is reported.
Disclaimer
The information provided in the Knowledge Base may not be applicable to all systems and situations. Intershop Communications will not be liable to any party for any direct or indirect damages resulting from the use of the Customer Support section of the Intershop Corporate Web site, including, without limitation, any lost profits, business interruption, loss of programs or other data on your information handling system.
Home
Knowledge Base
Product Releases
Log on to continue
This Knowledge Base document is reserved for registered customers.
Log on with your Intershop Entra ID to continue.
Write an email to supportadmin@intershop.de if you experience login issues,
or if you want to register as customer.