Parametrize Angular Jasmine Tests

Functions that map a value from one form to another are useful since you can separate the mapping logic from things like services and components which are best kept as simple as possible. This allows for easy tests where you can check a range of conditions which might take a lot more boilerplate code if the logic was embedded in a component or service. It also allows you to re-use the mapping.

This does lead to simpler test cases, but because they are so simple the test boilerplate can become repetitive and overwhelm what is actually being tested. In practice, these types of tests are often copied and pasted which can lead to more maintenance overhead or outdated commentary around tests. To illustrate, consider the following mapping function.

Transformating Function

The following function takes in a number and returns numbers based on a series of conditional checks.
transform.ts

// transform.ts
export function transform(value: number): number {
  if (value === undefined) {
    return 0;
  }
  if (value === null) {
    return 0;
  }
  if (value < 0) {
    return -1;
  }
  if (value === 0) {
    return 0;
  }
  return 1;
}

Traditional Tests

To test the function you might come up with cases where the input is undefined, null, -10, -1, 0, 1 and 10. Those seem reasonable given the checks and boundaries in the tests. This would result in something like the following test file.
transform.spec.ts

// transform.spec.ts
import { transform } from './transform';

describe('transform', () => {
  describe('input value is undefined', () => {
    it('should return 0', () => {
      expect(transform(undefined)).toEqual(0);
    });
  });

  describe('input value is null', () => {
    it('should return 0', () => {
      expect(transform(null)).toEqual(0);
    });
  });

  describe('input value is -10', () => {
    it('should return -1', () => {
      expect(transform(-10)).toEqual(-1);
    });
  });

  describe('input value is -1', () => {
    it('should return -1', () => {
      expect(transform(-1)).toEqual(-1);
    });
  });

  describe('input value is 0', () => {
    it('should return 0', () => {
      expect(transform(0)).toEqual(0);
    });
  });

  describe('input value is 10', () => {
    it('should return 1', () => {
      expect(transform(10)).toEqual(1);
    });
  });

  describe('input value is 1', () => {
    it('should return 1', () => {
      expect(transform(1)).toEqual(1);
    });
  });
});

The key pieces of information in the tests is that undefined and null map to 0, negative numbers map to -1, 0 maps to 0 and positive numbers map to 1. Compare that simple sentence to 43 lines of code required to implement the tests.

Parametrized Tests

The following shows how the 43 lines of test code can be reduced to 18.
transform.parametrized.spec.ts

// transform.parametrized.spec.ts
import { transform } from './transform';

describe('transform parametrized tests', () => {
  // Defining input value and expected transformation
  [
    { inputValue: undefined, expectedValue: 0 },
    { inputValue: null, expectedValue: 0 },
    { inputValue: -10, expectedValue: -1 },
    { inputValue: -1, expectedValue: -1 },
    { inputValue: 0, expectedValue: 0 },
    { inputValue: 1, expectedValue: 1 },
    { inputValue: 10, expectedValue: 1 }
  ].forEach(({inputValue, expectedValue}) => {
    describe(`input is ${inputValue}`, () => {
      it(`should return ${expectedValue}`, () => {
        expect(transform(inputValue)).toEqual(expectedValue);
      });
    });
  });
});

Lines 7 to 13 clearly demonstrate the expected input to output mapping. Lines 15-19 demonstrate how the mapping is achieved. The forEach loop takes advantage of object destructing to assign an object from the array to parameters of the testing function on line 14. The describe and it functions are passed arguments that include the input and expected value, which makes reading and understanding the output of failed tests easier, on lines 15 and 16, respectively.

There are drawbacks to parametrizing tests and, in a lot of cases, can make tests confusing. In this case, the parameters and code to execute the test fit on the same page in a code editor, which makes it easy to understand. There are also few input parameters and the test logic is simple.

When the number of input parameters gets large, it makes the test harder to read because you have to jump back and forth between the parameter definition and the test code. If the test code is longer and complex, introducing parameters increases the complexity of the test even further. If parametrizing the test means you have to significantly increase the logic of the test, it means the test is hard to understand when you come back to it a few months down the track.

However, with few input parameters and a function for which little test logic is required, using parametrised tests reduces boilerplate code and makes the tests easier to understand. TypeScript also helps as it will infer the types of the input and expected parameters for you.

Testing the Testing Guard Decorator in Python

Testing functions in Python that have a decorator applied to them is not ideal as the tests have to be written to take into account the decorator. The testing guard decorator can help remove the problem by selectively running the function with or without the decorator by detecting the environment in which it is run. When you make the decorator a part of your project you might like to write tests for the testing guard decorator itself.

Testing Guard Decorator

As a reminder, the following is the decorator code for the testing guard.

# testing_guard.py
"""Demonstrates a guard decorator."""

import os


def testing_guard(decorator_func):
    """
    Decorator that only applies another decorator if the appropriate
    environment variable is not set.

    Args:
        decorator_func: The function that applies the decorator.

    Returns:
        Function that dynamically decides whether to apply the decorator based
        on the environment.
    """
    def replacement(original_func):
        """
        Function that is used as the decorator function instead of the
        decorator function.

        Args:
            original_func: The function being decorated.

        Returns:
            Function that dynamically picks between the original and
            decorated function depending on the environment.
        """
        # Creating decorated function
        decorated_func = decorator_func(original_func)

        def apply_guard(*args, **kwargs):
            """
            Dynamically picks between decorated and original function based on
            environment.
            """
            if os.getenv('TESTING') is not None:
                # Use original function
                return original_func(*args, **kwargs)
            # Use decorated function
            return decorated_func(*args, **kwargs)

        return apply_guard

    return replacement

The key pieces of functionality are on lines 39-43. Line 39 is the check for whether the function is being executed in the testing environment. If it is, the original function is called without the decorator on line 41. If it isn’t, then the decorated function is called on line 43. The key reason that the decorator works is that it gets access to both the decorator and original function and that it gets to run code every time the decorated function is run.

Testing the Testing Guard Decorator

There are 2 scenarios to test. The first is that the decorator is executed in the test environment, and the second is when it is executed in the non-test environment. In both cases we care about both that the correct function is called with the correct arguments (and that the other function is not called) and that the return value from the correct function is returned.

