TDD'ing the Implementation of mapDispatchToProps
I like my React.js presentational (dumb) components (and therefore corresponding .js files) to be truly dumb. All I like to see in them is jsx, the react component, and props...that's it. I don't want those files to hold any behavior or local state. I don't want React lifecycle methods or other behavior muddying up my presentational components. Local component state, any react lifecycle component behavior, etc. can be maintained/centralized in other parts of our UI such as connected container components, action creators, and so on.
Yesterday, I needed to implement some new behavior for a feature I'm working on. I'd like to show you how to implement that behavior via Test Driven Development.
Before we start, lets pretend that someone has previously already TDD'd a <LoginContainer />
component. And the test that they wrote which forced them at th etime to implement that code was this test:
describe('Login Container', () => {
it('shows Login', () => {
const loginContainer = shallow(<LoginContainer store={fakes.store()} />),
login = loginContainer.dive().find(Login)
expect(login).to.exist
})
})
The story/feature for this blog post that we need to start working on is titled User can log in
.
It's ok to brainstorm a bit before you start implement the code via TDD, and to think about how things might work, but knowing that TDD might take us down a different route as we flush things out. By brainstorming for a moment, it allows us to think about how we might approach it and what our next test should be
So to start off, I'm thinking we'll probably need a handler that we can call from the onClick()
of a login button that currently lives in our <Login />
component.
Quick Note: This blog post is only showing TDD'ing some of the initial behavior for this. Further implementation for the login handler will live in our action creator .js file, reducers, etc. We won't go that far in this blog post. Subsequent action creator tests, reducer tests, etc. we will force us to flush out/TDD the of that behavior/implementation for this story.
We've decided that the next test should probably be to make sure that we can at least pass a login handler to our <Login />
component. Remember, this is TDD, so we take quick baby steps (fast feedback cycles).
Hmm.. simplest test possible that I we think of that we could write next, which would force us to create the minimal amount of code to achieve this behavior?
Well, this might be a good one:
it('passes login handler to Login', () => {
})
Ok, so lets think about this test for a second.
Lets take the approach of writing an assert first. Then after that assert fails, we can work our way up to writing the arrange and act part of our test.
Now before we write that assert. Keep in mind that we do know some knowledge up front about how react-redux
works, so I didn't just come up with this assert out of thin air.
Lets start by writing an assert that asserts something we wish we had working that we don't yet (AKA wishful thinking
in TDD):
it('passes login handler to Login', () => {
expect(container.find(Login).props().login).to.exist
})
Ok, ok, I don't even know if I've got my assert syntax correct. I'm not sure if this test will even compile yet. That's fine. We'll run the test soon an see. And TDD will force us to correct it as go. And honestly, I don't even know if I like the way I named the test. But lets not worry about that right now. We can always refactor in the blue step in TDD to make things more readable, clean, etc.
What I'm wishing here is that <LoginContainer />
which is currently rendering <Login />
has successfully passed the login handler via props to the <Login />
.
When we start a new test in the TDD cycle, we always run our test first to see it fails. If it does not fail, it's not a good test. If it does not fail, it means one of two things:
- it's green but it's because you got a false positive
- or you're writing a test for code that already exists...you're not TDD'ing, you're writing tests after which is not what we want to be doing here
Lets run this test to see it fail before we proceed:
Awesome, now we're ready to make it pass by writing the prod implementation.
Hmm, the test results are telling me the test fails because it doesn't know what container
means. We know we already have <LoginContainer />
available to us as mentioned earlier, but how can we work with it? And how are we going to work with the container in an isolated fashion? Well, enzyme has a shallow()
method which is perfect for creating isolated unit tests.
Lets use shallow, and lets create that missing constant that our assert is trying to assert on:
it('passes login handler to Login', () => {
const container = shallow(<LoginContainer store={fakes.store()} />)
expect(container.find(Login).props().login).to.exist
})
Here, I'm shallow rendering <LoginContainer />
and sending in a dummy store, because I don't care about the store; the store in this case just a collaborator, not the thing we're trying to test (SUT). I care about being able to work with the container, and to be able to implement behavior in that container which passes the login handler to my <Login />
component. So by dummy
I mean "lets tell our code to ignore that part, we don't care about it nor do we want it interferring with what we're trying to test for".
Shallow
rendering ensures that we only go one level deep on whatever component we're testing. That is...we want to make sure we are testing the SUT component's behavior only, not any of its child component implementations/behaviors.
If we start going deeper, such as trying to check behavior in any of its child components, we've actually just stepped into integration land. It's no longer a unit test because we're testing behavior of multiple componets (more than one level in the React component tree) and so our test is no longer isolated to just testing this component's (SUT's) behavior.
That's why those Jest Snapshots are integration tests not unit tests. They're fragile in that they testing multiple components in your app's React Component tree. That's ok for integration tets, but they don't make sense nor are they a replacement for isolated unit tests. And TBH I'm not a big fan of Jest Snapshots for several reasons I won't elaborate on in this post. I prefer to write my own integration tests as I have in any language I've written; they really aren't that big of a deal to write once you get the hang of it. I still write integration tests with mocha and enzyme or mocha and supertest depending on what kind of integration test I'm writing.
Before we proceed, one last thing about the "don't step into integration land". We're using shallow()
rendering because that ensures we're only testing the immediate component's behavior. If we want to keep our test isolated, we don't want to be using the following in our unit test:
- mount()
- render()
- shallow() then dive()
- shallow(), then shallow() again (double-shallow...we shallow the container and shallow the login, we don't want that)
Now back to the test. Lets run it again:
it('passes login handler to Login', () => {
const container = shallow(<LoginContainer store={fakes.store()} />)
expect(container.find(Login).props().login).to.exist
})
Eh! still an error:
No biggie, we're making progress because we know what we need to do next...resolve this next error. But I'll let you in on a little secret. Enzyme rocks, but sometimes its error messages don't. Having worked with enzyme for quite a while now I can tell you that this:
Method “props” is only meant to be run on a single node. 0 found instead.
Ok what the enzyme team should have said was props() cannot be run on undefined
or something like that. It means it didn't find the Login component, and then it tried to run props()
on undefined
...so it's blowing up.
Ok well, hmm...why couldn't it find the <Login />
component in the shallowly rendered <LoginContainer />
connected component?
Well lets check a few things:
- Has the
<Login />
component even been created yet? - If so, did we tell
<LoginContainer />
to render<Login />
yet?
lets go look:
class LoginContainer extends Component {
render(){
return( <Login /> )
}
}
export { Login }
export default connect(undefined, mapDispatchToProps)(LoginContainer)
It turns out we have. If you recall, we had a previous test that was written at some point in the past by another person TDD'ing that forced whoever coding at the time to create the <Login />
component and to also make sure that the container is rendering it. That test was the test we saw at beginning:
it('shows Login', () => {
const loginContainer = shallow(<LoginContainer store={fakes.store()} />),
login = loginContainer.dive().find(Login)
expect(login).to.exist
})
(My test says "shows" not "render" because I like to keep technical terms out of my test names and keep it business or natural language.)
Ok, well, if those two things aren't causing the error, what is? It turns out, that we actually do need to call enzyme's dive()
method on our shallowly rendered container component. Yea yea, I know. I told you not to dive()
...that it would make it an integration test. Well not so fast. It depends. Sometimes you've got some weird situations. And it depends what you're diving into . So making use of dive()
is a judgement call whether it makes sense to be doing that or not.
Sometimes that is not an easy line to draw or to recognize when you've cross the line into making it an integration test. That comes with experience and practice. I'm able to recognize when it is and when it isn't because I've practiced TDD for quite a while, especially with React.
So in this case it's safe to dive()
. It's is still an isolated unit test because I don't have a choice but to dive()
to be able to work with that shallowly rendered connected container component. Why you ask? The keyword here is connected, meaning react-redux's connect()
magic. Because when we shallowly rendered a connected container:
const loginContainer = shallow(<LoginContainer store={fakes.store()} />)
What we got back was actually more than you realize because we're using enzyme and react-redux. We got back an enzyme api wrapper
that wraps a connect wrapper
that is wrapping our shallowly rendered <LoginContainer />
(I think I got all that right). I know, your head is probably spinning but anyone who has worked with Redux for a while, knows exaclty what I mean. If you're new to Redux, your head will be spinning; keep practicing with react-redux.
So because of that unique situation, we need to dive()
to get down a level and to get past the collaborator methods (methods we don't implement, we don't have control off, and that we are not testing). We don't want to work with the connect wrapper, we want to dive and work with the container component and then do our find on the container component. So that's why. This is still an isolated unit test. It's just that here, we used dive()
to get past react-redux's magic / connect wrapper function so we can work with our <LoginContainer />
.
Now you may ask, when would dive()
make your test turn into an integration test?
Well, if you were to shallow render a non-connected React component, say a presentational component and did an enzyme find()
on it to check whether that component contains some child component. And did dive()
on that child; now you've stepped to0 far down the component tree: (shallow() => find() => dive()) You're stepping into the implementation of that child and you do not want to do that for isolated unit tests. That's fine for an integration test but we're not writing integration tests right now, we're TDD'ing our implementation by writing isolated unit tests.
Sorry for steering off onto another path. Lets get back to the test and get this passing.
Ok we know one thing we need to do is add dive()
, so lets do that:
it('passes login handler to Login', () => {
const container = shallow(<LoginContainer store={fakes.store()} />)
expect(container.dive().find(Login).props().login).to.exist
})
So now we have container.dive().find(Login)
, not container.find(Login)
. Lets run our test again:
Notice how our tests guide us, and is telling us the next step we need to do, which is to resolve this error.
Ah, now's the time we need to create mapDispatchToProps
! This is why Test driven development is nice. It'll guide you like this, which makes your life very easy! Lets do it!
Right now as it stands, here's our component:
class LoginContainer extends Component {
constructor(props) {
super(props)
}
render(){
return ( <Login /> )
}
}
export { Login }
export default connect(undefined, mapDispatchToProps)(LoginContainer)
Lets implement mapDispatchToProps
now and get this test to pass:
import { login } from '../../actions/UserActions'
class LoginContainer extends Component {
render(){
return( <Login login={this.props.login} /> )
}
}
const mapDispatchToProps = {
login: login
}
export { Login }
export default connect(undefined, mapDispatchToProps)(LoginContainer)
Ok, I added 3
things here:
import { login } from '../../actions/UserActions'
pass container's props.login to the Login component
that it is available to call from our button's onclick event
<Login login={this.props.login} />
this will actually add a login prop on the container since we're connecting to our container, not our Login
component via connect():
mapDispatchToProps = {
login: login
}
Note: we've defined our login handler function in UserActions
(sometimes it's nice to keep your action creators and related handlers together). In this case, login(email, password)
is going to fetch some data, do a few dispatches
, and eventually our reducer will update state with the data coming back from the fetch()
call.
Cool. Lets run our test again, does it turn green?
It did! Our test forced us to test drive the implementation of mapDispatchToProps, along with minimal logic to pass the login handler as a prop to <Login />
Yay! And, we know it works because we failed first, then made it green! AND now we're covered by our tests!
On top of all this, our code is being kept lean each test we write because we follow rules of TDD:
- our test is super small
- we're only writing enough production code to make a test pass
- we refactor our tests and production code often (clean code / architecting) along the way
What you'll find is you'll usually end up with less implementation code when you TDD rather than write tests after or not write them at all.
What's interesting about this is orignially I planned on TDD'ing the mapDispatchToProps itself (including exporting it to make it public so that is possible), but found I didn't need to do that.
What's the next test? I'll let you ponder that. But it'll be a test that forces us to create a little more behavior. It'll be a test that forces us to start implementing that login handler.
If you liked this post, I'd love to have you express that by helping to donate/support a great resource I started.
What post would you like to see next? Tweet it to me at @DaveSchinkel
Cheers!