Angular: Testing NgRx selectors that use other selectors

The other day I was navigating through the unit tests code written by one of my juniors. That part of the code was testing the Store Selectors. For beginners it is tempting to think that testing the Selectors also requires somehow mocking the Store since the Selectors read the state from the Store. That setup seems complex and entirely unnecessary since we are doing Unit Testing and therefore should be able to test the Selectors separately.

Curiously I went to see if NgRx has provided some capabilities to unit test the selectors separately without having to mock the Store. Luckily they have and its really a piece of cake. They have provided a magic function projector which is available with selectors where you can pass any states and the selector will produce the result. Enough talking, let see it in action.

Lets start by defining the state. We usually do it in the reducer file.

// Entity Model
export interface Fruit {
  id: number;
  description: string;
}

// State structure
export interface ProductsState {
  entities: { [id: number]: Fruit };
  selectedProductId: number;
}

// initial value of the state
export const initialState: ProductsState = {
  entities: {},
  selectedProductId: 0
};

Pretty staright forward. Now let define some selectors.

import { ProductsState } from './fruits.reducer';
import { createSelector } from '@ngrx/store';

export const getProductEntities = (state: ProductsState) => state.entities;

export const getSelectedProductId = (state: ProductsState) => state.selectedProductId;

// example of selector that uses another selector
export const getProductsAsArray = createSelector(
  getProductEntities,
  (entities) => Object.keys(entities).map(k => entities[k])
);

// example of selector that uses two other selectors
export const getSelectedProduct = createSelector(
  getProductEntities,
  getSelectedProductId,
  (entities, selectedPrdouctId) => entities[selectedPrdouctId]
);

In the snippet above we have two simple seletors getProductEntities and getSelectedProductId. We also have getProductsAsArray selector that uses the result from another selector getProductEntities. And finally we also have getSelectedProduct selector that uses two other selectors getProductEntities and getSelectedProductId.

[amazon_auto_links id=”529″]

That is basically the setup. Now we are good to write the unit tests. For this we will only cover the last two selectors since everything will be covered with that.

As we have seen getProductsAsArray selector uses the result of one other selector as input.

// example of selector that uses another selector
export const getProductsAsArray = createSelector(
  getProductEntities,
  (entities) => Object.keys(entities).map(k => entities[k])
);

does it mean to test that selector we need to test its input selector as well? No! we don’t need to. We can simply project what state this selector is expecting and wait for the result.

import * as fromSelectors from './fruits.selector';

describe('Products Selector', () => {
  it('selectProductsAsArray: should return entities as array', () => {
    const entities = {
      1: { id: 1, description: 'Apple' },
      2: { id: 2, description: 'Orange' }
    };
    const expectedResult = [
      { id: 1, description: 'Apple' },
      { id: 2, description: 'Orange' }
    ];

    expect(fromSelectors.getProductsAsArray.projector(entities))
      .toEqual(expectedResult);
  });
});

Simple right! Now let us look at getSelectedProduct Selector that uses inputs from two other selectors

// example of selector that uses two other selectorsexport const getSelectedProduct = createSelector(  getProductEntities,  getSelectedProductId,  (entities, selectedPrdouctId) => entities[selectedPrdouctId]);

and here is how we can write a unit test for this

it('getSelectedProduct: should return the selected product', () => {
  const entities = {
    1: { id: 1, description: 'Apple' },
    2: { id: 2, description: 'Orange' }
  };
  const selectedProductId = 1;
  const expectedResult = { id: 1, description: 'Apple' };

  expect(fromSelectors.getSelectedProduct.projector(entities, selectedProductId))
    .toEqual(expectedResult);
});

Leave a Reply