Skip to content
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

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 @@ -254,3 +254,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
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
2 changes: 1 addition & 1 deletion website/siteConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ const siteConfig = {
/* Colors for website */
colors: {
primaryColor: '#292422',
secondaryColor: '#3344bb',
secondaryColor: '#2468e5',
Copy link
Collaborator Author

@alexkrolick alexkrolick Feb 13, 2019

Choose a reason for hiding this comment

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

Unrelated, but the links were hard to read. This is a lighter color so they stand out from the text.

},

// Add custom scripts here that would be placed in <script> tags.
Expand Down
1 change: 0 additions & 1 deletion website/static/img/users/codecademy.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.