Skip to content

Instantly share code, notes, and snippets.

@Waksuu Waksuu/blog.md Secret
Created Mar 25, 2020

Embed
What would you like to do?

Black box testing React components connected to Redux

What's the better way to make your code more maintainable and easier to change in the future than writing tests? Probably hiring a bunch of interns to test code manually after each commit, but we don't have time and money for that so tests will have to do.

In this post I will focus on the importance of black box testing and how this can be achieved in React. I will be using jest, react-testing-library, redux-thunk and Typescript for my own sanity.
I am also going to assume that you know the basics of these technologies.

In addition, I will only focus on Component Integration Testing - how to test flow of your component in isolated environment - because there are no good resources on this topic, and Unit Testing is well understood in the community.

Black box testing, why bother?

I am big fan of black box testing, especially when working with front-end. In the world of javaScript frameworks changing so fast that before you download npm dependencies this dependency is already outdated I cannot imagine writing tests that can be easily broken by the change in implementation that doesn't have any impact on the behavior of the application.
This kind of environment discourages developers from refactoring and writing tests because everything you touch has high chance of breaking something (be it tests or the actual code).

We can avoid this problem by changing the way of thinking about our code and trying to test behavior and not the internal implementation (that can be easily changed). Doing so greatly reduces the need of changing tests when we want to change the way that our code works (e.g. refactoring code for optimization reasons).

For example, when testing, let's say, spinner component for data loading, I shouldn't care if the way of triggering that spinner is implemented with events or some boolean flag, this is an implementation detail that can change, but the behavior of the spinner on that page will stay the same.

You might say, that this is a very basic example, real world is not that easy and components have some kind of dependencies (e.g. component is calling some endpoint to retrieve data), and how do I test that?

Keep that in mind that topic of this post is integration testing and how to do that in the most flexible way.
Your tests suite should still consist mostly of unit tests, but from time to time integration tests can come in handy and I will present to you how to write integration tests that are cheap and easy to maintain.

Test scenario

Note: All code samples can be found on my github

Let's say that we want to test component that is responsible for managing movies, and for this basic example our component will have two methods: retrieveMovies() and clearMovies().

MoviePanel.component.tsx

https://gist.github.com/f321e7a78b355e94437070949e63b322

Movie.action.ts

https://gist.github.com/eabacd0f886441b16735aeb7bfda50c7

Movie.reducer.ts

https://gist.github.com/2f71bb978a6003bef261f6e7b3f8cce2

Let's assume that method getAllMoviesREST()in Movie.action.ts is an API call that returns promise (for the simplicity of this example under the hood it is just a mock but I'll leave it to your imagination to do the REST). Now there is one problem, how do we write test for a component that is depended on external API? Well there are two most popular options:

You can intercept all API calls with some test interceptor (external library) but that will leave your test fragile and hard to mock up (any change in the API method will force you to do some changes in test) and we want to avoid that.
Or we can apply Inversion of Control principle and take our dependency (the getAllMoviesREST() method) as a parameter to action (as a method reference) and then compose our component, with all of its dependencies in a MoviePanel.component.tsx.

Making our component testable

Firstly let's make our action method (retrieveMoviesAction()in Movie.action.ts) independent of concrete implementation of getAllMoviesREST() by simply receiving this method as function parameter.

Movie.action.ts

https://gist.github.com/5b1d89431663733cfb543fb3409500e0

Simple enough.

Ok but now our MoviePanel.component.tsx is complaining that function retrieveMoviesAction() requires 1 argument and not 0.

Now you might be tempted to just directly add concrete implementation of our getAllMovies method in mapDispatchToProps like this.

MoviePanel.component.tsx

https://gist.github.com/9c4f6af40433ad7869658812f526a8b3

But this solution still blocks us from injecting mocks into our component.
Now we need to apply Inversion of Control principle for mapDispatchToProps in conjunction with currying.

https://gist.github.com/f30809bed9473454301282aba53141d7

Notice that we pass getAllMoviesREST in the connect function, allowing connect function to compose our component.

I will provide detailed explanation on how it exactly works later.

In order to create MoviePanel component in tests we have to add few exports

MoviePanel.component.tsx

