DEV Community

loading...

Don't Be Afraid of ... Snapshot Testing and Mocking Forms and Props in React

neosaurrrus profile image Lukie Kang ・9 min read

In our last post, we got introduced to React Testing via React Testing Library. For the sake of keeping things short and sweet, we left out a few extra things to talk about. For that reason, this post will be quite a mixture of things. In this post we will look at:

  • Snapshot Testing
  • Mocking a Form submission
  • Testing for errors
  • Testing Specific Input Values
  • Negative Assertions

Snapshot Testing.

Snapshot testing sounds a bit like what it sounds like. If you took a photo of the resulting code, did something then happen that makes it look different to that photo? Because we take the snapshot at a high-level on the component, typically the enclosing Div Snapshot testing lets us watch for changes across everything under that element. However, since Snapshot testing compares to a moment frozen in time, it works great for components that are static in nature, but ones with dynamic changeable elements, they will just be noise. Certainly, they get in the way while actually doing TDD. Anyhow. let's look at implementing it.

Implementing Snapshot Testing

Jest makes this a doddle. First we need to grab container from our render:

const {container} = render(<NewBook/>)

Container being the contents of the rendered component including any child components. Then we want to say what we expect to match the Snapshot:

expect(container.firstChild).toMatchSnapshot();

The firstChild in this regard is the enclosing div.

Soon as you have done that for the first time, Jest will do something cool, it will create the snapshot for us in the __snapshots__ folder. If you check it out you will see it is basically the output of the enclosing div. That's cool but here what I said about it being best for things that done change very often, what if you decide you wanted to add or tweak something? For example, an extra

tag? Soon as you have done that the test suite will be pointing out it no longer matches the snapshot:

expect(value).toMatchSnapshot() Received value does not match stored snapshot 1. - Snapshot

Snapshot Summary
1 snapshot test failed in 1 test suite. Inspect your code changes or press u to update them.`

If it was a tweak that was intended, then as it says, it's straightforward to update the snapshot with a tap of the u key. This also makes it easy to accept something that has not been intended so be careful that Snapshot does not make things too easy for you to the point you snapshot intended stuff.

Still, snapshot testing is a very useful way of quickly flagging when something changes and definitely should be considered for less dynamic components. This not intended as a replacement for unit testing, and it's not really practical to write a snapshot so they are not really compatible with TDD principles but provide a good quick additional layer of testing. You can learn more from the JEST Documentation about Snapshots

Mocking and Spying a Form Submission

Ok, so let's take another look at Mocking which I touched on in my first testing post. But this time we can apply it to a more complex real-world example. Namely, let's look at a testing a form component. This is a common use case for mocking a function as we don't want to actually submit data to the database when we test things. I am sure we all have databases that are full of entries like "test" and "aaaa" from our manual testing days, let's see about reducing that a little!

So let's go with a New Book Form that takes a book title and submits it, not too complex but will do as an example. First of all let's build out the test to:

  1. Check the button exists,
  2. And tell the test suite to click it.

`

import React from 'react'
import { render, cleanup, fireEvent} from 'react-testing-library'; //Added FireEvent from React Testing Library
import BookForm from './BookForm';

afterEach(cleanup)

test('<BookForm>', () => {
  const {debug, getByText} = render(<BookForm/>)
  expect(getByText('Submit').tagName).toBe('BUTTON') //Looks for an element with the text Submit, just for the sake of being different.
  fireEvent.click(getByText('Submit'))
  debug()
});
Enter fullscreen mode Exit fullscreen mode

So let's build the component with the button and also a little cheeky function when the form is submitted:

import React, { Component } from 'react'

export default class BookForm extends Component {
    render() {
        return (
            <div>
               <form data-testid='book-form' onSubmit={ ()=> console.log("clicked the button!")}>
                   <button type="submit">Submit</button>
               </form>
            </div>
        )
    }
}
Enter fullscreen mode Exit fullscreen mode

The reason I added that click function is to show that when we run the test, we can see that clicked the button! appears in the log:

