React and Redux integration testing - Kacper Starzyński - Medium
source link: https://medium.com/@kacperpatrykstarzynski/react-and-redux-integration-testing-3b623f680f5b
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
React and Redux integration testing
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
Movie.action.ts
Movie.reducer.ts
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
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
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.
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 at the end of this post.
In order to create MoviePanel component in tests we have to add few exports
MoviePanel.component.tsx
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
MovieList.component.tsx
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.
(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
Simple enough.
And finally rendering our component
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.
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?
To perform injection, we will take a closer look into mapDispatchToProps
argument.
Yeah… that does not look simple, we need to go deeper.
mapDispatchToProps
Not deep enough
MapDispatchToPropsFunction
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 functionMoviePanel.component.tsx
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.
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
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:
This is also valid, but more explicit
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!
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK