Testing Reducers page
Learn how to write unit tests for NgRx Reducers.
Quick Start: You can checkout this branch to get your codebase ready to work on this section.
Overview
Verify Login State updates properly when the
LoginActions.loginSuccess
Action dispatches.Verify Login State resets properly when the
LoginActions.logoutSuccess
Action dispatches.
Running Tests
To run unit tests in your project, you can either use the test
npm script, or the ng test
command:
npm run test
# or
ng test --watch
The --watch
switch will rerun your tests whenever a code file changes. You can skip it to just run all tests once.
Description
When testing a Reducer, we will test each of its on()
handlers by verifying each pure function returns the expected value given an Action and a Login State (typically the initialState
). Unlike the previous testing sections, when testing the Reducer, we don’t need a TestBed
since there shouldn’t be any dependencies when dealing with the Reducer.
Update login.selectors.spec.ts
Before we can test our Reducer, our tests involving our Selectors are failing. This is because we’ve changed the shape and initial value of our Login State. To continue, we need to update them. Copy the following code to replace the contents of src/app/store/login/login.selectors.spec.ts
:
src/app/store/login/login.selectors.spec.ts
// src/app/store/login/login.selectors.spec.ts
import * as fromLogin from './login.reducer';
import { selectLoginState } from './login.selectors';
describe('Login Selectors', () => {
it('should select the feature state', () => {
const result = selectLoginState({
[fromLogin.loginFeatureKey]: {
...fromLogin.initialState,
userId: 'some-user-id',
username: 'some-username',
token: 'some-token',
},
});
expect(result).toEqual({
userId: 'some-user-id',
username: 'some-username',
token: 'some-token',
});
});
});
// src/app/store/login/login.selectors.spec.ts
import * as fromLogin from './login.reducer';
import { selectLoginState } from './login.selectors';
describe('Login Selectors', () => {
it('should select the feature state', () => {
const result = selectLoginState({
[fromLogin.loginFeatureKey]: {
...fromLogin.initialState,
userId: 'some-user-id',
username: 'some-username',
token: 'some-token',
},
});
expect(result).toEqual({
userId: 'some-user-id',
username: 'some-username',
token: 'some-token',
});
});
});
We will go over these changes in the upcoming section where we go over testing Selectors (Testing Selectors). For now after you update src/app/store/login/login.selectors.spec.ts
, all of your tests should pass.
Update login.reducer.spec.ts
We will walk through updating src/app/store/login/login.reducer.spec.ts
to run tests for your Reducer.
Verify Login State Updates Properly When the LoginActions.loginSuccess
Action Dispatches
To test the on()
handler associated with LoginActions.loginSuccess
, we will define 3 values:
Expected Login State after Reducer handles Action.
The Action we are testing.
The generated Login State after passing
initialState
and Action toreducer()
.
src/app/store/login/login.reducer.spec.ts
// src/app/store/login/login.reducer.spec.ts
import { reducer, initialState, State } from './login.reducer';
import * as LoginActions from './login.actions';
describe('Login Reducer', () => {
describe('loginSuccess action', () => {
it('should update the state in an immutable way', () => {
// Expectation of new state
const expectedState: State = {
...initialState,
userId: 'some-user-id',
username: 'some-username',
token: 'some-token',
};
const action = LoginActions.loginSuccess({
userId: 'some-user-id',
username: 'some-username',
token: 'some-token',
});
const state = reducer({ ...initialState }, action);
});
});
describe('an unknown action', () => {
it('should return the previous state', () => {
const action = {} as any;
const result = reducer(initialState, action);
expect(result).toBe(initialState);
});
});
});
Next we will add our expectations to verify that our expected Login State matches the generated Login State. When we do this, we will also verify that we do this in an immutable way to maintain an immutable data structure:
src/app/store/login/login.reducer.spec.ts
// src/app/store/login/login.reducer.spec.ts
import { reducer, initialState, State } from './login.reducer';
import * as LoginActions from './login.actions';
describe('Login Reducer', () => {
describe('loginSuccess action', () => {
it('should update the state in an immutable way', () => {
// Expectation of new state
const expectedState: State = {
...initialState,
userId: 'some-user-id',
username: 'some-username',
token: 'some-token',
};
const action = LoginActions.loginSuccess({
userId: 'some-user-id',
username: 'some-username',
token: 'some-token',
});
const state = reducer({ ...initialState }, action);
// Compare new state
expect(state).toEqual(expectedState);
// Check for immutability
expect(state).not.toBe(expectedState);
});
});
describe('an unknown action', () => {
it('should return the previous state', () => {
const action = {} as any;
const result = reducer(initialState, action);
expect(result).toBe(initialState);
});
});
});
Verify Login State Resets Properly When the LoginActions.logoutSuccess
Action Dispatches
We will do the same when testing the on()
handler associated with LoginActions.logoutSuccess
, but instead of defining an expected Login State, we will just use the initialState
since it already is our expected Login State. And instead of passing our initialState
to the reducer()
, we will pass some updated Login State instead to ensure that all values are reset properly:
src/app/store/login/login.reducer.spec.ts
// src/app/store/login/login.reducer.spec.ts
import { reducer, initialState, State } from './login.reducer';
import * as LoginActions from './login.actions';
describe('Login Reducer', () => {
describe('loginSuccess action', () => {
it('should update the state in an immutable way', () => {
// Expectation of new state
const expectedState: State = {
...initialState,
userId: 'some-user-id',
username: 'some-username',
token: 'some-token',
};
const action = LoginActions.loginSuccess({
userId: 'some-user-id',
username: 'some-username',
token: 'some-token',
});
const state = reducer({ ...initialState }, action);
// Compare new state
expect(state).toEqual(expectedState);
// Check for immutability
expect(state).not.toBe(expectedState);
});
});
describe('logoutSuccess action', () => {
it('should reset LoginState to initialState', () => {
const action = LoginActions.logoutSuccess();
const state = reducer(
{
...initialState,
userId: 'some-user-id',
username: 'some-username',
token: 'some-token',
},
action
);
// Compare new state
expect(state).toEqual(initialState);
// Check for immutability
expect(state).not.toBe(initialState);
});
});
describe('an unknown action', () => {
it('should return the previous state', () => {
const action = {} as any;
const result = reducer(initialState, action);
// Shouldn’t update state at all
expect(result).toBe(initialState);
});
});
});
Final Result
At the end of this section, the following spec file(s) should be updated. After each spec file has been updated and all the tests have passed, this means that all the previous sections have been completed successfully:
src/app/store/login/login.reducer.spec.ts
// src/app/store/login/login.reducer.spec.ts
import { reducer, initialState, State } from './login.reducer';
import * as LoginActions from './login.actions';
describe('Login Reducer', () => {
describe('loginSuccess action', () => {
it('should update the state in an immutable way', () => {
// Expectation of new state
const expectedState: State = {
...initialState,
userId: 'some-user-id',
username: 'some-username',
token: 'some-token',
};
const action = LoginActions.loginSuccess({
userId: 'some-user-id',
username: 'some-username',
token: 'some-token',
});
const state = reducer({ ...initialState }, action);
// Compare new state
expect(state).toEqual(expectedState);
// Check for immutability
expect(state).not.toBe(expectedState);
});
});
describe('logoutSuccess action', () => {
it('should reset LoginState to initialState', () => {
const action = LoginActions.logoutSuccess();
const state = reducer(
{
...initialState,
userId: 'some-user-id',
username: 'some-username',
token: 'some-token',
},
action
);
// Compare new state
expect(state).toEqual(initialState);
// Check for immutability
expect(state).not.toBe(initialState);
});
});
describe('an unknown action', () => {
it('should return the previous state', () => {
const action = {} as any;
const result = reducer(initialState, action);
// Shouldn’t update state at all
expect(result).toBe(initialState);
});
});
});
src/app/store/login/login.selectors.spec.ts
// src/app/store/login/login.selectors.spec.ts
import * as fromLogin from './login.reducer';
import { selectLoginState } from './login.selectors';
describe('Login Selectors', () => {
it('should select the feature state', () => {
const result = selectLoginState({
[fromLogin.loginFeatureKey]: {
...fromLogin.initialState,
userId: 'some-user-id',
username: 'some-username',
token: 'some-token',
},
});
expect(result).toEqual({
userId: 'some-user-id',
username: 'some-username',
token: 'some-token',
});
});
});
Wrap-up: By the end of this section, your code should match this branch. You can also compare the code changes for our solution to this section on GitHub or you can use the following command in your terminal:
git diff origin/test-reducer