Unit testing NGRX Selectors (including routes)

This is the third article in my series on NGRX selectors. It will touch on how to unit test the NGRX selectors created in my previous articles:

    • Top 5 Ways to Misuse NGRX Selectors
    • A Complicated NGRX Selectors Example

This one is important because I made claims that unit testing the selectors was easy. So, let’s get to it!

Unit Testing the selectors.ts File

The selectors.ts file is basic and straightforward but it will get our feet wet and set the course for the rest of the selectors. I won’t go through all of the tests for time/space considerations but you can check the spec yourself to see them all.

Files in github:


Spec Setup

First, setup initialState for these tests (this isn’t necessary in all cases but will come in handy for this spec.)

describe('Selectors', () => {
  const initialState: IApp = {
    apiStuff: { ships: null, vehicles: null, films: null },
    currentPerson: {
      birth_year: null,
      eye_color: 'blue',
      films: null,
      gender: 'male',
      hair_color: 'blond',
      height: 70,
      mass: 200,
      name: 'Luke Skywalker',
      starships: null,
      vehicles: null,
    },
    lastPersonId: '0',
  };

Testing getAppState

This is getAppState:

export const getAppState = createFeatureSelector<IApp>(appRootKey);
    • It takes in the appRootKey string of “AppState” but that doesn’t really matter in the test.

This is how to test it:

it('should select getAppState', () => {
    // Act
    const result = fromSelectors.getAppState.projector(initialState);

    // Assert
    expect(result).toBeDefined();
    expect(result.apiStuff).toBeDefined();
    expect(result.currentPerson).toBeDefined();
    expect(result.lastPersonId).toEqual('0');
  });
  • Notice the “projector” method, I’ll use this for all cases as the recommended way from NGRX
    • o In most cases, we’ll be passing in whatever the results of the selector are in the parameter list
      o However, here we pass in initial state

Testing getAdditionalData

This is getAdditionalData:

export const getAdditionalData = createSelector(
  getAppState,
  (state) => state?.apiStuff
);
This is how to test it:
it('should select getAdditionalData', () => {
    // Arrange
    initialState.apiStuff.films = [
      {
        title: 'A New Hope',
        director: 'Rich',
        episode_id: 1,
        opening_crawl: 'Blah blah',
        producer: 'John',
      },
    ];

    // Act
    const result = fromSelectors.getAdditionalData.projector(initialState);

    // Assert
    expect(result).toBeDefined();
    expect(result.films).toBeDefined();
    expect(result.films.length).toEqual(1);
  });
  • Pass in initialState here because that is what getAppState returns
    • o getAppState is the first parameter into this selector

Testing getPersonName

This is getPersonName:

export const getPersonName = createSelector(
  getCurrentPerson,
  (person) => person?.name
);
This is how to test it:
it('should select getPersonName', () => {
    // Act
    const result = fromSelectors.getPersonName.projector(
      initialState.currentPerson
    );

    // Assert
    expect(result).toEqual('Luke Skywalker');
  });
  • Notice in this case, we pass in currentPerson instead of initialState
    • o That’s because getCurrentPerson returns a People object

Testing getFilms

This is getFilms:

export const getFilms = createSelector(
  getAdditionalData,
  (data) => data?.films
);
This is how to test it:
it('should select getFilms', () => {
    // Arrange
    initialState.apiStuff.films = [
      {
        title: 'A New Hope',
        director: 'Rich',
        episode_id: 1,
        opening_crawl: 'Blah blah',
        producer: 'John',
      },
    ];

    // Act
    const result = fromSelectors.getFilms.projector(initialState.apiStuff);

    // Assert
    expect(result).toBeDefined();
    expect(result.length).toEqual(1);
    expect(result[0].title).toEqual('A New Hope');
  });
  • This one passes in apiStuff as a parameter to the project method
    • o That’s because that maps to the getAdditionalData selector, which is the parameter into this selector

Unit Testing the additional-api-data.selectors.ts File

Files in github:

Testing getAdditionalApiData

This is getAdditionalApiData:

export const getAdditionalApiData = createSelector(
  getFilms,
  getShips,
  getVehicles,
  (films, ships, vehicles) => {
    return new AdditionalApiViewModel({
      films,
      ships,
      vehicles,
    });
  }
);
This is how to test it:
it('should select getAdditionalApiData', () => {
    // Arrange
    const ships = [
      {
        name: 'Cruiser',
        starship_class: 'Big One',
      },
    ];
    const vehicles = [
      {
        name: 'Speeder',
        manufacturer: 'Freds Vehicles',
      },
    ];
    const films = [
      {
        title: 'A New Hope',
        director: 'Rich',
        episode_id: 1,
        opening_crawl: 'Blah blah',
        producer: 'John',
      },
    ];

    // Act
    const result = fromSelectors.getAdditionalApiData.projector(
      films,
      ships,
      vehicles
    );

    // Assert
    expect(result).toBeDefined();
    expect(result.ships).toBeDefined();
    expect(result.ships.length).toEqual(1);
    expect(result.vehicles).toBeDefined();
    expect(result.vehicles.length).toEqual(1);
    expect(result.films).toBeDefined();
    expect(result.films.length).toEqual(1);
  });
  • No need to create initialState here since we just need to pass in films, ships and vehicles
      o Just need to create those arrays and pass them into the projector method

Unit Testing the opening-crawl.component.selectors.ts File

Files in github:

Testing getEpisodeNumberFromRoute

This is getEpisodeNumberFromRoute:

export const getEpisodeNumberFromRoute = selectRouteParam('episodeNumber');
  • selectRouteParam is one of the router store selectors created in this file
  • I couldn’t find anyplace that wrote about how to test this
      – I figured it out though 😊

This is how to test it:

it('should select getEpisodeNumberFromRoute', () => {
    // Arrange
    const routerState: Record<string, any> = {
      episodeNumber: '1',
    };

    // Act
    const result =
      fromSelectors.getEpisodeNumberFromRoute.projector(routerState);

    // Assert
    expect(result).toEqual('1');
  });
  • Create a Record object with the route parameter name as a property name (episodeNumber) and ‘1’ as the value
      — Hover over the selectRouteParam method and you’ll see this “Record
      — You could do the same with the other router store selector methods to test those
  • Pass it into the projector method

Testing getOpeningCrawl

This is getOpeningCrawl:

export const getOpeningCrawl = createSelector(
  getFilms,
  getEpisodeNumberFromRoute,
  (films, episodeNumber) => {
    if (films && episodeNumber) {
      const film = films.find((f) => f.episode_id == +episodeNumber);
      if (film) {
        return film.opening_crawl;
      }
    }
    return 'Film not found!';
  }
);
This is how to test it:
it('should select getOpeningCrawl', () => {
    // Arrange
    const episodeNumberFromRoute = '1';
    const films = [
      {
        title: 'A New Hope',
        director: 'Rich',
        episode_id: +episodeNumberFromRoute,
        opening_crawl: 'Blah blah',
        producer: 'John',
      },
    ];

    // Act
    const result = fromSelectors.getOpeningCrawl.projector(
      films,
      episodeNumberFromRoute
    );

    // Assert
    expect(result).toEqual('Blah blah');
  });

  it('should select getOpeningCrawl of Not Found', () => {
    // Arrange
    const episodeNumberFromRoute = '2';
    const films = [
      {
        title: 'A New Hope',
        director: 'Rich',
        episode_id: 1,
        opening_crawl: 'Blah blah',
        producer: 'John',
      },
    ];

    // Act
    const result = fromSelectors.getOpeningCrawl.projector(
      films,
      episodeNumberFromRoute
    );

    // Assert
    expect(result).toEqual('Film not found!');
  });

Unit Testing the people.component.selector.ts File

Files in github:

Testing getHeightFormatted

This is getHeightFormatted:

export const getHeightFormatted = createSelector(getPersonHeight, (height) => {
  const heightInCm = height;
  const heightInFeetFractional = heightInCm / 30.48;
  const heightInFeet = Math.floor(heightInFeetFractional);
  const remainderInches = Math.round(fracMemoized(heightInFeetFractional) * 12);
  return `${heightInFeet}' ${remainderInches}"`;
});
This is how to test it
it('should select formatted height of 6 feet 2 inches', () => {
    // Arrange
    const height = 187;

    // Act
    const result = fromSelectors.getHeightFormatted.projector(height);

    // Assert
    expect(result).toEqual('6\' 2"');
  });
  • Just have to pass in height in cm here and it formats as 6’ 2”

Testing getPeopleComponentData

This is getPeopleComponentData:

export const getPeopleComponentData = createSelector(
  getPersonName,
  getPersonHairColor,
  getPersonGender,
  getHeightFormatted,
  (name, hair, gender, height) => {
    if (name) {
      return new PeopleComponentViewModel({
        name: name,
        hair_color: hair,
        gender: gender,
        height,
      });
    }
    return null;
  }
);
This is how to test it:
  it('should select object of people component data', () => {
    // Arrange
    const personName = 'Luke Skywalker';
    const personHairColor = 'Blond';
    const personGender = 'Male';
    const height = '6\' 2"';

    // Act
    const result = fromSelectors.getPeopleComponentData.projector(
      personName,
      personHairColor,
      personGender,
      height
    );

    // Assert
    expect(result).toBeDefined();
    expect(result.gender).toBe(personGender);
    expect(result.hair_color).toBe(personHairColor);
    expect(result.height).toBe(height);
    expect(result.name).toBe(personName);
  });
  • Again, just have to pass in name, hair color, gender and height because that’s what the selector has for parameters

Conclusion

I left out the starwars-container.component.selectors.spec.ts because it doesn’t teach anything new. Feel free to check it out though.

Hopefully, this helped you to understand how easy it is to unit test selectors with the “projector” method. Also, how to unit test routes easily.

About Intertech

Intertech is a Software Development Consulting Firm that provides single and multiple turnkey software development teams, available on your schedule and configured to achieve success as defined by your requirements independently or in co-development with your team. Intertech teams combine proven full-stack, DevOps, Agile-experienced lead consultants with Delivery Management, User Experience, Software Development, and QA experts in Business Process Automation (BPA), Microservices, Client- and Server-Side Web Frameworks of multiple technologies, Custom Portal and Dashboard development, Cloud Integration and Migration (Azure and AWS), and so much more. Each Intertech employee leads with the soft skills necessary to explain complex concepts to stakeholders and team members alike and makes your business more efficient, your data more valuable, and your team better. In addition, Intertech is a trusted partner of more than 4000 satisfied customers and has a 99.70% “would recommend” rating.