Home > OS >  Weird error testing a reducer (redux toolkit)
Weird error testing a reducer (redux toolkit)

Time:06-20

I'm beginner with react and redux and I coded an very simple app to learn it where you can add expenses with a description an amount a date and a note.

I'm testing one of my reducers with jest and I get an error that I cannot understand. I'm probably missing something but I can't figure out so it would be nice if you could help me out.

So I have one expenseSlice with a reducer action addExpense that prepare the payload just adding an ID and set default values if necessary and add it to the previous state.

import { createSlice } from "@reduxjs/toolkit"
import { nanoid } from "nanoid"

const expensesDefaultState = []

const expensesSlice = createSlice({
    name: 'expenses',
    initialState: expensesDefaultState,
    reducers: {
        addExpense: {
            reducer: (state, action) => {
                return [
                    ...state,
                    action.payload
                ]
            },
            prepare: (payload) => {
                return {
                    payload: {
                        id: nanoid(),
                        description: payload.description ? payload.description : '',
                        note: payload.note ? payload.note : '',
                        amount: payload.amount ? payload.amount : 0,
                        createdAt: payload.createdAt ? payload.createdAt : 0
                    }
                }
            }
        }

And in my test file when I test the action the test pass correctly like so :

describe('Expenses actions', () => {

    it('should returns an addExpense action object with prepared payload', () => {
        const newExpense = {
            description: 'this is a description',
            amount: 1000,
            createdAt: 0,
            note: 'this is anote'
        }
        expect(addExpense(newExpense)).toEqual({
            type: 'expenses/addExpense',
            payload: {
                id: expect.any(String),
                description: 'this is a description',
                amount: 1000,
                createdAt: 0,
                note: 'this is a note'
            }
        })
    })

But when I test the reducer with the same action and expect an id it failed and I get the object without the ID like so :

describe('Expense Reducer', () => {

    it('should add an expense', () => {
        const newExpense = {
            description: 'my new expense',
            amount: 1000,
            createdAt: 0,
            note: 'this is a note'
        }
        const state = expenseReducer([], {
            type: 'expenses/addExpense',
            payload: newExpense
        })

        expect(state).toEqual([
            {
                id: expect.any(String),
                description: 'my new expense',
                amount: 1000,
                createdAt: 0,
                note: 'this is a note'
            }
        ])
    })
})

And the error :

● Expense Reducer › should add an expense

    expect(received).toEqual(expected) // deep equality

    - Expected  - 1
      Received    0

      Array [
        Object {
          "amount": 1000,
          "createdAt": 0,
          "description": "my new expense",
    -     "id": Any<String>,
          "note": "this is a note",
        },
      ]

What I'm missing here or not understanding correctly ? Thank you in advance !

CodePudding user response:

Because you ran the expensesSlice.reducer function directly, it's a pure function. So the prepare callback will not execute. You can use store.dispatch(addExpense(newExpense)) to trigger the prepare callback and test the changes of the state slice.

createSlice() will use createAction() to create action creators with or without prepare callback and export these action creators as stateSlice.actions property. See v1.7.2/packages/toolkit/src/createSlice.ts#L294

When the action creators execute like addExpense(newExpense), it will use prepare callback to customize action content(the id: nanoid() in your case).

E.g.

index.ts:

import { createSlice, nanoid } from '@reduxjs/toolkit';

const expensesDefaultState = [];

const expensesSlice = createSlice({
  name: 'expenses',
  initialState: expensesDefaultState,
  reducers: {
    addExpense: {
      reducer: (state, action) => {
        return [...state, action.payload];
      },
      prepare: (payload) => {
        return {
          payload: {
            id: nanoid(),
            description: payload.description ? payload.description : '',
            note: payload.note ? payload.note : '',
            amount: payload.amount ? payload.amount : 0,
            createdAt: payload.createdAt ? payload.createdAt : 0,
          },
        }
      },
    },
  },
});

export const { addExpense } = expensesSlice.actions;
export default expensesSlice.reducer;

index.test.ts:

import { configureStore, nanoid } from '@reduxjs/toolkit';
import expenseReducer, { addExpense } from '.';

describe('Expenses actions', () => {
  it('should returns an addExpense action object with prepared payload', () => {
    const newExpense = {
      description: 'this is a description',
      amount: 1000,
      createdAt: 0,
      note: 'this is a note',
    };
    expect(addExpense(newExpense)).toEqual({
      type: 'expenses/addExpense',
      payload: {
        id: expect.any(String),
        description: 'this is a description',
        amount: 1000,
        createdAt: 0,
        note: 'this is a note',
      },
    });
  });

  it('should add an expense', () => {
    const newExpense = {
      id: nanoid(),
      description: 'my new expense',
      amount: 1000,
      createdAt: 0,
      note: 'this is a note',
    };
    const state = expenseReducer([], {
      type: 'expenses/addExpense',
      payload: newExpense,
    });

    expect(state).toEqual([
      {
        id: expect.any(String),
        description: 'my new expense',
        amount: 1000,
        createdAt: 0,
        note: 'this is a note',
      },
    ]);
  });

  it('should add an expense - 2', () => {
    const newExpense = {
      description: 'my new expense',
      amount: 1000,
      createdAt: 0,
      note: 'this is a note',
    };
    const store = configureStore({ reducer: expenseReducer });
    store.dispatch(addExpense(newExpense));
    expect(store.getState()).toEqual([
      {
        id: expect.any(String),
        description: 'my new expense',
        amount: 1000,
        createdAt: 0,
        note: 'this is a note',
      },
    ]);
  });
});

Test result:

 PASS  stackoverflow/72677850/index.test.ts (10.106 s)
  Expenses actions
    ✓ should returns an addExpense action object with prepared payload (2 ms)
    ✓ should add an expense (1 ms)
    ✓ should add an expense - 2 (2 ms)

Test Suites: 1 passed, 1 total
Tests:       3 passed, 3 total
Snapshots:   0 total
Time:        10.577 s, estimated 13 s
  • Related