In this post, I will cover scenarios of writing unit tests for Angular applications that use ngrx using karma and jasmine.

Angular and ngrx already have well-documented API for testing tools but from my point of view, there is a bit lack of real-world use cases. It may connect to complex data structures, legacy code and something else which block you from writing tests. In my series of articles I’m going to cover some case from the real world.

How to start?

Let’s assume that you are working in a team of 5–10 developers on an app, and you still don’t have a single test. For you, it’s a great chance to level up your self and move the quality of the application to the next level as well. As we are talking about an Angular app in ngrx context the simple way is to start with selectors. I hope you already use them.

Btw, I assume that you bootstrap you project with angular cli and going to use Karma + Jasmine for your tests. The bad thing about Angular that it works only with included libs. It’s possible to use Ava or Tape for your tests, but you will waste a lot of time on integration and don’t get a lot of value.

If you are not confident with Angular tools for testing start with these articles:

Simple selector test

A simple selector may look like:

export const getUsers = state => state.users.list;

To be honest, there is nothing to test here, so let’s move forward. Sometimes you may need to know that users have already loaded and detected that they are empty.

export const getIsUsersLoaded = state => state.users.list.length > 0

It’s not enough because you actually don’t know are they loaded or not. To handle this we need to introduce a new flag ‘isLoaded’

// users.selector.ts export const getUsers = state => state.users.list;

export const getIsLoaded = state => state.users.isLoaded export const getIsEmpty = createSelector(

getUsers,

users => users.length === 0,

); export const getIsLoadedAndEmpty = createSelector(

getIsLoaded,

getIsEmpty,

(isLoaded, isEmpty) => isLoaded && isEmpty,

);

// users.selector.spec.ts it(‘getIsLoadedAndEmpty should return true if users are loaded and not empty’, () => {

const loadedState = {

...globalStateMock,

users: {

...globalStateMock.users,

isLoaded: true,

users: [...usersMock],

}

}



const actual = getIsLoadedAndEmpty(loadedState);

expect(actual).toBeTruthy();

});

Do the same for situations when you don’t have users or state is not loaded yet and you are done.

Tip from the real world — write functions which provide mocks for general entities of your application.

How to avoid mocks?

In the real world, an application may have a complex structure of the state with nested reducers and cumbersome data interfaces. The tricky thing here that you will have to mock almost whole application to test a single piece of it. To avoid it try to decouple logic from functions which get the data from the state tree. Let’s rewrite our previous example:

export const checkUsersReady = (isLoaded, isEmpty) => isLoaded

&& isEmpty; export const getIsLoadedAndEmpty = createSelector(

getIsLoaded,

getIsEmpty,

checkUsersReady,

);

Now you can separately check getIsLoadedAndEmpty outside of the existing structure.

The extra point is that you can use this function not only for users but for any entities you have in the application. Believe me that you will have a lot of data in lists like users.

Power of selector functions and more complex example

With ngrx, it’s very easy to implement the simplest app like a huge monster. Observables and store give you a lot of power for controlling the state of the application and in a moment you may find yourself not understanding whats going on in the mess of endless streams of actions.

For example, you could end up in a situation when a user is moving back and forward from one page to another and you don’t even know where to start debugging.

Usually, it happens when you try to control the transition between pages in effects. Or destination URL depends on what you do have in the state.

Way to solve this is to look at your UI as on representation of state depends on some conditions. The key word here is a condition. Literally, it’s an if in your code.

It doesn’t depend on Angular or any other frontend framework, it doesn’t depend on if it browser or a desktop app. It depends on fields in an object and how you should reflect this fields in UI. Generally it’s a business logic.

Simple example — if you have JWT token in the state user see the main screen, if no he sees a login page.

if (token) {

showMain();

} else {

showLogin();

}

In the real world, you will have more complex conditions and there is a great room for selectors. Let’s see what we may have in e-commerce application with checkout flow.

Checkout flow is the place for a customer to select shipment address, delivery method, payment method and billing address. Tricky thing is that we shouldn’t navigate customer to the first step if he has already selected an address and move him to the next step.

It comes as an issue when you have to handle the situation of page refresh when a customer moves between another part of an application or next steps may affect previous steps. You navigate customer to the first not filled step.

The main goal is to keep all this logic in one place and leave a room for further improvements, for example, a new step in the flow.

Let’s see how may look interface of checkout flow state which you get from the API.

// checkout.ts export interface CheckoutFlow {

shipmentAddressId: number,

shipmentMethodId: number,

paymentMethodId: number,

billingAddressId: number,

warnings: any[], // here should be a type

} export enum CheckoutFlowStep {

shipment = 'shipment',

shipmentMethod = 'shipmentMethod',

paymentMethod = 'paymentMethod',

billingAddress = 'billingAddress',

} // checkout.actions.ts // We need will run this action after every checkout step change to recalculate current checkout step. export class InvalidateCheckoutStep implements Action {

readonly type = CheckoutAction.InvalidateCheckoutStep;

constructor(public payload: CheckoutFlow) {}

}

To complete with it we need to implement a function which gets the current state, do some calculation and return step of checkout. Then we will use this step to navigate our customer.

// checkout.selector.ts export function getCurrentCheckoutStep(checkout: CheckoutFlow): CheckoutFlowStep {



...implementation } // checkout.selector.spec.ts describe('getCurrentCheckoutStep', () => { it('Should return shipment if all fields are empty', () => {

const actual = getCurrentCheckoutStep(mockWithEmptyCheckout);

expect(actual).toBe(CheckoutFlowStep.shipment)

}); it('Should return shipment method if shipment filled and method not', () => {

const actual = getCurrentCheckoutStep(mockWithoutShipmentMethod);

expect(actual).toBe(CheckoutFlowStep.shipment)

}); // Do the same for every possible state; }); // checkout.effects.ts @Effect({dispatch: false})

invalidateCheckoutStep$ = this.actions$

.ofType(CheckoutAction.InvalidateCheckoutStep)

.do((action) => {

const step = getCurrentCheckoutStep(action.payload);



const route = mapStepToRoute(step);

this.router.navigate(route);

});

That’s it. Not it’s quite easy to manage logic of transitions between checkout flow steps and also it controlled with tests.

One possible improvement is to replace router.navigate with navigation action — https://github.com/ngrx/platform/blob/master/docs/router-store/api.md#navigation-actions

When it comes to testing of effects it’s much easier to test that it ends with expected action instead of spy on navigate function.

Conclusion

Hope with approach will help you. Probably the most time-consuming part will be a generation of mocks for tests but in long term, they can be reused in other tests. Also, you may spend some time on clarification requirement for business logic or lurk through the project to get know how everything works.

Take into account that even 100% coverage of your selectors still tests only these functions. Testing of full application is still very complicated when it comes to the data flow of actions and effects.

This points of integration between different parts of your application is the most common place for bugs. And we will cover this in the next articles. But with testing selectors, you can at least be confident in fundamental business logic.

P. S. Feel free to contact me for any questions. If you see that I didn’t cover something interesting for you shot me an email and I will update the article. sapronov.egor@gmail.com