Let’s first define 2 fixtures that will help during testing. We will repeatedly need some generic *args and **kwargs and the particular value we choose for either doesn’t add anything to help understand the tests themselves so it is better to hide that detail in a fixture.

@pytest.fixture(scope='session')
def args():
    """Generates function *args"""
    return ('arg1', 'arg2')


@pytest.fixture(scope='session')
def kwargs():
    """Generates function **kwargs"""
    return {'kwargs1': 'kwarg1', 'kwarg2': 'kwarg2'}

Test Environment

When the decorator is executed in the test environment, it should call the original function and return the return value of the original function. It should not call the decorator return value. To focus the tests, let’s split those into three separate tests.

The first test just checks that the decorator return value is not called:

def test_testing_guard_set_decorated_call(monkeypatch):
    """
    GIVEN TESTING environment variable is set and mock decorator
    WHEN decorator is applied to a function after decorating it with the
        testing guard and calling the decorated function
    THEN decorator return value is not called.
    """
    # Setting TESTING environment variable
    monkeypatch.setenv('TESTING', '')
    # Defining mock decorator
    mock_decorator = mock.MagicMock()

    # Decorating with testing guard and calling
    guarded_mock_decorator = testing_guard(mock_decorator)
    # Applying decorator
    mock_decorated_func = guarded_mock_decorator(mock.MagicMock())
    # Calling function
    mock_decorated_func()

    # Checking decorator call
    mock_decorator.return_value.assert_not_called()

To understand the test, some understanding of Python decorators is required. A decorator is another function whose return value replaces the function it is decorating. A decorator could, for example, return the print function and then the function being decorator would never be called. In most cases, however, the function being decorator is called in the body of the function the decorator returns.

On line 9 of the test the testing environment is setup. On line 11 a mock function that will serve as the decorator to which the testing guard is applied is defined. On line 14 the testing guard is applied to the mock decorator. Then a mock function is decorated with guarded mock decorator on line 16 and the decorated function is called on line 18. On line 21 it is checked that the return value of the decorator, which would usually be called instead of the original function, is not called since, in the testing environment, the original function should always be called.

The next test checks that the original function is called with the correct arguments. For this we will ned the args and kwargs fixtures.

def test_testing_guard_set_func_call(monkeypatch, args, kwargs):
    """
    GIVEN TESTING environment variable is set, mock function and args and
        kwargs
    WHEN a decorator is applied to the function after decorating it with the
        testing guard and calling the decorated function with args and kwargs
    THEN function is called with args and kwargs.
    """
    # Setting TESTING environment variable
    monkeypatch.setenv('TESTING', '')
    # Defining mock decorator
    mock_func = mock.MagicMock()

    # Decorating with testing guard and calling
    mock_decorator = mock.MagicMock()
    guarded_mock_decorator = testing_guard(mock_decorator)
    # Applying decorator
    mock_decorated_func = guarded_mock_decorator(mock_func)
    # Calling function
    mock_decorated_func(*args, **kwargs)

    # Checking decorator call
    mock_func.assert_called_once_with(*args, **kwargs)

This tests is very similar to the first test but, instead of keeping track of the decorator, the original function is defined on line 12. The procedure on lines 14-20 is very similar to the first test with the only difference being that the mock decorated function is called with the args and kwargs fixtures. On line 23 the original function call is checked.

The final test checks the return value of the mock decorated function call. It is much like the previous test, except that it doesn’t pass in any args nor kwargs.

def test_testing_guard_set_return(monkeypatch):
    """
    GIVEN TESTING environment variable is set and mock function
    WHEN a decorator is applied to the function after decorating it with the
        testing guard and calling the decorated function
    THEN the return value is the function return value.
    """
    # Setting TESTING environment variable
    monkeypatch.setenv('TESTING', '')
    # Defining mock function
    mock_func = mock.MagicMock()

    # Decorating with testing guard and calling
    mock_decorator = mock.MagicMock()
    guarded_mock_decorator = testing_guard(mock_decorator)
    # Applying decorator
    mock_decorated_func = guarded_mock_decorator(mock_func)
    # Calling function
    return_value = mock_decorated_func()

    # Checking decorator call
    assert return_value == mock_func.return_value

Non-Test Environment

The second series of tests are outside the test environment. Since the tests for the testing guard are likely running inside the test environment, it is usually best to deactivate the testing environment as a part of the test as you may want to have the testing environment active by default during your tests.

The tests are very similar to the tests in the test environment. The difference is that now the decorator return value should be called, the original function should not be called and the return value of the decorator return value should be returned. The tests are shown below.

def test_testing_guard_not_set_decorated_call(monkeypatch, args, kwargs):
    """
    GIVEN TESTING environment variable is not set, mock decorator and args and
        kwargs
    WHEN decorator is applied to a function after decorating it with the
        testing guard and calling the decorated function with args and kwargs
    THEN decorator return value is called with args and kwargs.
    """
    # Removing TESTING environment variable
    monkeypatch.delenv('TESTING', raising=False)
    # Defining mock decorator
    mock_decorator = mock.MagicMock()

    # Decorating with testing guard and calling
    guarded_mock_decorator = testing_guard(mock_decorator)
    # Applying decorator
    mock_decorated_func = guarded_mock_decorator(mock.MagicMock())
    # Calling function
    mock_decorated_func(*args, **kwargs)

    # Checking decorator call
    mock_decorator.return_value.assert_called_once_with(*args, **kwargs)


def test_testing_guard_not_set_return(monkeypatch):
    """
    GIVEN TESTING environment variable is not set and mock decorator
    WHEN decorator is applied to a function after decorating it with the
        testing guard and calling the decorated function
    THEN the return value is the decorator's return value return value.
    """
    # Removing TESTING environment variable
    monkeypatch.delenv('TESTING', raising=False)
    # Defining mock decorator
    mock_decorator = mock.MagicMock()

    # Decorating with testing guard and calling
    guarded_mock_decorator = testing_guard(mock_decorator)
    # Applying decorator
    mock_decorated_func = guarded_mock_decorator(mock.MagicMock())
    # Calling function
    return_value = mock_decorated_func()

    # Checking decorator call
    assert return_value == mock_decorator.return_value.return_value


