In this article I want to show you how easy is to create component just starting from tests – react TDD. What we need to do on the start is specification. Here we can something more sophisticated than counter or TODO list. Let say, searchable list component. Our list will take as an argument a list of strings, create a list and input with search option. Pretty easy? Stay with me – we will not see that in the browser until we end writing tests and component to the point we accomplish the task.

As a base we can use repository from this article.

  1. Clone and install repository
  2. Create empty component
  3. First test
  4. Add list check in tests
  5. Test user interaction
  6. Summary

Start

Repository

Install add dependencies

npm install

Check the tests

npm test

For this article change in package.json line in scripts with test:

"test": "react-scripts test --watchAll=false",

to:

"test": "react-scripts test",

That will run tests with watch mode – on each save tests will run again.

Create empty component

In src/components add new file searchableList.tsx. And prepare empty component:

import React, { useState } from 'react'

export const SearchableList: React.FC<{}> = () => {

  return (
    <>
    </>
  )
}

First test

Here we start with react TDD. At the beginning we want to have input filed and rendered list. Let’s write the test witch will check both! Create in src/components file searchableList.test.tsx.

import React from 'react'
// tslint:disable-next-line: no-implicit-dependencies
import { create } from 'react-test-renderer'

import { SearchableList } from './searchableList'

describe('SearchableList component tests', () => {
  test('it render input and list', () => {
    const list = create(<SearchableList />)
    const instance = list.root
    const inputs = instance.findAllByType('input')
    expect(inputs.length).toBe(1)
  })
})

This test checks if there is any input in the component. In result we have an error:

No instances found with node type: "undefined"

This is our very first TDD – what we have to do now is update searchableList.tsx the way to pass the test. Go!

import React, { useState } from 'react'

export const SearchableList: React.FC<{}> = () => {

  return (
    <>
      <input type="text" />
    </>
  )
}

When you save the file the test should pass. We have input in component.

Add check for the list

Add some data for test and check for the list we want to render (searchableList.test.tsx)

import React from 'react'
// tslint:disable-next-line: no-implicit-dependencies
import { create } from 'react-test-renderer'

import { SearchableList } from './searchableList'

const cities = [
  'New York',
  'London',
  'Paris',
  'Lisbon',
  'Tokio',
  'Pekin',
]

describe('SearchableList component tests', () => {
  test('it render input and list', () => {
    const list = create(<SearchableList list={cities} />)
    const instance = list.root
    const inputs = instance.findAllByType('input')
    expect(inputs.length).toBe(1)
    const listElements = instance.findAllByType('li')
    expect(listElements.length).toBe(cities.length)
  })
})

Now we want to render full list – 6 cities but as a result we get length = 0. Let’s update component again in the way that it will pass the tests (searchableList.tsx).

import React, { useState } from 'react'

type SearchableListProps = {
  list: string[],
}

export const SearchableList: React.FC<SearchableListProps> = ({ list }) => {

  return (
    <>
      <input type="text" />
      <ul>
        {list.map((value, index) => <li key={index}>{value}</li>)}
      </ul>
    </>
  )
}

Save and observe that test pass. Easy? Now lets test some user interaction with the list. We can simulate typing string in input.

Test user interaction

import React from 'react'
// tslint:disable-next-line: no-implicit-dependencies
import { act, create } from 'react-test-renderer'

import { SearchableList } from './searchableList'

const cities = [
  'New York',
  'London',
  'Paris',
  'Lisbon',
  'Tokio',
  'Pekin',
]

describe('SearchableList component tests', () => {
  test('it render input and list', () => {
    const list = create(<SearchableList list={cities} />)
    const instance = list.root
    const inputs = instance.findAllByType('input')
    expect(inputs.length).toBe(1)
    const listElements = instance.findAllByType('li')
    expect(listElements.length).toBe(cities.length)
  })
  test('it search the listenerCount', async () => {
    const list = create(<SearchableList list={cities} />)
    const instance = list.root
    const input = instance.findByType('input')
    const event = {
      target: {
        value: 'on',
      },
    }
    await act(async () => {
      input.props.onChange(event)
    })
    const listElements = instance.findAllByType('li')
    expect(listElements.length).toBe(2)
  })
})

When saved we can see an error in the test. We don’t have prop onChange in the component. Let start with it.

import React, { ChangeEvent, useState } from 'react'

type SearchableListProps = {
  list: string[],
}

export const SearchableList: React.FC<SearchableListProps> = ({ list }) => {

  const [search, setSearch] = useState('')

  const handleChange = (event: ChangeEvent<HTMLInputElement>) => setSearch(event.target.value)

  return (
    <>
      <input type="text" value={search} onChange={handleChange} />
      <ul>
        {list.map((value, index) => <li key={index}>{value}</li>)}
      </ul>
    </>
  )
}

Now we can see no error in the test. But still doesn’t pass – we don’t have any logic in component to update our list.

import React, { ChangeEvent, useState } from 'react'

type SearchableListProps = {
  list: string[],
}

export const SearchableList: React.FC<SearchableListProps> = ({ list }) => {

  const [state, setState] = useState({
    actualList: list,
    search: '',
  })

  const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
    setState({
      actualList: list.filter((item) => item.indexOf(event.target.value) >= 0),
      search: event.target.value,
    })
  }

  return (
    <>
      <input type="text" value={state.search} onChange={handleChange} />
      <ul>
        {state.actualList.map((value, index) => <li key={index}>{value}</li>)}
      </ul>
    </>
  )
}

Now our tests pass.

Summary

At this point we can test a lot of other stuff – if value in input change properly, add some debouncing, create more tests with usage of capital letters.

A lot of people don’t write test’s at all – that sad because tests are an excellent documentation and helps a lot to understand how something works. It’s far more easier to maintain the whole application or rewrite it in the future when you have proper tests first. Why TDD – in most cases when we start to write a project the first though is that we will write tests in the future – functionalities first! In this approach the future never came. Happy TDD! πŸ™‚

Repo.

If this article was helpful for you – buy me a 🍺🍺🍺


2 Comments

brian · October 4, 2019 at 12:40 pm

great post! btw React.SFC is deprecated, you should use React.FC

https://github.com/DefinitelyTyped/DefinitelyTyped/pull/30364

    woles · October 4, 2019 at 10:08 pm

    My mistake – this is the same thing almost, now when we have hooks and we can have state in functional component so it would be confusing to see stateless functional component (SFC) πŸ™‚ so now we have only functional component (FC). Thank you for the comment!!

Leave a Reply

Your email address will not be published. Required fields are marked *