Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hooks #32

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
202 changes: 202 additions & 0 deletions docs/example-react-hooks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
---
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pulled in these examples from the rtl repo

id: example-react-hooks
title: React Hooks
---

`react-testing-library` provides the
[`testHook`](/docs/react-testing-library/api#testhook) utility to test custom
hooks.

> **Note**
>
> This is the recommended way to test reusable custom react hooks. It is not
> however recommended to use the testHook utility to test single-use custom
> hooks. Typically those are better tested by testing the component that is
> using it.

## Using `result`

Testing the last returned value of a hook using the `result` ref

```jsx
function useCounter({ initialCount = 0, step = 1 } = {}) {
const [count, setCount] = React.useState(initialCount)
const increment = () => setCount(c => c + step)
const decrement = () => setCount(c => c - step)
return { count, increment, decrement }
}
```

```jsx
test('returns result ref with latest result from hook execution', () => {
const { result } = testHook(useCounter)
expect(result.current.count).toBe(0)
act(() => {
result.current.increment()
})
expect(result.current.count).toBe(1)
})
```

## State

Testing a hook that provides state

```jsx
import { useState } from 'react'

export function useCounter({ initialCount = 0, step = 1 } = {}) {
const [count, setCount] = useState(initialCount)
const increment = () => setCount(c => c + step)
const decrement = () => setCount(c => c - step)
return { count, increment, decrement }
}
```

```jsx
import { testHook, act, cleanup } from 'react-testing-library'
afterEach(cleanup)

describe('useCounter', () => {
test('accepts default initial values', () => {
let count
testHook(() => ({ count } = useCounter()))

expect(count).toBe(0)
})

test('accepts a default initial value for `count`', () => {
let count
testHook(() => ({ count } = useCounter({})))

expect(count).toBe(0)
})

test('provides an `increment` function', () => {
let count, increment
testHook(() => ({ count, increment } = useCounter({ step: 2 })))

expect(count).toBe(0)
act(() => {
increment()
})
expect(count).toBe(2)
})

test('provides an `decrement` function', () => {
let count, decrement
testHook(() => ({ count, decrement } = useCounter({ step: 2 })))

expect(count).toBe(0)
act(() => {
decrement()
})
expect(count).toBe(-2)
})

test('accepts a default initial value for `step`', () => {
let count, increment
testHook(() => ({ count, increment } = useCounter({})))

expect(count).toBe(0)
act(() => {
increment()
})
expect(count).toBe(1)
})
})
```

## Unmount Side-Effects

Using the `unmount` function to check useEffect behavior when unmounting

```jsx
import { useState, useEffect } from 'react'

export function useDocumentTitle(title) {
const [originalTitle, setOriginalTitle] = useState(document.title)
useEffect(() => {
setOriginalTitle(document.title)
document.title = title
return () => {
document.title = originalTitle
}
}, [title])
}
```

```jsx
describe('useDocumentTitle', () => {
test('sets a title', () => {
document.title = 'original title'
testHook(() => {
useDocumentTitle('modified title')
})

expect(document.title).toBe('modified title')
})

test('returns to original title when component is unmounted', () => {
document.title = 'original title'
const { unmount } = testHook(() => {
useDocumentTitle('modified title')
})

unmount()
expect(document.title).toBe('original title')
})
})
```

## Rerender Side-Effects

Using the `rerender` function to test calling useEffect multiple times

```jsx
import { useEffect } from 'react'

export function useCall(callback, deps) {
useEffect(() => {
callback()
}, deps)
}
```

```jsx
describe('useCall', () => {
test('calls once on render', () => {
const spy = jest.fn()
testHook(() => {
useCall(spy, [])
})
expect(spy).toHaveBeenCalledTimes(1)
})

test('calls again if deps change', () => {
let deps = [false]
const spy = jest.fn()
const { rerender } = testHook(() => {
useCall(spy, deps)
})
expect(spy).toHaveBeenCalledTimes(1)

deps = [true]
rerender()
expect(spy).toHaveBeenCalledTimes(2)
})

test('does not call again if deps are the same', () => {
let deps = [false]
const spy = jest.fn()
const { rerender } = testHook(() => {
useCall(spy, deps)
})
expect(spy).toHaveBeenCalledTimes(1)

deps = [false]
rerender()
expect(spy).toHaveBeenCalledTimes(1)
})
})
```
66 changes: 66 additions & 0 deletions docs/react-testing-library/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -271,3 +271,69 @@ This is a light wrapper around the
[`react-dom/test-utils` `act` function](https://reactjs.org/docs/test-utils.html#act).
All it does is forward all arguments to the act function if your version of
react supports `act`.

## `testHook`

`testHook` is a utility to test custom hooks. It is designed to help test
reusable hooks in isolation.

You should also write integration tests for components using custom hooks, and
one-off hooks should be tested as part of the component instead.

**Usage**

```jsx
import { testHook } from 'react-testing-libary'

testHook(hook[, renderOptions])
```

**Arguments**

- `hook` customHook to test
- `renderOptions` options object to pass to the underlying `render`. See
[render options](#render-options). This is mostly useful for wrapping the hook
alexkrolick marked this conversation as resolved.
Show resolved Hide resolved
with a context provider.

**Returns**

```jsx
const { rerender, unmount, result } = testHook(hook)
```

- `rerender` Call this function to render the wrapper again, i.e., to test that
the hook handles props changes
- `unmount` Call this to unmount the component, i.e., to test side-effects and
cleanup behavior
- `result` An object that acts like a React ref with a `current` property
pointing to the last value the hook returned. For example:
`expect(result.current.count).toBe(0)`

**Example**

```jsx
// Example custom hook
function useCounter({ initialCount = 0, step = 1 } = {}) {
const [count, setCount] = React.useState(initialCount)
const increment = () => setCount(c => c + step)
const decrement = () => setCount(c => c - step)
return { count, increment, decrement }
}
```

```jsx
// Test using the `result` ref
test('returns result ref with latest result from hook execution', () => {
const { result } = testHook(useCounter)

expect(result.current.count).toBe(0)
act(() => result.current.increment())
expect(result.current.count).toBe(1)
})
```

**More**

- [More Examples](/docs/example-react-hooks)
- [Tests](https://github.com/kentcdodds/react-testing-library/blob/master/src/__tests__/test-hook.js)
- [Types](https://github.com/kentcdodds/react-testing-library/blob/master/typings/index.d.ts)
16 changes: 16 additions & 0 deletions website/blog/2019-02-06-react-hooks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a little FYI blog post about hooks, since they are new in the docs

title: React Hooks Are Supported
author: Alex Krolick
authorURL: http://github.com/alexkrolick
---

[Hooks have been released in React 16.8](https://reactjs.org/blog/2019/02/06/react-v16.8.0.html#testing-hooks)
and they are supported out of the box by `react-testing-library`!

Because `react-testing-library` only uses the external interface of your React
components, hooks work right away! If you rewrite a class component with hooks
your tests should still pass.

For unit testing custom hooks, we've also added a `testHook` utility. Check out
the [docs for `testHook`](/docs/react-testing-library/api#testhook). Thanks to
[@donavon](https://github.com/donavon) for the PR.
1 change: 1 addition & 0 deletions website/sidebars.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"example-input-event",
"example-update-props",
"example-react-context",
"example-react-hooks",
"example-react-redux",
"example-react-router",
"example-reach-router",
Expand Down