def test_testing_guard_not_set_func_call(monkeypatch):
    """
    GIVEN TESTING environment variable is not set and mock function
    WHEN a decorator is applied to the function after decorating it with the
        testing guard and calling the decorated function
    THEN function is not called.
    """
    # Removing TESTING environment variable
    monkeypatch.delenv('TESTING', raising=False)
    # Defining mock decorator
    mock_func = mock.MagicMock()

    # Decorating with testing guard and calling
    mock_decorator = mock.MagicMock()
    guarded_mock_decorator = testing_guard(mock_decorator)
    # Applying decorator
    mock_decorated_func = guarded_mock_decorator(mock_func)
    # Calling function
    mock_decorated_func()

    # Checking decorator call
    mock_func.assert_not_called()

This demonstrates how to test the testing guard. The setup and teardown of your test environment might be more complicated in which case your tests would have to reflect that.

Testing RxJS Service with a Subject Component Interaction

The service with a subject pattern is a good entry point into the RxJS world of observables in Angular. When you are using RxJS, writing traditional tests can be cumbersome. There are great tools, such as jasmine-marbles, that help make these kinds of tests a lot easier. A great way to get started with jasmine-marbles is testing the service with a subject when the service only performs HTTP calls. However, when the service also accepts updates from components, writing these tests can be more difficult as you have to bridge the gap between the timing of function calls and observables.

The Service under Test

To illustrate a starting point for how to write these kinds of tests, consider a service that supports a component that allows the user to pick the sauce they would like to have on their burger 🍔. You might come up with a sauce service to keep track of which sauce was picked, such as the one shown below.
sauce.service.ts

// sauce.service.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class SauceService {
  private mSauce$ = new BehaviorSubject<string>('initial sauce');

  constructor() {}

  sauce$(): Observable<string> {
    return this.mSauce$;
  }

  setSauce(sauce: string) {
    this.mSauce$.next(sauce);
  }
}

Testing the Service

There are two things you may be interested in testing. The first is that the initial sauce is set correctly and the second is that, on calling setSauce, that the new sauce is emitted by sauce$. Both of these can be tested using jasmine-marbles. Without jasmine-marbles, you would have to subscribe to sauce$, check that the correct sauces are emitted and make use of the done function. With jasmine-marbles this can be simplified.

To see how, you need to understand that the jasmine-marbles cold function simply creates an observable. Just like any other observable, you can subscribe to it. So if you define an observable that emits the sauces that you want to call setSauce with using cold, subscribe to that observable and call setSauce with the sauces that you receive, you have bridged the gap between function calls an observables. The marble syntax allows you to specify the timing of the setSauce calls. The tests could like like what is shown below.
sauce.service.spec.ts

// sauce.service.spec.ts
import { cold } from 'jasmine-marbles';

import { SauceService } from './sauce.service';


describe('SauceService', () => {
  let service: SauceService;

  beforeEach(() => {
    service = new SauceService();
  });

  describe('construction', () => {
    it('should emit initial sauce', () => {
      // Setting up expected observable
      const expectedMarbles = 'a';
      const expected = cold(expectedMarbles, {a: 'initial sauce'});

      // Checking sauce$
      expect(service.sauce$()).toBeObservable(expected);
    });
  });

  describe('setSauce', () => {
    it('should emit new sauce', () => {
      // Defining marbles
      const setSauceCallMarbles = '-a';
      const expectedMarbles =     'bc';

      // Setting up setSauce call
      cold(setSauceCallMarbles, {a: 'new sauce'})
        .subscribe(sauce => service.setSauce(sauce));

      // Checking sauce$
      const expected = cold(expectedMarbles, {b: 'initial sauce', c: 'new sauce'});
      expect(service.sauce$()).toBeObservable(expected);
    });
  });
});

The test on lines 15-22 checks that the initial sauce is emitted after construction. The interesting test is on lines 26-39. On line 28 the timing of the setSauce calls is defined. In this case, it is setup so that the first setSauce call occurs one frame after the construction is complete. On lines 32-33 the input to the setSauce call is defined and the plumbing to perform the setSauce calls is specified. On lines 36-37 the standard work is being done to check the values emitted by sauce$.

This technique is more concise and easy to understand than messing around with observables, subscriptions and the done function. There are still things that could be better. For example, it would be great if jasmine-marbles could come up with a standardised way of performing this technique (maybe they already have and I just don’t know about it). You may not feel like using this technique all the time, depending on the complexity of the logic and its dependence on timing. However, knowing about it and how it helps you can be valuable when you encounter the need for finer control over timing of function calls in tests.

Testing Angular Material Basic Spreadsheet

Previously, you had an idea to move a spreadsheet from an email trail onto a website. To achieve that, you used an Angular Material Basic Spreadsheet. You showed it to your manager and he thought it had potential if you add a few more features. Before going ahead with that, you realise that, to achieve complexity scale, you will have to start writing some tests.

As a reminder, the key elements of the Angular Material Basic Spreadsheet are the component that displays individual orders. This component is used by an Angular Material table based table component that maps a number of orders onto the screen similar to a spreadsheet based on the date and product of the order, as shown below.

The component to be tested.

Testing Individual Order Component

The following is the code and template behind the individual order component.
order.component.ts
order.component.html

// order.component.ts
import { Component, OnInit, Input } from '@angular/core';

import { Order } from './order.model';

@Component({
  selector: 'app-order',
  templateUrl: './order.component.html',
  styleUrls: ['./order.component.css']
})
export class OrderComponent implements OnInit {
  @Input() order: Order;

  constructor() { }

  ngOnInit() {
  }

}
<!-- order.component.html -->
<ng-container *ngIf="order">{{ order.value | number }}</ng-container>

To figure out the tests that need to be written, let’s think about what could go wrong. Firstly, we could have forgotten to import something or setup something incorrectly. Next, we could have assumed that anyone that uses the component will always pass in an order. Thirdly, when an order is passed in, we could have forgotten to use the number pipe to display the value. The following specification tests for those three cases.
order.component.spec.ts