https://gist.github.com/3ddc7b9fa078708036ecd8641025f80b

https://gist.github.com/2326b65d335e25de3f07f23e291e1281

https://gist.github.com/931eae72eb3870c94b6ffa4fe2655165

And in order to check if our component was changed upon some action we need to add data-testid in two places.

MovieControls.component.tsx

https://gist.github.com/b4aca1c935defa1d6bd5a86193edc12e

MovieList.component.tsx

https://gist.github.com/552af3cef5e768717ebcd9b3d3af133d

Testing our component

Testing components connected to redux is very similar to testing typical components. The main difference is that we have to create component using connect function, wrap our component in Provider component and create mock store.

Let's start with creating our component with connect function.

https://gist.github.com/b2f9ec33702e3ef977cd8ec2c257021f

(The component name has to start from upper case)
Also note that we have to import MoviePanel as not default export (hence the curly brackets around import).
And that's it, now we created mock component with injected dependencies.

Now we need to create store

https://gist.github.com/34d3587c2b53065d1984c65717feff52

Simple enough.

And finally rendering our component

https://gist.github.com/d35f8e3d4688f9d26eee0644938023fc

Notice that we are awaiting for the render function, if we don't do this then our test won't wait for reducer to finish (even though dispatches in redux are synchronous)

In the end our sample test can look like this.

https://gist.github.com/326b50a5a6460361f1375fc2ca7be5d8

How are we able to inject dependencies in connect function?

Let's take a look into the connect function. What does the connect function do?

https://gist.github.com/51464fefa50adbdf22ec56efdce7d814

To perform injection, we will take a closer look into mapDispatchToProps argument.

connect

https://gist.github.com/3d42d01fe8e465b1bf68b00ca9506aa7

Yeah... that does not look simple, we need to go deeper.

mapDispatchToProps

https://gist.github.com/b41126c4ddcfec3092fc484d918c7a3f

Not deep enough

MapDispatchToPropsFunction

https://gist.github.com/07e7db7419ac396bf90918afba75e78e

Ok we can work with that.
As you can see our mapDispatchToProps should be a function that has two parameters
(dispatch: Dispatch<Action>, ownProps: TOwnProps) and this is the information that we were looking for.

Let's create that function
MoviePanel.component.tsx

https://gist.github.com/e3c96c7594d8f050f12da5cb54a0eeae

We are not using second paramter ownProps so we can omit it in JavaScript
But wait what's that the dispatch parameter was of type Dispatch<Action> and not ThunkDispatch<AppState, undefined, AppActions> is it a mistake?

No, thanks to ReduxThunk middleware standard redux dispatch is enhanced with thunk dispatch and now it's type looks like this ThunkDispatch<AppState, undefined, AppActions>

Since second parameter of connect function must be a function that takes dispatch: ThunkDispatch<AppState, undefined, AppActions> we can wrap our mapDispatchToProps function into anonymous function and pass dispatch parameter to mapDispatchToProps explicitly.

https://gist.github.com/5aa189a228bef6ce29b2f2c53a957bcf

Now we have full control over when to pass dispatch to mapDispatchToProps allowing us to create Higher Order Function and apply Inversion of Control to get rid of this nasty concrete implementation of getAllMoviesREST in our component.
Are you still with me? Good, let's just do that.

Time to move getAllMoviesREST to a parameter of mapDispatchToProps

https://gist.github.com/c790b0cea76e183c4c45b0917932531a

As you can see instead of adding another parameter next to dispatch in mapDispatchToProps we are wrapping it into another function by using currying. Thanks to this we won't be interfering with standard interface of mapDispatchToProps function (which takes ownProps as second parameter) allowing us to write it in more programmer friendly syntax.

Like this:

https://gist.github.com/0ff475ad957fe0cfdf069c47f6d90df9

This is also valid, but more explicit

https://gist.github.com/5d0384ed9f0fb0ef01b28dd9eef4f8ac

Now we can easily inject dependencies into our components, allowing it to be free of slow and unreliable communication means such as API calls, which makes tests faster and easier to maintain.

Huge shout out to Jacek Lipiec for helping me to figure this stuff out!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.