A Request to be Mocked

We’re starting the day with a failure.

The application test fails because it is trying to make a network request and, as everyone knows, we don’t want to make network requests in unit tests.

I suppose I could change the Application.test to use shallow() instead of mount() but then we wouldn’t actually be testing very much — and we’ll have to deal with this network request problem eventually.

Axios comes with an API for mocking network requests but that all feels a bit low-level to me.

What I’d like to do is refactor the code so that it is easy to switch out the network layer entirely when we are testing. Let’s introduce an API layer with a function to fetch posts.

We’ll start by extracting a method fetchPost(), then convert it to be an async function and await the response in componentDidMount(). Then, finally, we’ll move the function to its own file.

I’ll just show the final result.

// posts/api.js
import * as axios from 'axios'

export function fetchPost(id) {
  const request = {
    url: `http://localhost:3000/posts/${id}.json`
  }
  return axios(request).then(response => response.data)
}
  async componentDidMount() {
    const post = await fetchPost(1)
    this.setState({post})
  }

EXPLAIN PROMISES

EXPLAIN ASYNC

We’re not done yet because I’d like to introduce one more level of indirection. I want to abstract away the call to the server partly because this code will end up being used in a bunch of places and partly because that’s where I want to introduce my cleavage point for mocking.

All problems in computer science can be solved by another level of indirection.

David Wheeler
// remote/server.js
import * as axios from 'axios'

export const server = {send}
function send(url) {
  const request = {url}
  return axios(request).then(response => response.data)
}

// posts.api.js
import {server} from 'remote/server'

export function fetchPost(id) {
  return server.send(`/posts/${id}.json`)
}

There. That looks much nicer and now we are finally ready to mock out that server connection In Application.test.jsx.

import {Application} from 'application/Application'
import {server} from 'remote/server'

describe('The application', () => {
  server.send = jest.fn()

  beforeEach( () => {
    const post = {
      id: 1,
      title: 'React on Rails',
      body: 'I can use React with Rails.',
    }
    server.send.mockReturnValue(post)
  })

  test('shows the first blog post', async () => {
    const component = await mount(<Application />)

    expect(component.find('.site-name').text()).toEqual('Blogging')
    expect(component.find('.title').text()).toEqual('React on Rails')
    expect(component.find('.body').text()).toEqual('I can use React with Rails.')

    expect(server.send).toBeCalledWith('/posts/1.json')
  })
})

EXPLAIN jest.fn()

ADD A PASSED banner

Meet me in the Middle?

Story so far:

  • We have a Rails back end that can serialize a blog post to JSON.
  • We have a Reach front end that can render a blog post.

In this instalment, we want to connect the two together. Let’s do a bit of tidying first.

It sucks that our Application component is doing everything. I think we need a Post component to take care of the post stuff. Let’s extract one. Test first.

I’ll start by copy-pasting application/Application.test.jsx to posts/Post.test.jsx and then stripping out the non-post stuff.

describe('The post component', () => {
  test('shows a blog post', () => {
    const component = mount(<Post />)
    expect(component.find('.title').text()).toBe('React on Rails')
    expect(component.find('.body').text()).toBe('I can use React with Rails.')
  })
})

I’ll do the same thing with a Post component and check that all the tests still pass.

describe('The post component', () => {
  test('shows a blog post', () => {
    const component = mount(<Post />)
    expect(component.find('.title').text()).toBe('React on Rails')
    expect(component.find('.body').text()).toBe('I can use React with Rails.')
  })
})

Now I’ll change Application to use the Post component and get rid of the duplication.

import React from 'react'
import {Post} from 'posts/Post'

export function Application(_props) {
  return <div id='application'>
    <h1 className='site-name'>Blogging</h1>
    <Post />
  </div>
}

The tests still pass. Refactoring successful. There’s some overlap between Application.test and Post.test but I’m OK with that.

We’re almost ready to hook the Post component up to the backend but there’s one more fake it step we can take on the road to making it. Let’s pass the post data into the Post component so it doesn’t need to concern itself with where its data came from. Small steps are the best steps.