// order.component.spec.ts
import { TestBed, ComponentFixture } from '@angular/core/testing';

import { OrderComponent } from './order.component';
import { DebugElement } from '@angular/core';

describe('OrderComponent', () => {
  let fixture: ComponentFixture<OrderComponent>;
  let component: OrderComponent;
  let debugElement: DebugElement;

  beforeEach(() => {
    TestBed.configureTestingModule({
      declarations: [OrderComponent]
    });

    fixture = TestBed.createComponent(OrderComponent);
    component = fixture.componentInstance;
    debugElement = fixture.debugElement;
  });

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

  describe('order null', () => {
    it('should display empty template.', () => {
      // Setting order to null
      component.order = null;

      // Triggering change detection
      fixture.detectChanges();

      // Checking template
      expect(debugElement.nativeElement.textContent).toEqual('');
    });
  });

  describe('order not null', () => {
    it('should display order value formatted as number.', () => {
      // Setting order to null
      component.order = {date: '2000-01-01', product: 'product 1', value: 1000};

      // Triggering change detection
      fixture.detectChanges();

      // Checking template
      expect(debugElement.nativeElement.textContent).toEqual('1,000');
    });
  });
});

On lines 8-20 the standard setup for component tests is defined. The test on lines 22-25 checks that the component is created. The test on lines 27-38 checks what happens if the input Order is null. Finally, the test on lines 40-51 checks that the Order value is displayed using the number pipe.

Here the DebugElement is used to inspect the template. Since the template is relatively simple, we only ever check the text content of the whole template under the different test cases.

Testing Table Component

The following is the code and template for the order table component.
order-table.component.ts
order-table.component.html

// order-table.component.ts
import { Component, OnInit } from '@angular/core';

import { OrderTable } from './order-table.model';
import { OrderService } from './order.service';

@Component({
  selector: 'app-order-table',
  templateUrl: './order-table.component.html',
  styleUrls: ['./order-table.component.css']
})
export class OrderTableComponent implements OnInit {
  orderTable: OrderTable;
  productHeader = 'Product';
  headers: string[];

  constructor(private orderService: OrderService) { }

  ngOnInit() {
    this.orderTable = this.orderService.getOrderTable();
    this.headers = [this.productHeader].concat(this.orderTable.headers);
  }

}
<!-- order-table.component.html -->
<ng-container *ngIf="orderTable.rowLabels.length">
  <table mat-table [dataSource]="orderTable.rowLabels" class="mat-elevation-z8">
    <ng-container [matColumnDef]="header" *ngFor="let header of headers">
      <th mat-header-cell *matHeaderCellDef>
        <ng-container *ngIf="header === productHeader">{{ header }}</ng-container>
        <ng-container *ngIf="header !== productHeader">{{ header | date }}</ng-container>
      </th>
      <td mat-cell *matCellDef="let element">
        <ng-container *ngIf="header === productHeader">{{ element }}</ng-container>
        <ng-container *ngIf="header !== productHeader"><app-order [order]="orderTable.orderMap[header][element]"></app-order></ng-container>
      </td>
    </ng-container>

    <tr mat-header-row *matHeaderRowDef="headers"></tr>
    <tr mat-row *matRowDef="let row; columns: headers;"></tr>
  </table>
</ng-container>

The table component is more complex and will require more tests. Broadly, there are tests for the component class and for the template. The main test for the component class is that the getOrderTable function is called on initialisation. Ideally, component code is tested without the use of the TestBed as it tends to slow down tests. However, since the component code to be tested is linked to an Angular event (OnInit), in this case it is better to use the TestBed and, therefore, the standard Angular lifecycle functions. The template tests are complex due to the number of ngIf statements and other logic in the template. Additionally, there are some complexities testing Angular Material tables. The following is the test code that checks common cases.
order-table.component.spec.ts

// order-table.component.spec.ts
import { TestBed, ComponentFixture } from '@angular/core/testing';
import { Component, Input } from '@angular/core';
import { MatTableModule } from '@angular/material';

import { OrderTableComponent } from './order-table.component';
import { Order } from './order.model';
import { OrderService } from './order.service';

@Component({
  selector: 'app-order',
  template: `
    {{ order.date }}
    {{ order.product }}
    {{ order.value }}
  `
})
class TestOrderComponent {
  @Input() order: Order;
}

