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
);
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
);
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
);
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,
});
}
);
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!';
}
);
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}"`;
});
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;
}
);
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.