PASS  src/BookForm.test.js
  ● Console
    console.log src/BookForm.js:10
      clicked the button!
Enter fullscreen mode Exit fullscreen mode

That might be useful in testing things work in a quick and dirty way. But if that form submission actually did something, then our tests would start getting dangerous so we need a safe way to submit the form when testing. To do this we need to consider the pattern we use for the component so we can safely mock it. This involves providing the function that runs on submit via props. The component we will end up with looks like this:

export default class BookForm extends Component {

    state = {
        text: ''
    }
    render() {
        const {submitForm} = this.props
        const {text} = this.state
        return (
            <div>
               <form data-testid='book-form' onSubmit={ ()=> submitForm({text})}>

                   <button type="submit">Submit</button>
               </form>
            </div>
        )
    }
}
Enter fullscreen mode Exit fullscreen mode

Ok, so the big question here is, why have we bumped the submitForm function to props? Because we need to change what that function does if it is run by our test compared to its normal job in the application. This will make sense when we look at the test we have written:

import React from 'react'
import { render, cleanup, fireEvent} from 'react-testing-library'; 
import BookForm from './BookForm';

afterEach(cleanup)
const onSubmit = jest.fn(); //Our new Spy function

test('<BookForm>', () => {
  const {debug, getByText, queryByTestId} = render(<BookForm submitForm={onSubmit} />) // The spy function is used to for the submit form

  //Unit Tests to check elements exist
  expect(queryByTestId('book-form')).toBeTruthy()
  expect(queryByTestId('book-form').tagName).toBe("FORM")
  expect(getByText('Submit').tagName).toBe('BUTTON')

  //Check Form Submits
  fireEvent.click(getByText('Submit'))
  expect(onSubmit).toHaveBeenCalledTimes(1); //This tests makes sure we van submit the spy function
  debug()
});
Enter fullscreen mode Exit fullscreen mode

So to repeat what the comments say, we...:

  1. Create a spy function that does nothing
  2. This function is passed via props when we render the component.
  3. We test to see if it runs with a expect(onSubmit).toHaveBeenCalledTimes(1). Which hopefully it does.

This is all very clever but we have not done much but tested the form submits ok. Which is important but let's take things a step further looking at the inputs that are submitted.

Bonus: Spying on Console Errors

We can spy on pretty much anything we like. Even errors when a component is not called properly. Let's say, for example, we had a component that needs a bunch of props with specific proptypes defined, we may want to test what happens when we don't provide them. So we can use the mocking function to handle the console errors like so:

console.error = jest.fn()
test('<ExampleComponent'>, () => {
  render(<ExampleComponent />)
    expect(console.error).toBeCalled()
});
Enter fullscreen mode Exit fullscreen mode

Of course, while this gets rid of the console error, this will still show any errors that may occur due to the lack of props passed in.

Right, back to our scheduled blogging.

Specifying Input Values for testing

To make our testing more aligned to real-life we may want to write a test that checks that a form can be submitted with certain specified inputs. In our example, we want our Book Form to have a text input for a title. The way you might approach this is as follows:

  1. Find a way to target the relevant part to be tested (i.e. the input field)
  2. Change the value of the input.
  3. Check that the form was submitted with the value we wanted.

That is pretty good but there is a gotcha you need to be aware of. Changing the value of the input does not cause React's state to update in our test, we need to use a *change event to update the value for the change to occur. Here are the additional parts we need to add to do this:

