full coverage, all green Co-written with Yedidya Kennard and Ethan Sharabi Part two of our Redux workflow series. A well-implemented system is only half the work. The other half is an automated suite of tests proving our system is behaving according to spec. This is a crucial factor in increasing our engineering velocity. One of the main benefits of Redux is clear separation between views and business logic, which also allows the two parts to be tested independently. Testing our business logic translates into testing the various Redux constructs — reducers, selectors and action handlers (thunks). A simple and robust workflow for real life apps Presented below is part two of our series about a simple and robust workflow for real life apps. If you haven’t read part one — “ ” — it’s recommended you do so first. Redux Step by Step: A Simple and Robust Workflow for Real Life Apps has become one of the most popular implementations for managing data flow in apps. Reading about Redux, though, often causes a sensory overload where you can’t see the forest for the trees. This also holds for testing Redux projects. Redux Flux React As usual, we’ll start by iterating that there isn’t one right way to test your Redux project. We will present an flavor that we personally believe in. opinionated Some motivation One of the biggest motivations for testing is . It may seem that writing tests slows down the development process, but this perceived notion only holds in the short term. Without automated tests, we’ve noticed that our projects can only grow that much before our ability to deliver grinds to a halt. engineering velocity An automated suite of tests with full coverage is an incredibly powerful tool. It enables new contributors to push code without worry of breaking anything. It battles code rot by alleviating fear of drastic refactoring. It enables faster releases with more confidence. It allows to fix issues in production continuously without waiting for manual regression. The pyramid of testing What kind of tests should we be writing? Should we test the entire system together or every unit separately? Consider the following example: If we were only testing the entire system together, we would need to write 25 tests for full coverage (5x5 flow combinations). A better approach is to mix tests from multiple levels: 5 for Module A by itself, 5 more for Module B by itself and 1–2 of the entire system together. unit tests unit tests integration tests Applying the multi-level approach to the domain of testing yields the famous : front-end applications pyramid of testing There’s a lot of ambiguity regarding what the pyramid looks like and the names of the different levels. This is our take: — Testing the entire app from outside. Running on an actual browser or mobile device, with real servers, usually on production. E2E Tests — Testing major modules of the app from outside. Running on an actual browser or mobile device, with mock servers. UI Automation Tests — Testing the UI and possibly its integration with business logic from inside. Usually running on , with mock services. Component Tests Node — Testing business logic without UI, as separate units. Running on , mocking everything outside the unit. There’s also usually a layer of integration tests between units included here. Unit Tests Node The different levels of tests have different characteristics. The higher you go up the pyramid, the more brittle and flaky the tests are. They provide more confidence, but are usually more expensive to write and maintain. They also run much slower. A good balance is having a lot of tests from the lower levels and fewer and fewer tests from the higher levels. Unit tests for business logic The remainder of this post will focus on the foundation of the pyramid of testing — for business logic. These are the majority of tests we expect to have in our project. We will focus on other levels in future posts. unit tests Our Redux methodology enforces clear separation between components and business logic. Remember the rule: “Place all business logic inside action handlers (thunks), selectors and reducers.” This will allow us to ignore components altogether from this point forward. We’ll continue the discussion over the Reddit app we’ve implemented in the . It lets the user choose 3 topics from the front page subreddits and then see their posts in a filterable list. Refresh your memory with the code, it’s available here: previous post https://github.com/wix/react-dataflow-example Setting up for testing Running requires some sort of test runner. Modern runners like include everything you need to test like , and . unit tests jest setup and teardown expectations mocking Our app relies on which already ships with pre-configured. You can run the following in terminal to set up: create-react-app jest Note that we’re also installing — a library that reduces the boilerplate for testing Redux and makes the process easier and more fun. redux-testkit The last command will start jest in watch mode and automatically run our tests as we’re writing them. If your project does not have jest pre-installed, you can install it easily by following or simply running npm test these instructions npm install jest --save-dev Our game plan If we’ve been following the , all of our business logic is found in the various Redux constructs like ( ), and . All we have to do is practice how to test each one: methodology action handlers thunks selectors reducers Unit tests for reducers Unit tests for selectors Unit tests for (thunks) action handlers Unit tests for services Integration tests for the entire store Unit tests for reducers Reducers are pure functions that take the existing state and an action and return the new state after the action was applied. The benefit of pure functions is that there are no side effects so no mocking is required. One thing we need to watch out for is . Reducers are not allowed to mutate existing state by changing one of its keys in place. This is an example where helps as it verifies immutability for us. immutability redux-testkit The recommended practice with jest is to colocate tests with the files being tested. Our first reducer is found at . We’ll place its tests nearby in . src/store/topics/reducer.js src/store/topics/__tests__/reducer.spec.js Let’s start with the simplest test for . When our reducer is executed without providing existing state and without an action ( for both), it should return the initial state: topics reducer undefined Next, let’s add some tests that send actions and check that the correct state is returned. For this, we’ll use redux-testkit’s : Reducer recipe Note that when providing our reducer with existing state, we need to maintain compatibility to how the reducer implementation holds its state. In this case, it uses which means we must as well. seamless-immutable If your state object is complex and deeply nested, redux-testkit contains other methods for assertions on or using . state deltas custom expectations The fully tested topics reducer is available , posts reducer . here here Unit tests for selectors Selectors are also pure functions that take the existing global state and return some derivative data from it. Once again there are no side effects so no mocking is required. We also need to watch out for here. Selectors are read-only and should not mutate state. It’s easy to miss this when using functions like that mutate the object they run on and accidentally change the state when the selector is running. Just like before, redux-testkit helps as it verifies immutability for us. immutability array.reverse We’ll colocate the tests with the files being tested, but we’ll separate the selector tests from reducer tests even though the reducer implementation contains selectors in the same file. The are found inside . We’ll place their tests nearby in . topics selectors src/store/topics/reducer.js src/store/topics/__tests__/selectors.spec.js Let’s start with a simple test for the . We’ll test that it returns a correct result when the state is empty (initial). Like before, we’ll use redux-testkit’s : getSelectedTopicsByUrl selector Selector recipe Note again that when providing our selector with existing state, we need to maintain compatibility to how the reducer implementation holds its state. In this case it uses which means we must as well. seamless-immutable We should add a couple more tests to this selector to cover cases where the state is not empty: Note that redux-testkit contains other methods like if you prefer using custom expectations. execute The fully tested topics selectors are available , posts selectors . here here Unit tests for (thunks) action handlers According to our , almost every action we export (to be dispatched by views) is a . Thunks wrap synchronous or asynchronous functions that perform the action. They can also cause side effects like accessing servers, which are normally when writing unit tests. methodology thunk mocked What do we need to assert when unit testing a thunk? The main output of a thunk is dispatching other actions — mostly plain objects actions that trigger state modifications in reducers. Thunks can also dispatch other thunks. This means we should set expectations over what was . In addition, since thunks can cause side effects like accessing servers, we can set expectations over these as well. dispatched As usual, we’ll colocate the tests with the files being tested. The are found inside . We’ll place their tests nearby in . topics actions src/store/topics/actions.js src/store/topics/__tests__/actions.spec.js Let’s start with testing the . We’ll test that it triggers the expected dispatches. Since this is a unit test, we’re not going to actually perform the dispatches — they will be mocked. We’ll use redux-testkit’s : fetchTopics action Thunk recipe Unit testing thunks calling other thunks As your system gets more complicated, you’ll probably have one thunk dispatching another thunk. This is an interesting case to test. We can see an example of this case in our Reddit app in the . selectTopic action Our best practice is to always consider different thunks as different units — even if they’re found in the same file. This means that when a thunk that dispatches another, we will not actually execute the second thunk — we’ll always mock it. In order to improve our test, it’s also a to give an explicit name to the anonymous internal function of the second thunk: unit testing good idea Our helper library, redux-testkit, poses several when testing a thunk that dispatches another thunk. We cannot set expectations on the arguments sent to the second thunk and we cannot mock its return value. It may seem at first that these limitations lower the extensiveness of our tests, but they actually enforce correct treatment of sibling thunks. limitations If you have a use-case that suffers from these limitations, you’re probably not treating different thunks as different units — and this is usually a . Further discussion is beyond the scope of this post, but the topic is so important that we’ve decided to dedicate a short post to it — “ ”. code smell Redux Thunks Dispatching Other Thunks — Discussion and Best Practices So what would the unit test of look like? selectTopic action The fully tested topics actions are available , posts actions . here here Unit tests for services Services are abstraction facades for external API (like backend servers). They are stateless and usually contain pure logic. A common side effect for services is to use to make HTTP requests — which we will have to mock. For this purpose, we’re going to use . Install it with fetch jest-fetch-mock npm install jest-fetch-mock --save-dev As usual, we’ll colocate the tests with the files being tested. The is found inside . We’ll place its tests nearby in . reddit service src/services/reddit.js src/services/__tests__/reddit.spec.js Let’s start with a simple test for the : getDefaultSubreddits command These tests are also very convenient during development because they let us work against example data that follows the contract. For this test, we’ve manually recorded the Reddit API and placed the recording as a JSON in . It’s actually more convenient to write the tests first and only then complete the implementation that fulfills them. reddit.subreddits.json The fully tested reddit service is available . here Integration tests for the entire store Up until now, all of our tests focused on a single — a single reducer, a single selector, a single thunk or a single service. Next on our agenda is testing how our units interconnect — in other words, . unit integration testing There’s a lot of ambiguity regarding the term “integration testing” — it all depends on what integrates with what. Let’s be clear and define exactly what plays a part in our case. Our focus is on integration between business logic units in our client. Not integration between client and server. Not integration between business logic and UI. Working with Redux, our business logic is neatly separated from the rest of the app and located under our . In order to test all of the Redux constructs together, we’ll need an actual Redux store instance. There’s no reason not to use the real thing, so we’ll create a store instance in the exact same way we create our store in our . store production code What is the input that will drive our test scenarios? Looking back on the Flux , the only way to trigger a change in the store is by dispatching an action. This means will be our inputs. architecture diagram actions What are the assertions we’ll make in our scenarios? Looking back on the Flux , the only way to consume our store is by listening on updated state. We normally never access new state directly, but use selectors instead. This means will be used for our assertions. architecture diagram selectors In order to colocate tests with the files being tested we’ll use the folder. The is found under . We’ll place its tests nearby in . src/store topics domain src/store/topics src/store/__tests__/topics.spec.js Let’s start with an empty skeleton instantiating the store: We don’t need much help from redux-testkit because all we’re doing is using the official Redux API. The only helper we’ve added is a middleware called . This middleware keeps track of all thunks that have been dispatched. It’s useful for the case where one thunk dispatches another thunk and lets us wait until all promises have been resolved before running our assertions. If you don’t have thunks dispatching other thunks, you can remove this middleware. FlushThunks Let’s implement an interesting topics-related integration scenario: We’ll start by fetching all topics from the reddit service. Then select just two of them and verify that our selection remains invalid (3 are needed). We’ll then select the third and verify that the selection becomes valid. Once 3 topics are selected, the system prefetches posts from these topics by a posts action. We’ll finish by verifying that prefetch succeeded and the correct posts found their way to our store. The last action is a thunk dispatching another thunk — so we’ll need to use FlushThunks to wait for everything to subside: dispatching It may seem that this scenario repeats aspects that were already tested in our previous . This is correct. By definition take several units and combine their flows. unit tests integration tests The idea is to test a small number of happy flows between units. There’s no need to check every possible combination. Let’s take the above scenario, there’s no need to add another test checking what happens when a fourth topic is selected (we expect the first to be replaced). We’ve already covered these edge cases in our extensive . unit tests Remember our pyramid. The higher up we go, the less scenarios we need to write. The overall number of integration scenarios should be significantly lower than unit scenarios. Unsure which integration scenarios you should write? Try to find you haven’t covered in an integration scenario yet and build one around them. actions The fully tested topics domain is available , posts domain . here here Summary We’ve completed our game plan, our business logic should be covered. The full code of our example Reddit app including full tests for business logic is available on GitHub in the following branch: _react-dataflow-example - Experimenting with different dataflow approaches for a real life React app_github.com wix/react-dataflow-example Coverage How can you tell if you have adequate test coverage? One way is to use jest coverage reports, which indicate the percentage of lines of code that were actually executed during the tests. Generate the report by running: When testing business logic, we should be able to get to 100% coverage without much difficulty. Remember that full coverage does not necessarily mean the tests are meaningful and we can now deploy with blind confidence. But if you have less than 100% — they are definitely lacking. This is the coverage report from the tests we’ve written for our Reddit app: Our service still needs a little work. We can see the , and they seem like an edge condition which we may even choose to ignore. src/services/reddit.js untested lines Since we’re only testing business logic, we can limit the report to relevant directories — and — with this command: src/store src/services Post credits scene There’s an art to writing good tests. If you write too little tests, you’ll not gain the confidence that full coverage provides. If you write too many, they’ll become a hassle to maintain without adding value. One of the methods that guarantees writing exactly the right amount of tests is TDD — . We are big fans of TDD in Wix, but we’ve not been successful in creating a TDD workflow that feels natural with Redux. This eventually brought upon the creation of — an interesting mix of idiomatic inspired by the dogma of . More on that soon. Test Driven Development remx MobX Redux