describe('OrderTableComponent', () => {
  let fixture: ComponentFixture<OrderTableComponent>;
  let component: OrderTableComponent;
  let nativeElement: HTMLElement;
  let orderServiceSpy: jasmine.SpyObj<OrderService>;

  beforeEach(() => {
    // Creating mock OrderService
    orderServiceSpy = jasmine.createSpyObj('OrderService', ['getOrderTable']);

    TestBed.configureTestingModule({
      declarations: [OrderTableComponent, TestOrderComponent],
      providers: [{provide: OrderService, useValue: orderServiceSpy}],
      imports: [MatTableModule]
    });

    fixture = TestBed.createComponent(OrderTableComponent);
    component = fixture.componentInstance;
    nativeElement = fixture.nativeElement;
  });

  describe('TestOrderComponent', () => {
    it('should create', () => {
      // Defining getOrderTable return value
      orderServiceSpy.getOrderTable.and.returnValue({
        headers: [],
        rowLabels: [],
        orders: [],
        orderMap: {}
      });

      // Triggering change detection
      fixture.detectChanges();

      // Checking component create
      expect(component).toBeTruthy();
    });

    describe('construction', () => {
      it('should call getOrderTable on OrderService', () => {
        // Defining getOrderTable return value
        orderServiceSpy.getOrderTable.and.returnValue({
          headers: [],
          rowLabels: [],
          orders: [],
          orderMap: {}
        });

        // Triggering change detection
        fixture.detectChanges();

        // Checking getOrderTable call
        expect(orderServiceSpy.getOrderTable).toHaveBeenCalledWith();
      });
    });

    describe('empty table', () => {
      it('should display empty template.', () => {
        // Defining getOrderTable return value
        orderServiceSpy.getOrderTable.and.returnValue({
          headers: [],
          rowLabels: [],
          orders: [],
          orderMap: {}
        });

        // Triggering change detection
        fixture.detectChanges();

        // Checking template
        expect(nativeElement.textContent).toEqual('');
      });
    });

    describe('single item table', () => {
      it('should display Product and date header', () => {
        // Defining getOrderTable return value
        const order = {
          date: '2000-01-01',
          product: 'product 1',
          value: 1
        };
        orderServiceSpy.getOrderTable.and.returnValue({
          headers: ['2000-01-01'],
          rowLabels: ['product 1'],
          orders: [order],
          orderMap: {'2000-01-01': {'product 1': order}}
        });

        // Triggering change detection
        fixture.detectChanges();

        // Checking headers in template
        const trs = nativeElement.querySelectorAll('tr');
        expect(trs.length).toEqual(2);
        const ths = trs.item(0).querySelectorAll('th');
        expect(ths.length).toEqual(2);
        ['Product', 'Jan 1, 2000'].forEach((expectedHeader, idx) => expect(ths.item(idx).innerText).toEqual(expectedHeader));
      });

      it('should display single row with Order', () => {
        // Defining getOrderTable return value
        const order = {
          date: '2000-01-01',
          product: 'product 1',
          value: 1
        };
        orderServiceSpy.getOrderTable.and.returnValue({
          headers: ['2000-01-01'],
          rowLabels: ['product 1'],
          orders: [order],
          orderMap: {'2000-01-01': {'product 1': order}}
        });

        // Triggering change detection
        fixture.detectChanges();

        // Checking headers in template
        const trs = nativeElement.querySelectorAll('tr');
        expect(trs.length).toEqual(2);
        const tds = trs.item(1).querySelectorAll('td');
        expect(tds.length).toEqual(2);
        ['product 1', '2000-01-01 product 1 1'].forEach((expectedItem, idx) => expect(tds.item(idx).innerText).toEqual(expectedItem));
      });
    });

    describe('multiple item table with single row', () => {
      it('should display Product and date headers', () => {
        // Defining getOrderTable return value
        const orders = [{
          date: '2000-01-01',
          product: 'product 1',
          value: 1
        }, {
          date: '2000-01-02',
          product: 'product 2',
          value: 2
        }];
        orderServiceSpy.getOrderTable.and.returnValue({
          headers: ['2000-01-01', '2000-01-02'],
          rowLabels: ['product 1'],
          orders: orders,
          orderMap: {
            '2000-01-01': {'product 1': orders[0]},
            '2000-01-02': {'product 1': orders[1]}
          }
        });

        // Triggering change detection
        fixture.detectChanges();

        // Checking headers in template
        const trs = nativeElement.querySelectorAll('tr');
        expect(trs.length).toEqual(2);
        const ths = trs.item(0).querySelectorAll('th');
        expect(ths.length).toEqual(3);
        [
          'Product', 'Jan 1, 2000', 'Jan 2, 2000'
        ].forEach((expectedHeader, idx) => expect(ths.item(idx).innerText).toEqual(expectedHeader));
      });

      it('should display single row with Orders', () => {
        // Defining getOrderTable return value
        const orders = [{
          date: '2000-01-01',
          product: 'product 1',
          value: 1
        }, {
          date: '2000-01-02',
          product: 'product 1',
          value: 2
        }];
        orderServiceSpy.getOrderTable.and.returnValue({
          headers: ['2000-01-01', '2000-01-02'],
          rowLabels: ['product 1'],
          orders: orders,
          orderMap: {
            '2000-01-01': {'product 1': orders[0]},
            '2000-01-02': {'product 1': orders[1]}
          }
        });

        // Triggering change detection
        fixture.detectChanges();

        // Checking headers in template
        const trs = nativeElement.querySelectorAll('tr');
        expect(trs.length).toEqual(2);
        const ths = trs.item(1).querySelectorAll('td');
        expect(ths.length).toEqual(3);
        [
          'product 1', '2000-01-01 product 1 1', '2000-01-02 product 1 2'
        ].forEach((expectedItem, idx) => expect(ths.item(idx).innerText).toEqual(expectedItem));
      });
    });

    describe('multiple item table with single column', () => {
      it('should display Product and date header', () => {
        // Defining getOrderTable return value
        const orders = [{
          date: '2000-01-01',
          product: 'product 1',
          value: 1
        }, {
          date: '2000-01-01',
          product: 'product 1',
          value: 2
        }];
        orderServiceSpy.getOrderTable.and.returnValue({
          headers: ['2000-01-01'],
          rowLabels: ['product 1', 'product 2'],
          orders: orders,
          orderMap: {'2000-01-01': {
            'product 1': orders[0],
            'product 2': orders[1]
          }}
        });

        // Triggering change detection
        fixture.detectChanges();

        // Checking headers in template
        const trs = nativeElement.querySelectorAll('tr');
        expect(trs.length).toEqual(3);
        const ths = trs.item(0).querySelectorAll('th');
        expect(ths.length).toEqual(2);
        ['Product', 'Jan 1, 2000'].forEach((expectedHeader, idx) => expect(ths.item(idx).innerText).toEqual(expectedHeader));
      });

      it('should display multiple rows with Orders', () => {
        // Defining getOrderTable return value
        const orders = [{
          date: '2000-01-01',
          product: 'product 1',
          value: 1
        }, {
          date: '2000-01-01',
          product: 'product 2',
          value: 2
        }];
        orderServiceSpy.getOrderTable.and.returnValue({
          headers: ['2000-01-01'],
          rowLabels: ['product 1', 'product 2'],
          orders: orders,
          orderMap: {'2000-01-01': {
            'product 1': orders[0],
            'product 2': orders[1]
          }}
        });

        // Triggering change detection
        fixture.detectChanges();

        // Checking headers in template
        const trs = nativeElement.querySelectorAll('tr');
        expect(trs.length).toEqual(3);
        let tds = trs.item(1).querySelectorAll('td');
        expect(tds.length).toEqual(2);
        ['product 1', '2000-01-01 product 1 1'].forEach((expectedItem, idx) => expect(tds.item(idx).innerText).toEqual(expectedItem));
        tds = trs.item(2).querySelectorAll('td');
        expect(tds.length).toEqual(2);
        ['product 2', '2000-01-01 product 2 2'].forEach((expectedItem, idx) => expect(tds.item(idx).innerText).toEqual(expectedItem));
      });
    });
  });
});