test('<BookForm>', () => {
  const {getByLabelText} = render(<BookForm submitForm={onSubmit} />) //Adding the getByLabelText

  //1. Unit Test to check our input element exists
  expect(getByLabelText('Title').tagName).toBe('INPUT') //test to make sure the input is there

  //2. change the Input Value using the change event.
  fireEvent.change(getByLabelText('Title'), {target: {value: "Girl, Woman, Other"}}) //This event sets the value of the input and lets the change affect the state. 

  //3. Check Form Submits as expected
  fireEvent.click(getByText('Submit'))
  expect(onSubmit).toHaveBeenCalledWith({title: 'Girl, Woman, Other'}) //This checks that the submission has the title we asked it to have earlier.

Enter fullscreen mode Exit fullscreen mode

Note that I am using a new query, getByLabelText which, unsurprisingly looks at the text of the label to find the element we are after. Step 2, is where we use our fireEvent. since our target is the input element, we need to drill down to find our value and change it. Finally, we can check what our Spy function used with the toHaveNeenCalledWith method which is hopefully an easy one to understand.

So we better see what the React code looks like that passes these tests:

import React, { Component } from 'react'
export default class BookForm extends Component {

    state = {
        title: '' //what gets sent on submit
    }

    render() {
        const {submitForm} = this.props
        const {title} = this.state
        return (
            <div>
               <form data-testid='book-form' onSubmit={ ()=> submitForm({title})}>
                   <label htmlFor="title">Title</label> //Remember that it is the text of the element our test is looking for not the HTMLFor
                   <input id="title" type="text" onChange={(e) => this.setState({title: e.target.value})}></input> //Quick and Dirty input controlling
                   <button type="submit">Submit</button>
               </form>
            </div>
        )
    }
}
Enter fullscreen mode Exit fullscreen mode

Cool, now it isn't the most complex form in the world but hopefully, you can see how the techniques can be scaled up accordingly and also are getting a grasp of how simply we test dynamic content. If you set up the snapshot test earlier you will now see they can be a little annoying when you are writing out the code!

Bonus: Negative Assertions

In our test we had the following line:

expect(onSubmit).toHaveBeenCalledWith({title: 'Girl, Woman, Other'})

Which is checking if that assertion is true if it did happen. There might be occasions where passing means checking if something did not happen. In Jest that's as easy as adding a not as part of the method like so:

expect(onSubmit).not.toHaveBeenCalledWith({title: 'Girl, Woman, Other'})

This can be useful when, for example, you are testing what happens when data is not provided by props to a component that needs them. Which is handy as our next topic is...

Mocking Props

So we are able to emulate form data, but another thing we commonly deal with in React is props. If our component needs props, we need a way of providing some. On a basic level, this is quite straightforward if all the above-made sense. In our test we need to:

  1. Mock out what the props should be
  2. Include those props when we render:
console.error = jest.fn()

const book = {
  title: "The Stand"
}

test('<Book> without Book props', () => { //No props so 
  render(<Book />)
  expect(console.error).toHaveBeenCalled();
})

test('<Book> with Book Props', () => {
  render(<Book book={book}/>)
  expect(console.error).not.toHaveBeenCalled();
})
Enter fullscreen mode Exit fullscreen mode

Pretty cool right? Well yes but now we are into multiple tests, we have a little gotcha to be aware of. In the example above we have two places where we check if the console.error has been called. Once without props and a second time without props where we expect that it will not run. However, if you run this it will fail as it will say that console.error was run the second time.... what gives?!

Put simply, console.error was called when it ran the first test so it thinks it was called when doing the second. The fix for this is fairly simple and requires a tweak to our clean up function.

afterEach( () => {
  cleanup
  console.error.mockClear()
})
Enter fullscreen mode Exit fullscreen mode

Now the memory of the console error is cleared between tests and things are more normal.

There are unfortunately lots of little gotchas you will hit as you start testing real-world components. A common one is around React Router expecting things that are not found in the test by default, it's beyond the scope of this blog post to cover all use cases but its the kind of thing that is going to need some research when you encounter them.

Taking a step by step approach when writing tests and code does help narrow down and help search for solutions to such issues.

Wrapping things up

This is one of those annoying blog posts where I touch on some things and ignore others. hopefully testing props, forms and inputs are useful to most users of React. My goal is to give a grounding in 60% of what you would typically test and give you a little context to search for the other 40%

Next time we can look at testing APIs and the async fun that brings!

Discussion (0)

pic
Editor guide