Everything should be made as simple as possible, but not simpler.
~ Albert Einstein
I prefer to apply KISS Principle when solving problems, so the same goes for testing React-Redux apps.
I am building testing upon my Idiomatic Redux article and app. Here you can find React Countries app with tests included that I will be referring. After spending some time reading, thinking and trying different approaches I came to relatively simple, but efficient testing strategy. My goal was not to have near 100% test coverage, but to test what really matters without using too much resources (time, energy) and complex solutions.
Testing described in this article has in mind following app setup/configuration:
- React 16
- Thunk middleware
Although I am sure it can be adjusted for other setups.
What (not) to Test
This is one of the most important questions – what to test and, more importantly, what not to test. My approach is to focus mostly on unit testing and some functional testing. Integration, E2E and complex interaction testing is completely skipped and left for QA team 😛
We will test:
- Dumb/Presentational React components – minimalistic approach
- Thunk action creators
- Util/Common code
We will NOT test:
- Smart/Connected components
- Simple action creators
- Interaction between components
- Integration and E2E
Reasoning behind this decision is this:
- We just want to make sure that presentational components actually render without exploding and testing some simple things like: is checkbox initially checked or are there 3 li elements if I pass array with 3 countries, etc.
- Smart connected components require redux store, mocking or using real one, this complicates things and it’s closer to integration testing that we want to avoid.
- Reducers are core of Redux app and simple functions, so they have to be fully tested and it’s not difficult.
- Since simple (object) action creators are reaching reducers that are already tested, we don’t have to test these action creators again separately.
- We want to fully test thunk action creators and make sure that when promises are resolved we have proper actions (simple objects) triggered.
- All util/common code (functions) should be fully unit tested.
- If we want to test that button click in one component would trigger something within that same component – that’s ok. But as soon as we want to test interactions between different components that automatically goes more to integration testing and it’s not easy. Why? We can mount root component in our test, simulate click event and then assert and make test, but as soon as we have connected components in the tree (and we have) it complicates things and we have to mock store, pass it with Provider, etc.
There are many different tools and options out there, I did some investigation and this is my weapon of choice:
- Jest – default test runner provided by create-react-app by Facebook
- Jasmine – enables you to write assertions and it’s included with Jest and create-react-app
- Enzyme – AirBnb’s library for testing React components
- Redux Mock Store – library for mocking Redux store
- Nock – HTTP mocking library
These tools are powerful and in this article I am not explaining all possible usages, refer to corresponding docs for more details. Fire this one-liner to install everything you need:
npm i -D react-test-renderer @types/jasmine react-addons-test-utils enzyme enzyme-adapter-react-16 redux-mock-store nock
There are also some other tools that I had to install and configure to make everything smooth:
- @type/jasmine from above, so that Webstorm doesn’t complain about not seeing functions from Jasmine. You don’t have to import them, it’s just for the IDE.
- I had to install Watchman.
- In root of my project check __mocks__/popper.js.js it’s a mock of popper.js library I had to do, to make mount function of Enzyme working.
Jest is searching for __tests__ folders in your project tree and will run all files/tests from there. So for example – if I am testing reducers, I will create reducers/__tests__/my-reducer.test.js. I like this filename convention, but you can use anything else.
Testing Dumb Components
Let’s test our first presentational component. Check file src/components/__tests__/Toolbar.test.js and notice imports, Enzyme configuration/adapter and 3 tests we have:
- Component actually renders – this is our first test and we are using shallow function from Enzyme to render only light/shallow RcToolbar component without any children. Enzyme has jQuery like syntax, so it’s really easy to find what you need and assert.
- Here I am checking that Add button has “+” icon from Material UI. I am using mount function here and not shallow because I need to access child component to make assert.
- In the last test I am checking if toggle button/filter is turned on by default.
Other good example would be to test if RcList component renders N li elements when I pass N countries.
So we are making minimalistic tests for presentational components – do they render and show what they need to show.
Now open src/reducers/__tests__/countries.test.js. Country reducer is combined reducer for using normalized state, so we are testing both byId and list reducers here.
Testing reducers is really straightforward because they are pure functions. For the given input, we expect always the same output. In my example we are checking if country add success will result in state that contains newly added country.
Notice that we are using addCountrySuccess action creator inside the test, that’s why we don’t have to explicitly test simple action creators. Refactor your code, so that you can use these action creators in both code and tests, if you already don’t have them.
Testing Thunk Action Creators
It’s important to test thunk / async action creators to make sure that we have proper simple actions dispatched after promise has been resolved. Open src/actions/__tests__/actions.test.js. First thing you will notice is that we are using redux-mock-store library to mock Redux store. We are also adding thunk middleware to it. Mocking HTTP request (using Nock library) since adding a country will result in remote API call and we don’t want that in the test. We are then simply dispatching thunk action and when done checking if all expected actions have been dispatched. In this specific case we expect that adding a country will result in success event, closed dialog and a message.
I don’t have any utility/common code in the project, but these should be fully tested, as they are usually pure functions.
It’s time to run our tests locally with npm test. It’s configured to run only newly added/changed tests from the last commit (more details here). There is also interactive mode allowing you to press “a” to run all tests anyway. This doesn’t work well in Webstorm when npm test is invoked from UI, so just use terminal if you want interactive mode.
I am using Travis and if you check travis configuration file you will see that one new line has been added to enable CI testing. NPM testing will be invoked on every commit.
Testing topic is really complex and there are many possible options, but I think this approach is relatively simple and tests the core of React-Redux app – reducers, actions and presentational components. In combination with QA team, this can be good testing strategy 😉