The test setup is done on lines 23-41. The tests on lines 43-76 check the code of the component. The tests on lines 78-286 check the template under a range of scenarios.

Test Setup for Angular Material Table

There are a few interesting pieces to highlight for the tests. As the OrderTemplateComponent has a dependency on the OrderComponent, it needs to be given to the TestBed along with the OrderTableComponent. For testing, it is better to define a mock implementation of the component rather than use the real component, which is done on lines 10-20.

At the time of writing, the DebugElement does not work with the Angular Material table. This means that, to figure out exactly which OrderComponent is displayed in a cell, we need to make use of the template. Each OrderComponent displays an Order. Therefore, it should be enough to display all the member variables of the Order in the template of the mock OrderComponent to know that the correct Order was passed.

Since the template of the OrderTableComponent makes use of the Angular Material table, we need to pass in the MatTableModule to the TestBed.

Component Code Tests

The tests on lines 43-76 check that the component can be constructed and that the getOrderTable function is called on the OrderService during component initialisation. In this case, the component create test is a little more complicated because the OrderService needs to be called during initialisation which means that the getOrderTable function must at least exist. Also, during the initialisation, the headers member variable is used on the returned value of getOrderTable, so we may as well return an empty OrderTable.

Component Template Tests

The main reason for the length of the tests are the definition of the OrderTable to display in each test. There is a trade off between pre-defining or repeatedly defining them in each test. In production code brevity is preferred, so it is often better to define a range of OrderTables and importing them. However, for tests, it is better to have a single test be as much of a complete story as is needed to understand the test. As what is displayed in the template is tightly linked to the OrderTable, there are significant advantages to explicitly stating the OrderTable in each test.

The test scenarios are an empty table, a single item table and tests where either multiple columns or rows are displayed. In each case, the headers and rows are checked.

With these tests you have made an important step towards being able to scale up the complexity of the website. Now it is time to work on additional features! The most important feature request your manager has made is to make the spreadsheet editable so that people can revise the orders that were placed over time.

Angular Material Basic Spreadsheet

Spreadsheets are the lifeblood of many organisations. For example, a fruit company might be keeping track of the number of orders for Apples and Oranges over time using a spreadsheet. To scale, the organisation might have chosen to create 2 teams, one that looks after orders for Apples and the other for Oranges. Management wants a wholistic view of orders, so it instructs you to gather sales of Apples and Oranges from the two teams and aggregate the sales in a single spreadsheet. A trail of emails starts with many attached spreadsheets to keep track of the sales. Arguments ensue on whether the spreadsheet was updated correctly, who updated it and when.

What if there is a simpler solution? You decide to try building an internal website that keeps track of the sales. As a starting point, you only want the website to display orders broken down by product (Apples and Oranges) and date. You decide to try to use work that other people have already done for you. In particular, you decide to use @angular/material. To focus your mind on what you want to achieve, you draw up a draft of what you would like the website to look like:

Draft of what the website should look like.

Order Model

The first step is to define a model for an order. A simple model would keep track of the product, the date of the order and the amount that was ordered.
order.model.ts

// order.model.ts
export class Order {
    date: string;
    product: string;
    value: number;
}

Displaying the Order

To get a feeling for the model, you decide to write a component that can display a single Order. For now, it will be fairly basic and only display the value of the order, formatted as a number.
order.component.ts
order.component.html

// order.component.ts
import { Component, OnInit, Input } from '@angular/core';

import { Order } from './order.model';

@Component({
  selector: 'app-order',
  templateUrl: './order.component.html',
  styleUrls: ['./order.component.css']
})
export class OrderComponent implements OnInit {
  @Input() order: Order;

  constructor() { }

  ngOnInit() {
  }

}

The component code is simple, it just accepts an Order as input.

<!-- order.component.html -->
<ng-container *ngIf="order">
  {{ order.value | number }}
</ng-container>

In the template, you decide to check whether the order is truthy (ie. it has actually been passed in) using the *ngIf statement on line 2 and only display the value in that case.

Order Table Model

You anticipate the need to display multiple orders in a table. This table needs to be displayed by the Angular Material table component. To keep the template as simple as possible, you decide to use the following table model.
order-table.model.ts

// order-table.model.ts
import { Order } from './order.model';

export class OrderTable {
    headers: string[];
    rowLabels: string[];
    orders: Order[];
    orderMap: {[header: string]: {[rowLabel: string]: Order}};
}

It is designed to keep track of the orders in the table using the orders member variable. The ordering of the headers and rows is defined by headers and rowLabels member variables, respectively. To link a header and row label to and Order, you use the orderMap member variable.

Order Service

As you still need to convince your management and the teams you work with that the website is a good idea, for demonstration purposes, you decide to create a service that can easily be extended in the future, but that returns a hardcoded table for now.
order.service.ts

// order.service.ts
import { Injectable } from '@angular/core';

import { Order } from './order.model';
import { OrderTable } from './order-table.model';

@Injectable({
  providedIn: 'root'
})
export class OrderService {

  constructor() { }

  getOrderTable(): OrderTable {
    const orders: Order[] = [
      {date: '2000-01-01', product: 'Apples', value: 1000},
      {date: '2000-02-01', product: 'Apples', value: 2000},
      {date: '2000-01-01', product: 'Oranges', value: 10000}
    ];
    return {
      headers: ['2000-01-01', '2000-02-01'],
      rowLabels: ['Apples', 'Oranges'],
      orders: orders,
      orderMap: {
        '2000-01-01': {
          'Apples': orders[0],
          'Oranges': orders[2]
        },
        '2000-02-01': {
          'Apples': orders[1]
        }
      }
    };
  }
}

For the moment it returns 3 static orders, 2 for Apples and 1 for Oranges.

Displaying the Order Table

