- Published on
How to write tests for Redux Reducers in React.js/Next.js Apps!
Hi guys, In this post I'll explain how to test the the Reducers in the Redux state management portion of your React/Next.js applications.
Setup
First, let's use a sample application for this Redux testing tutorial. Here is a GitHub repo containing a Next.js/Redux app that we can use for this tutorial: https://github.com/ShehanAT/SaaS-Product-App
Start by cloning the project and opening it up in VSCode or a similar coding editor.
Note: If you want to follow along with a different Next.js app or even a React.js app feel free to do so. The tests to be covered in this blog post are also applicable for React.js apps because the Next.js framework is built on top of the React.js library
App Info
Before we get to writing the tests for the Redux Reducers, I'll give a brief overview of how Redux in used in the application mentioned above:
- This application is an ecommerce web app built using Next.js and uses Supabase as its backend and database
- It uses Redux to manage the following aspects of the application:
- Shopping cart
- Customer orders
- Store products
- User Sign in status & user info
Here is the store.js
file for this application so that you can get a better idea of the reducers we'll be testing:
import { createStore, combineReducers, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import Cookie from 'js-cookie';
import {
productListReducer,
productSaveReducer,
productDeleteReducer,
productDetailsReducer,
productReviewSaveReducer,
productUpdateReducer,
} from '../reducers/productReducers';
import {
useSigninReducer,
userUpdateReducer,
userRegisterReducer,
userSigninReducer,
userGetReducer,
} from '../reducers/userReducers';
import {
cartReducer
} from '../reducers/cartReducers';
import {
orderCreateReducer,
orderListReducer,
orderDeleteReducer,
orderPayReducer,
orderDetailsReducer,
shippingDetailsReducer,
myOrderListReducer,
} from '../reducers/orderReducers';
const userInfo = Cookie.getJSON('userInfo') || null;
const cartItems = Cookie.getJSON('cartItems') || [];
const initialState = {
cart: { cartItems, shipping: {}, payment: {} },
userSignin: { userInfo },
};
let composeEnhancers = compose;
if (typeof window !== 'undefined') {
composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
}
const reducer = combineReducers({
productList: productListReducer,
productSave: productSaveReducer,
productDelete: productDeleteReducer,
productDetails: productDetailsReducer,
productReviewSave: productReviewSaveReducer,
productUpdate: productUpdateReducer,
userSignin: userSigninReducer,
userGet: userGetReducer,
userRegister: userRegisterReducer,
userUpdate: userUpdateReducer,
cart: cartReducer,
orderCreate: orderCreateReducer,
orderList: orderListReducer,
orderDelete: orderDeleteReducer,
orderPay: orderPayReducer,
orderDetails: orderDetailsReducer,
myOrderList: myOrderListReducer,
shippingDetails: shippingDetailsReducer,
});
const store = createStore(
reducer,
initialState,
composeEnhancers(applyMiddleware(thunk))
);
export default store;
Also here is a link to the reducer files in the application if you want to view the reducers we'll be testing: https://github.com/ShehanAT/SaaS-Product-App/tree/master/saas-product-app/src/reducers
Development
First let's build out the /src/tests/reduxTesting/reducerTesting/cartReducer.spec.js
file:
const cartReducers = require("../../../reducers/cartReducers");
const cartReducer = cartReducers.cartReducer;
import { CART_ADD_ITEM, CART_REMOVE_ITEM, CART_SAVE_SHIPPING, CART_SAVE_PAYMENT } from "../../../constants/cartConstants";
describe('Testing Cart reducers', () => {
const mockCartState = { cartItems: [], shipping: {}, payment: {} }
it("Given cartReducer" +
"When empty cartItems is passed into cartReducer" +
"Then cartItems should be empty", () => {
const expectedResult = {
cartItems: [
{
"countInStock": 7,
"image": "https://static-content-1.boomfit.com/5490-large_default/kettlebell-12kg.jpg?auto=format&fit=crop&w=2560&q=100",
"name": "Kettlebell",
"price": 15,
"productId": 2,
"qty": 1,
}
]
};
const mockAction = {
type: CART_ADD_ITEM,
payload: {
countInStock: 7,
image: "https://static-content-1.boomfit.com/5490-large_default/kettlebell-12kg.jpg?auto=format&fit=crop&w=2560&q=100",
name: "Kettlebell",
price: 15,
productId: 2,
qty: 1,
}
}
const cartAddItemResult = cartReducer(mockCartState, mockAction);
expect(cartAddItemResult).toEqual(expectedResult);
});
Now for an explanation of what we just added:
- First we import the
cartReducer
viarequire("../../../reducers/cartReducers");
- Then we import the text constants required to validate our reducers from
'../../../constants/cartConstants'
- The
describe()
method is used to, as the name implies, describe the test suite that the file contains - Next a mocked state variable called
mockCartState
is defined. This variable is used as the default state when testing our reducers - The
it()
method is used to identify each individual test. Any code inside it will be run whenever running the command:jest
- Now, the
expectedResult
variable is used as a comparison object, against which the real result from thecartReducer
can be compared to. For now, it contains a single product(a Kettlebell priced at $15) - The
mockAction
is the second argument that is to be passed into thecartReducer
. It contains the type of action(CART_ADD_ITEM
) and the payload(which is the same kettlebell product mentioned in the above point) - After calling
cartReducer(mockCartState, mockAction)
and saving its result tocartAddItemResult
, we can now comparecartAddItemResult
toexpectedResult
. If the assertion passes we can conclude that thecartReducer
works as expected when passing in theCART_ADD_ITEM
action - This first test is designed to test the
CART_ADD_ITEM
action for thecartReducer
. The redux action that uses it is responsible for adding cart items to thecartItems
Redux store variable
Now let's move on to the next test for the /src/tests/reduxTesting/reducerTesting/cartReducer.spec.js
file:
it("Given cartReducer" +
"When removing one item from cartItems" +
"Then cartItems should be of length zero", () => {
const expected = { cartItems: [], payment: {}, shipping: {} };
const mockAction = {
type: CART_ADD_ITEM,
payload: {
productId: 1
}
};
const actual = cartReducer(mockCartState, mockAction);
expect(actual.cartItems.length).toEqual(1);
const mockDeleteAction = {
type: CART_REMOVE_ITEM,
payload: {
productId: 1
}
}
const deleteItemActual = cartReducer(mockCartState, mockDeleteAction);
expect(deleteItemActual.cartItems.length).toEqual(0);
});
Now for an explanation of what we just added:
- This test is designed to test the delete cart item functionality of the
cartReducer
Redux reducer - First we add a cart item to the
cartItems
Redux store variable via the action typeCART_ADD_ITEM
and the payload containing an item. Note that this item contains only theproductId
variable as an attribute for the sake of brevity - Then we make an assertion on whether the insertion above worked by checking that the length of the
actual.cartItems
list has a length of 1 - Now, we call
CART_REMOVE_ITEM
action type in order to delete the newly added product. The payload of this action containing the sameproductId
so that the newly added item can be targeted - Finally, in order to make sure that the item was truly deleted we end with an assertion that the
actual.cartItems
list should have a length of 0
Now let's move on to the next test for the /src/tests/reduxTesting/reducerTesting/cartReducer.spec.js
file:
it("Given cartReducer" +
"When saving shipping item in cartItems" +
"Then cartItems should return shipping item in the payload",() => {
const mockSaveShippingAction = {
type: CART_SAVE_SHIPPING,
payload: {
address: "123 Governor St",
city: "Toronto",
postalCode: "N8E 3T4",
country: "CAN",
}
}
const saveShippingItem = cartReducer(mockCartState, mockSaveShippingAction);
expect(saveShippingItem.shipping.address).toEqual(mockSaveShippingAction.payload.address);
expect(saveShippingItem.shipping.city).toEqual(mockSaveShippingAction.payload.city);
expect(saveShippingItem.shipping.postalCode).toEqual(mockSaveShippingAction.payload.postalCode);
expect(saveShippingItem.shipping.country).toEqual(mockSaveShippingAction.payload.country);
});
Now for an explanation of what we just added:
- This test is designed to test the ability to save an instance of shipping information in the
cart
Redux store variable - First, we create the shipping action containing the
CART_SAVE_SHIPPING
action type and the payload containing an instance of shipping information - Then the
cartReducer()
is called with the mocked shipping action and the result is saved to thesaveShippingItem
variable - Finally, we make four assertions that check for the consistency of the shipping address, city, postal code, and country across both the reducer's result and the mocked action's payload
Now let's move on to testing the Orders reducer. Navigate to the following file: /src/tests/reduxTesting/reducerTesting/orderReducer.spec.js
file and add the following code:
import { orderCreateReducer, orderListReducer, orderDeleteReducer, orderPayReducer, orderDetailsReducer, myOrderListReducer, shippingDetailsReducer } from "../../../reducers/orderReducers";
import { listOrders } from "../../../actions/orderActions";
import { ORDER_CREATE_FAIL, ORDER_CREATE_SUCCESS, ORDER_LIST_SUCCESS } from "../../../constants/orderConstants";
import { isTypedArray } from "util/types";
import { useDispatch } from 'react-redux';
describe("Testing Order reducers", () => {
const mockCartState = { cartItems: [], shipping: {}, payment: {} };
it("Given orderReducer" +
"When creating order" +
"Then order should be returned as the payload", () => {
const mockAction = {
type: ORDER_CREATE_SUCCESS,
payload: {
orderItems: [
{
countInStock: 5,
image: "https://media.istockphoto.com/photos/cement-bags-pile-picture-id476199756",
name: "Cement",
price: 20,
productId: 12,
qty: 1,
},
{
countInStock: 7,
image: "https://static-content-1.boomfit.com/5490-large_default/kettlebell-12kg.jpg?auto=format&fit=crop&w=2560&q=100",
name: "Kettlebell",
price: 15,
productId: 2,
qty: 1,
},
{
countInStock: 10,
image: "https://cdn.runningshoesguru.com/wp-content/uploads/2018/08/Brooks-Ghost-11-Lateral-Side.jpg",
name: "Running Shoes",
price: 50,
productId: 3,
qty: 1,
}
],
shipping: {
address: "123 St James",
city: "Hamilton",
country: "Canada",
postalCode: "N9B4T5"
},
payment: {
paymentMethod: "Stripe"
},
itemsPrice: 30,
shippingPrice: 30,
taxPrice: 10,
totalPrice: 70,
userEmail: "jim@gmail.com"
}
}
const expectedResult = {
success: true,
loading: false,
order: {
orderItems: [
{
countInStock: 5,
image: "https://media.istockphoto.com/photos/cement-bags-pile-picture-id476199756",
name: "Cement",
price: 20,
productId: 12,
qty: 1,
},
{
countInStock: 7,
image: "https://static-content-1.boomfit.com/5490-large_default/kettlebell-12kg.jpg?auto=format&fit=crop&w=2560&q=100",
name: "Kettlebell",
price: 15,
productId: 2,
qty: 1,
},
{
countInStock: 10,
image: "https://cdn.runningshoesguru.com/wp-content/uploads/2018/08/Brooks-Ghost-11-Lateral-Side.jpg",
name: "Running Shoes",
price: 50,
productId: 3,
qty: 1,
}
],
shipping: {
address: "123 St James",
city: "Hamilton",
country: "Canada",
postalCode: "N9B4T5"
},
payment: {
paymentMethod: "Stripe"
},
itemsPrice: 30,
shippingPrice: 30,
taxPrice: 10,
totalPrice: 70,
userEmail: "jim@gmail.com"
}
};
const orderCreateResult = orderCreateReducer(mockCartState, mockAction);
expect(orderCreateResult).toEqual(expectedResult);
});
...
});
Now for an explanation for the first test we just added:
- This test is designed to test the order create success use case. Whenever an order is successfully created, that order should be returned as an object in the payload
- First, we create a mock order object containing three order items, along with their shipping and payment items. Also, the total price of the items(
itemsPrice
), price of shipping(shippingPrice
), price of tax(taxPrice
) and total price of the order(totalPrice
) are also included - Similar to previously covered test cases, we create an expected result object containing the same order object as the one included in the mock order object in order to conduct the assertion
- After saving the result from
orderCreateReducer(mockCartState, mockAction)
to theorderCreateResult
variable, we finally assert that theorderCreateResult
object should be equal to theexpectedResult
object in order for the test case to pass
On to the second test for the /src/tests/reduxTesting/reducerTesting/orderReducer.spec.js
file:
it("Given orderReducer" +
"When passing a list of orders to orderListReducer method" +
"Then a list of orders should be returned as the payload"
, () => {
const mockCartState = { cartItems: [], shipping: {}, payment: {} };
const mockAction = {
type: ORDER_LIST_SUCCESS,
payload: {
orders: [
{
countInStock: 5,
image: "https://media.istockphoto.com/photos/cement-bags-pile-picture-id476199756",
name: "Cement",
price: 20,
productId: 12,
qty: 1,
},
{
countInStock: 7,
image: "https://static-content-1.boomfit.com/5490-large_default/kettlebell-12kg.jpg?auto=format&fit=crop&w=2560&q=100",
name: "Kettlebell",
price: 15,
productId: 2,
qty: 1,
},
{
countInStock: 10,
image: "https://cdn.runningshoesguru.com/wp-content/uploads/2018/08/Brooks-Ghost-11-Lateral-Side.jpg",
name: "Running Shoes",
price: 50,
productId: 3,
qty: 1,
}
],
loading: false
}
};
const actual = orderListReducer(mockCartState, mockAction);
expect(actual.orders.orders.length).toEqual(3);
});
Now for an explanation for the test we just added:
- This test is designed to validate that when we call the Order reducer for a list of orders, that three order objects are received in the response object
- First, we create three order items with their required fields in the
payload
field of the mocked action object. Note that thetype
value ofORDER_LIST_SUCCESS
is indicates to the reducer on which action to perform - Then we simply check the length of the response object and assert that it should be three
Next let's cover a test in the /src/tests/reduxTesting/reducerTesting/productReducer.spec.js
file:
...
// See the GitHub repo mentioned above for the full file
it("Given productReducer" +
"When calling productSaveReducer" +
"Then the saved product should be returned", () => {
const mockAction = {
type: PRODUCT_SAVE_SUCCESS,
payload: {
product: {
productId: 1,
name: "Cement",
image: "https://media.istockphoto.com/photos/cement-bags-pile-picture-id476199756",
price: 20,
countInStock: 5,
qty: 2,
}
}
}
const expectedResult = {
loading: false,
product: {
product: {
productId: 1,
name: "Cement",
image: "https://media.istockphoto.com/photos/cement-bags-pile-picture-id476199756",
price: 20,
countInStock: 5,
qty: 2,
}
},
success: true,
}
const actualResult = productSaveReducer(mockCartState, mockAction);
expect(expectedResult).toEqual(actualResult);
});
...
Now for an explanation of the above code:
- This test is designed to test the product saving functionality of the Product reducer
- First, we pass in a sample product object in the
payload
field of the mock action - Then, an expected result object containing the same product object is created
- Finally, we compare the resulting object with the expected result object and assert that they should be equal
Continuing with the /src/tests/reduxTesting/reducerTesting/productReducer.spec.js
file:
it("Given productReducer" +
"When calling productDeleteReducer" +
"Then the identified product should be deleted and a success indicator should be returned", () => {
const mockAction = {
type: PRODUCT_DELETE_SUCCESS,
payload: {
productId: 1
}
}
const expectedResult = {
loading: false,
success: true,
product: { productId: 1}
}
const actualResult = productDeleteReducer(mockCartState, mockAction);
console.log(actualResult);
expect(expectedResult).toEqual(actualResult);
});
Now for an explanation of the above test:
- This test is designed to test the product delete functionality of the Product reducer
- First, we create a mocked action object containing the
productId
of the product to be deleted - Next, an expected result object is created containing the
productId
of the product to be deleted - Finally, after calling
productDeleteReducer(mockCartState, mockAction)
and saving its result to theactualResult
variable, we compare theactualResult
variable to theexpectedResult
variable. Doing so ensures that the deleted product'sproductId
is contained in the actual result
On to the final reducer file in our testing tutorial: /src/tests/reduxTesting/reducerTesting/productReducer.spec.js
:
...
it("Given userReducer" +
"When calling userSigninReducer" +
"Then an user info object should be returned after sign in success", () => {
const mockAction = {
type: USER_SIGNIN_SUCCESS,
payload: {
user: {
userId: 1
}
}
}
const expectedResult = {
userLoading: false,
userInfo: {
user: {
userId: 1
}
}
}
const actualResult = userSigninReducer(mockCartState, mockAction);
expect(expectedResult).toEqual(actualResult);
});
...
Now for an explanation for this test case:
- This test case is designed to simualate a user sign in attempt. Apon a successful sign in attempt by the user, the
userSigninReducer
should return theuserId
of that specific user - As usual a mock action containing a
userId
in the payload field is created and included in the call to the user sign in reducer:userSigninReducer(mockCartState, mockAction)
- The actual result is then compared with the expected result in order to assert that the same
userId
value should be present in both objects
Ok, if you made it this far Congrats! You now have an understanding of how to test Redux reducers for React.js/Next.js applications.
Conclusion
Well that's it for this post! Thanks for following along in this article and if you have any questions or concerns please feel free to post a comment in this post and I will get back to you when I find the time.
If you found this article helpful please share it and make sure to follow me on Twitter and GitHub, connect with me on LinkedIn and subscribe to my YouTube channel.