I’m going to introduce a mediating component between Application and Post that will be responsible for fetching the data. Here it is (the data is still hard coded for the moment).

export function Post({post}) {
  return <div className='post'>
    <h2 className='title'>{post.title}</h2>
    <div className='body'>{post.body}</div>
  </div>
}

export default function ConnectedPost() {
  const post = {
    id: 1,
    title: 'React on Rails',
    body: 'I can use React with Rails.',
  }
  return <Post post={post} />
}

Notice that I used the default modifier when I exported the ConnectedPost. You can export multiple components (or functions or constants or whatever) from each Javascript file but only one of them can be the default.

When you import a component from another file, you can use named imports like this:

// Application.jsx
import {Post} from 'posts/Post'
...
<Post />

If I skip the braces and import the default export (ConnectedPost), I can call it whatever name I like. I’m gonna continue to call it Post, like this:

// Application.jsx
import Post from 'posts/Post'
...
<Post />

Now that the post object is no longer hard-coded, we can update Post.test.jsx as a unit test for Post and pass the post in from the outside. We’ll test the ConnectedPost separately later.

  const post = {
    id: 1,
    title: 'The title',
    body: 'The body.',
  }

  test('shows a blog post', () => {
    const component = mount(<Post post={post}/>)
    expect(component.find('.title').text()).toEqual('The title')
    expect(component.find('.body').text()).toEqual('The body.')
  })

Before do the next interesting change I need to introduce some new concepts. Let’s start with class components.

Class components

All of our components so far have been stateless so it makes sense that they are pure functions. We’ll need to maintain state for our ConnectedPost when it retrieves data from the server so we are going to convert it to a class component.

Aside: The React folks have added hooks to allow you to maintain state in a functional component but the whole things still feels weird to me. I’m sticking with class components for now.

export default class ConnectedPost extends React.Component {
  render() {
    const post = {
      id: 1,
      title: 'React on Rails',
      body: 'I can use React with Rails.',
    }
    return <Post post={post} />
  }
}

This class component is exactly equivalent to the functional component that it replaced with all the rendering logic moved into a method called render(). But the class component gives us two new capabilities. The first is state management.

State Management

State management lets us keep track of internal changes to the component for when the user checks a box or presses a button. In our case, we are going to keep track of the post that we are fetching from the server.

There are three aspects to state management.

  1. Initialize the state in the constructor.
  2. Access the state in the render() method (and other methods as necessary).
  3. Change the current state with this.setState().

The reason we are letting React manage our state rather than just maintaining an instance variable is to give React the chance to re-render our component every time the state changes.

Let’s look at the first two together. I’ll change the component to initialize the state with an empty post in the constructor and then access that post in the render() method.

export default class ConnectedPost extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      post: {}
    }
  }

  render() {
    const {post} = this.state
    return <Post post={post} />
  }
}

Before I show you how to change the state, let me introduce the second capability of class components.

Lifecycle methods

The second new capability of class components is lifecycle methods. Lifecycle methods get called when a component is initialized or when some external event happens that the component needs to know about. The most important lifecycle method is componentDidMount().

Let’s use it now to change the state when the component is first mounted.

export default class ConnectedPost extends React.Component {
 ...
  componentDidMount() {
    const post = {
      id: 1,
      title: 'React on Rails',
      body: 'I can use React with Rails.',
    }
    this.setState({post})
  }
}

We’re now exactly back to where we started but now we have a place to call out to the server to fetch out post. Let’s do it.

  componentDidMount() {
    const request = {
      url: 'http://localhost:3000/posts/1.json'
    }

    return axios(request).then(response => {
      const post = response.data
      this.setState({post})
    })
  }

I’m using axios rather than fetch. My reasons are lost in the mists of time but I remember that fetch did something weird with 404 errors that I found annoying.

We have to install axios with Yarn…

yarn add axios

…and import it at the top of the Post.jsx file.

import * as axios from 'axios'

We are finally displaying an actual blog post that we loaded from the server but the way we are doing it now means that all our tests are broken.

We’ll fix that tomorrow.