To be able to use @angular/material, you need to install some packages as shown in the getting started guide. To be able to use the Angular Material table, you need to import the MatTableModule and put it into the imports array of the module which will display the table.

The component code needs to make use of the OrderService to get the order table. It also needs to define the heading for the product name. The component template needs to dynamically generate headings and rows, depending on the number of Orders that need to be displayed.
table.component.ts
table.component.html

// table.component.ts
import { Component, OnInit } from '@angular/core';

import { OrderTable } from './order-table.model';
import { OrderService } from './order.service';

@Component({
  selector: 'app-order-table',
  templateUrl: './order-table.component.html',
  styleUrls: ['./order-table.component.css']
})
export class OrderTableComponent implements OnInit {
  orderTable: OrderTable;
  productHeader = 'Product';
  headers: string[];

  constructor(private orderService: OrderService) { }

  ngOnInit() {
    this.orderTable = this.orderService.getOrderTable();
    this.headers = [this.productHeader].concat(this.orderTable.headers);
  }

}

Line 14 defines the header under which the product will be displayed. In the component initialiser on line 20, the OrderTable is loaded. On line 21 the headers of the table are concentrated with the header for the product.

<!-- order-table.component.html -->
<table mat-table [dataSource]="orderTable.rowLabels" class="mat-elevation-z8">
  <ng-container [matColumnDef]="header" *ngFor="let header of headers">
    <th mat-header-cell *matHeaderCellDef>
      <ng-container *ngIf="header === productHeader">{{ header }}</ng-container>
      <ng-container *ngIf="header !== productHeader">{{ header | date }}</ng-container>
    </th>
    <td mat-cell *matCellDef="let element">
      <ng-container *ngIf="header === productHeader">{{ element }}</ng-container>
      <ng-container *ngIf="header !== productHeader"><app-order [order]="orderTable.orderMap[header][element]"></app-order></ng-container>
    </td>
  </ng-container>

  <tr mat-header-row *matHeaderRowDef="headers"></tr>
  <tr mat-row *matRowDef="let row; columns: headers;"></tr>
</table>

The table template is the most complicated piece of the puzzle. Line 2 defines that the data source for the table are the row labels of the table. This allows you to dynamically generate a row for each row label in the table. Line 14 defines how the header columns are defined and line 15 defines how each row puts data into the columns. Line 3 is the generator expression for each column of the table.

Lines 4-7 define how the header row is generated. Line 5 checks whether the current header is the product header, and if so displays the product header. Line 6 checks the opposite and displays the header after passing it through the date pipe.

Lines 8-11 define how the data rows are generated. Similar to the header row, line 9 checks for the product header and then displays the product. Line 10 checks for the opposite, picks out the Order based on the header and row label using the orderMap and passes on the Order to be displayed by the OrderComponent.

Now you have a working website that you can take to your management and the teams you work with as a showcase. They will probably ask you for additional features, such as being able to edit orders and add new orders to the table. To be able to deliver those more complex features, you also start thinking about how you might write tests for what you have created so far.

Testing Reactive Components in Angular

The Service with a Subject pattern in Angular is a good way to get started with reactive programming (ie. making use of RxJS) in Angular. The following article describes how to get started with the pattern: Getting Started with Service with a Subject in Angular.

That pattern leads to writing reactive components for which standard Angular component testing would lead to more complicated tests. With some changes to the standard Angular component testing methodologies, the tests can be simplified significantly.

Reactive Component under Test

For reference, the following is the typescript and HTML template for the component.
name.component.ts

// name.component.ts
import { Component, OnInit } from '@angular/core';

import { NameService } from './name.service';

@Component({
  selector: 'app-name',
  templateUrl: './name.component.html',
  styleUrls: ['./name.component.css']
})
export class NameComponent implements OnInit {
  constructor(public nameService: NameService) { }

  ngOnInit() {
    this.nameService.loadName();
  }

}

name.component.html

<!-- name.component.html -->
<ng-container *ngIf="nameService.name$() | async as name">
  {{ name }}
</ng-container>

Since the component depends on a reactive service, for reference, the following is sample code for the reactive service. For testing the reactive service see Testing Reactive Service with a Subject in Angular.
name.service.ts

// name.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable, of } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class NameService {
  private mName$ = new BehaviorSubject<string>(null);

  name$(): Observable<string> {
    return this.mName$;
  }

  constructor(private httpClient: HttpClient) { }

  loadName() {
    this.httpClient.get<string>('name URL')
      .subscribe(
        name => this.mName$.next(name)
      );
  }
}

To figure out what needs to be tested, let’s think about what could go wrong. There are 3 main things that are likely to go wrong. The first is that the component doesn’t even get created due to some syntactical or import error. The second is that the loadName function on the NameService doesn’t get called during initialisation. The third is that, once the NameService name$ function emits a name, it doesn’t get displayed in the component.

Reactive Component Tests

The following is the test code that checks the 3 things that are likely to go wrong described earlier.
name.component.spec.ts

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { of } from 'rxjs';

import { NameService } from './name.service';
import { NameComponent } from './name.component';

describe('NameComponent', () => {
  let nameServiceSpy: jasmine.SpyObj<NameService>;
  let component: NameComponent;
  let fixture: ComponentFixture<NameComponent>;

  beforeEach(() => {
    nameServiceSpy = jasmine.createSpyObj('NameService', ['loadName', 'name$']);

    TestBed.configureTestingModule({
      declarations: [ NameComponent ],
      providers: [ { provide: NameService, useValue: nameServiceSpy } ]
    });

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

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

  it('should call loadName on NameService', () => {
    fixture.detectChanges();

    // Checking loadName call
    expect(nameServiceSpy.loadName).toHaveBeenCalledWith();
  });

  it('should display NameService name', () => {
    // Setting name in service
    nameServiceSpy.name$.and.returnValue(of('the name'));

    fixture.detectChanges();

    // Checking displayed name
    expect(fixture.debugElement.nativeElement.innerText).toEqual('the name');
  });
});

Test Setup

The setup common to all tests is on lines 8-21 which define the component under test and other supporting variables needed for he tests. Lines 13-21 define the standard TestBed initialisation and injection logic. The three test scenarios are defined on lines 24-27 (checking the component can be created), 29-34 (checking that loadName is called on NameService) and 36-44 (checking that the name emitted on name$ is displayed).

Component Setup

The test on lines 24-27 checks that the component can be setup. This is a simple test but is a very useful step for verifying that all the component plumbing is correct and that the test setup has actually worked. If this test fails it is likely because an injection is missing or because the TestBed was not setup correctly.

loadName Call

The test on lines 29-34 checks that the loadName function is called on the NameService. For the ngOnInit function to be triggered, change detection is run on line 30. Line 33 checks that loadName has been called on the NameService.

Name Display

The test on lines 36-44 checks that, when name$ on NameService emits a new name, that name is displayed in the component. Line 38 triggers emitting a new name on name$. Line 40 triggers change detection so that the name is displayed. Line 43 checks that the name is displayed in the component.

These tests are the base on which tests can be built if the logic in the component becomes more complicated. Examples of additional complications are having to pass arguments to the service loadName call, displaying data from multiple services or subjects and a more complicated template that includes conditional displaying of data.

Testing Reactive Service with a Subject in Angular

The Service with a Subject pattern in Angular is a good way to get started with reactive programming (ie. making use of RxJS) in Angular. The following article describes how to get started with the pattern: Getting Started with Service with a Subject in Angular.

The next step after implementing the Service with a Subject pattern is to write tests for the service. These tests will involve working with observables. Testing observables with plain Jasmine leads to tests with logic not related to verifying that the service works. The jasmine-marbles library includes several methods that help test observables.

The Service with a Subject Under Test

For reference, the following is the service that needs to be tested.
name.service.ts

// name.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable, of } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class NameService {
  private mName$ = new BehaviorSubject<string>(null);

  name$(): Observable<string> {
    return this.mName$;
  }

  constructor(private httpClient: HttpClient) { }

  loadName() {
    this.httpClient.get<string>('name URL')
      .subscribe(
        name => this.mName$.next(name),
        () => null
      );
  }
}

To figure out what needs to be tested, let’s think about what could go wrong. There are 3 main things that are likely to go wrong. The first is that the incorrect URL is passed to the get function of the HttpClient. The second is that the return value of the HTTP GET request is not passed to the subject correctly. The third is that, if the HTTP GET request returns an error, an exception is raised.

Service with a Subject Tests

The following is the test code that checks the 3 things that are likely to go wrong described earlier.
name.service.spec.ts

// name.service.spec.ts
import { HttpClient } from '@angular/common/http';
import { cold } from 'jasmine-marbles';

import { NameService } from './name.service';

describe('NameService', () => {
  let service: NameService;
  let httpClientSpy: jasmine.SpyObj<HttpClient>;

  beforeEach(() => {
    httpClientSpy = jasmine.createSpyObj('HttpClient', ['get']);
    service = new NameService(httpClientSpy);
  });

  describe('loadName', () => {
    it('should call get on HttpClient with correct URL', () => {
      // Setting up get spy
      httpClientSpy.get.and.returnValue(cold('a|', {a: null}));

      // Calling loadName
      service.loadName();

      // Checking get call
      expect(httpClientSpy.get).toHaveBeenCalledWith('name URL');
    });

    it('should emit new name on name$', () => {
      // Defining marbles
      const getSpyMarbles =   '-a|';
      const expectedMarbles = 'bc';
      // Setting up get spy
      httpClientSpy.get.and.returnValue(cold(getSpyMarbles, {a: 'name'}));

      // Calling loadName
      service.loadName();

      // Checking get call
      const expected = cold(expectedMarbles, {b: null, c: 'name'});
      expect(service.name$()).toBeObservable(expected);
    });

    it('should emit nothing on name$ if an error occurs', () => {
      // Defining marbles
      const getSpyMarbles =   '-#|';
      const expectedMarbles = 'a';
      // Setting up get spy
      httpClientSpy.get.and.returnValue(cold(getSpyMarbles));

      // Calling loadName
      service.loadName();

      // Checking get call
      const expected = cold(expectedMarbles, {a: null});
      expect(service.name$()).toBeObservable(expected);
    });
  });
});

Test Setup

The setup common to all tests is on lines 8-14. Lines 8 and 9 define the service under test and the HttpClient spy used for the tests that are initialised on lines 11-14. The three test scenarios are defined on lines 17-26 (checking the URL passed to the HttpClient get function), 28-41 (checking that the return value of the HTTP GET request is passed on) and 43-56 (checking the behaviour of the service when the HTTP GET returns an error).

GET Request URL

The test on lines 17-26 checks that the correct URL is passed to the get function of the HttpClient. The first step on line 19 is to give the get call a return value. In this case we only care about what is passed to the get function, so what is set as the return value is irrelevant as long as it is an observable. Line 22 triggers the function that triggers the GET request. Line 25 checks that the get function was called with the correct URL.

Return Value is Passed On

The test on line 28-41 checks that the HTTP GET request return value is passed on by the name$ function as an observable. Lines 30 and 31 define marbles that control when the HTTP GET request emits a value and when the name$ function should emit a value, respectively. Line 33 defines what the HTTP GET request returns.

Line 36 triggers the function that triggers the HTTP GET request. Line 39 defines that value that is expected to be emitted by the name$ function (being the null with which the BehaviourSubject is initialised and the GET request return value) and line 40 checks for that expectation.

HTTP GET Request Error

The test on lines 43-56 checks the behaviour of the code if the HTTP GET request returns an error. Lines 45 and 46 define marbles that control when the HTTP GET request returns an error (indicated by #) and when the name$ function should emit a value, respectively. Line 48 defines what the HTTP GET request should return.

Line 51 triggers the function that triggers the HTTP GET request. Line 54 defines that value that is expected to be emitted by the name$ function (being only the null with which the BehaviourSubject is initialised) and line 55 checks for that expectation.

These tests are the base on which tests can be built if the logic in the service becomes more complicated. Examples of additional complications are if the HTTP GET return value needs to be transformed or if multiple HTTP GET calls need to be made that are potentially merged together.