Skip to content

Commit 98987c8

Browse files
pbombKent C. Dodds
authored and
Kent C. Dodds
committed
feat: Add getByTestId utility (#10)
* feat: Add getByTestId utility * Updates to getByTestId documentation * minor tweaks
1 parent 2cf3ab6 commit 98987c8

14 files changed

+143
-51
lines changed

Diff for: .all-contributorsrc

+12
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,18 @@
5555
"contributions": [
5656
"doc"
5757
]
58+
},
59+
{
60+
"login": "pbomb",
61+
"name": "Matt Parrish",
62+
"avatar_url": "https://avatars0.githubusercontent.com/u/1402095?v=4",
63+
"profile": "https://github.com/pbomb",
64+
"contributions": [
65+
"bug",
66+
"code",
67+
"doc",
68+
"test"
69+
]
5870
}
5971
]
6072
}

Diff for: README.md

+46-20
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
[![downloads][downloads-badge]][npmtrends]
1717
[![MIT License][license-badge]][license]
1818

19-
[![All Contributors](https://img.shields.io/badge/all_contributors-5-orange.svg?style=flat-square)](#contributors)
19+
[![All Contributors](https://img.shields.io/badge/all_contributors-6-orange.svg?style=flat-square)](#contributors)
2020
[![PRs Welcome][prs-badge]][prs]
2121
[![Code of Conduct][coc-badge]][coc]
2222

@@ -86,18 +86,18 @@ test('Fetch makes an API call and displays the greeting when load-greeting is cl
8686
}),
8787
)
8888
const url = '/greeting'
89-
const {queryByTestId, container} = render(<Fetch url={url} />)
89+
const {getByTestId, container} = render(<Fetch url={url} />)
9090

9191
// Act
92-
Simulate.click(queryByTestId('load-greeting'))
92+
Simulate.click(getByTestId('load-greeting'))
9393

9494
// let's wait for our mocked `get` request promise to resolve
9595
await flushPromises()
9696

9797
// Assert
9898
expect(axiosMock.get).toHaveBeenCalledTimes(1)
9999
expect(axiosMock.get).toHaveBeenCalledWith(url)
100-
expect(queryByTestId('greeting-text').textContent).toBe('hello there')
100+
expect(getByTestId('greeting-text').textContent).toBe('hello there')
101101
expect(container.firstChild).toMatchSnapshot()
102102
})
103103
```
@@ -146,18 +146,34 @@ unmount()
146146
// your component has been unmounted and now: container.innerHTML === ''
147147
```
148148

149+
#### `getByTestId`
150+
151+
A shortcut to `` container.querySelector(`[data-testid="${yourId}"]`) `` except
152+
that it will throw an Error if no matching element is found. Read more about
153+
`data-testid`s below.
154+
155+
```javascript
156+
const usernameInputElement = getByTestId('username-input')
157+
usernameInputElement.value = 'new value'
158+
Simulate.change(usernameInputElement)
159+
```
160+
149161
#### `queryByTestId`
150162

151-
A shortcut to `` container.querySelector(`[data-testid="${yourId}"]`) ``. Read
152-
more about `data-testid`s below.
163+
A shortcut to `` container.querySelector(`[data-testid="${yourId}"]`) ``
164+
(Note: just like `querySelector`, this could return null if no matching element
165+
is found, which may lead to harder-to-understand error messages). Read more about
166+
`data-testid`s below.
153167

154168
```javascript
155-
const usernameInputElement = queryByTestId('username-input')
169+
// assert something doesn't exist
170+
// (you couldn't do this with `getByTestId`)
171+
expect(queryByTestId('username-input')).toBeNull()
156172
```
157173

158174
## More on `data-testid`s
159175

160-
The `queryByTestId` utility is referring to the practice of using `data-testid`
176+
The `getByTestId` and `queryByTestId` utilities refer to the practice of using `data-testid`
161177
attributes to identify individual elements in your rendered component. This is
162178
one of the practices this library is intended to encourage.
163179

@@ -186,14 +202,14 @@ prefer to update the props of a rendered component in your test, the easiest
186202
way to do that is:
187203

188204
```javascript
189-
const {container, queryByTestId} = render(<NumberDisplay number={1} />)
190-
expect(queryByTestId('number-display').textContent).toBe('1')
205+
const {container, getByTestId} = render(<NumberDisplay number={1} />)
206+
expect(getByTestId('number-display').textContent).toBe('1')
191207

192208
// re-render the same component with different props
193209
// but pass the same container in the options argument.
194210
// which will cause a re-render of the same instance (normal React behavior).
195211
render(<NumberDisplay number={2} />, {container})
196-
expect(queryByTestId('number-display').textContent).toBe('2')
212+
expect(getByTestId('number-display').textContent).toBe('2')
197213
```
198214

199215
[Open the tests](https://github.com/kentcdodds/react-testing-library/blob/master/src/__tests__/number-display.js)
@@ -219,14 +235,16 @@ jest.mock('react-transition-group', () => {
219235
})
220236

221237
test('you can mock things with jest.mock', () => {
222-
const {queryByTestId} = render(<HiddenMessage initialShow={true} />)
238+
const {getByTestId, queryByTestId} = render(
239+
<HiddenMessage initialShow={true} />,
240+
)
223241
expect(queryByTestId('hidden-message')).toBeTruthy() // we just care it exists
224242
// hide the message
225-
Simulate.click(queryByTestId('toggle-message'))
243+
Simulate.click(getByTestId('toggle-message'))
226244
// in the real world, the CSSTransition component would take some time
227245
// before finishing the animation which would actually hide the message.
228246
// So we've mocked it out for our tests to make it happen instantly
229-
expect(queryByTestId('hidden-message')).toBeFalsy() // we just care it doesn't exist
247+
expect(queryByTestId('hidden-message')).toBeNull() // we just care it doesn't exist
230248
})
231249
```
232250

@@ -247,6 +265,14 @@ something more
247265
Learn more about how Jest mocks work from my blog post:
248266
["But really, what is a JavaScript mock?"](https://blog.kentcdodds.com/but-really-what-is-a-javascript-mock-10d060966f7d)
249267

268+
**What if I want to verify that an element does NOT exist?**
269+
270+
You typically will get access to rendered elements using the `getByTestId` utility. However, that function will throw an error if the element isn't found. If you want to specifically test for the absence of an element, then you should use the `queryByTestId` utility which will return the element if found or `null` if not.
271+
272+
```javascript
273+
expect(queryByTestId('thing-that-does-not-exist')).toBeNull()
274+
```
275+
250276
**I don't want to use `data-testid` attributes for everything. Do I have to?**
251277

252278
Definitely not. That said, a common reason people don't like the `data-testid`
@@ -286,18 +312,18 @@ Or you could include the index or an ID in your attribute:
286312
<li data-testid={`item-${item.id}`}>{item.text}</li>
287313
```
288314

289-
And then you could use the `queryByTestId`:
315+
And then you could use the `getByTestId` utility:
290316

291317
```javascript
292318
const items = [
293319
/* your items */
294320
]
295-
const {queryByTestId} = render(/* your component with the items */)
296-
const thirdItem = queryByTestId(`item-${items[2].id}`)
321+
const {getByTestId} = render(/* your component with the items */)
322+
const thirdItem = getByTestId(`item-${items[2].id}`)
297323
```
298324

299325
**What about enzyme is "bloated with complexity and features" and "encourage poor testing
300-
practices"**
326+
practices"?**
301327

302328
Most of the damaging features have to do with encouraging testing implementation
303329
details. Primarily, these are
@@ -358,8 +384,8 @@ Thanks goes to these people ([emoji key][emojis]):
358384
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
359385

360386
<!-- prettier-ignore -->
361-
| [<img src="https://avatars.githubusercontent.com/u/1500684?v=3" width="100px;"/><br /><sub><b>Kent C. Dodds</b></sub>](https://kentcdodds.com)<br />[💻](https://github.com/kentcdodds/react-testing-library/commits?author=kentcdodds "Code") [📖](https://github.com/kentcdodds/react-testing-library/commits?author=kentcdodds "Documentation") [🚇](#infra-kentcdodds "Infrastructure (Hosting, Build-Tools, etc)") [⚠️](https://github.com/kentcdodds/react-testing-library/commits?author=kentcdodds "Tests") | [<img src="https://avatars1.githubusercontent.com/u/2430381?v=4" width="100px;"/><br /><sub><b>Ryan Castner</b></sub>](http://audiolion.github.io)<br />[📖](https://github.com/kentcdodds/react-testing-library/commits?author=audiolion "Documentation") | [<img src="https://avatars0.githubusercontent.com/u/8008023?v=4" width="100px;"/><br /><sub><b>Daniel Sandiego</b></sub>](https://www.dnlsandiego.com)<br />[💻](https://github.com/kentcdodds/react-testing-library/commits?author=dnlsandiego "Code") | [<img src="https://avatars2.githubusercontent.com/u/12592677?v=4" width="100px;"/><br /><sub><b>Paweł Mikołajczyk</b></sub>](https://github.com/Miklet)<br />[💻](https://github.com/kentcdodds/react-testing-library/commits?author=Miklet "Code") | [<img src="https://avatars3.githubusercontent.com/u/464978?v=4" width="100px;"/><br /><sub><b>Alejandro Ñáñez Ortiz</b></sub>](http://co.linkedin.com/in/alejandronanez/)<br />[📖](https://github.com/kentcdodds/react-testing-library/commits?author=alejandronanez "Documentation") |
362-
| :---: | :---: | :---: | :---: | :---: |
387+
| [<img src="https://avatars.githubusercontent.com/u/1500684?v=3" width="100px;"/><br /><sub><b>Kent C. Dodds</b></sub>](https://kentcdodds.com)<br />[💻](https://github.com/kentcdodds/react-testing-library/commits?author=kentcdodds "Code") [📖](https://github.com/kentcdodds/react-testing-library/commits?author=kentcdodds "Documentation") [🚇](#infra-kentcdodds "Infrastructure (Hosting, Build-Tools, etc)") [⚠️](https://github.com/kentcdodds/react-testing-library/commits?author=kentcdodds "Tests") | [<img src="https://avatars1.githubusercontent.com/u/2430381?v=4" width="100px;"/><br /><sub><b>Ryan Castner</b></sub>](http://audiolion.github.io)<br />[📖](https://github.com/kentcdodds/react-testing-library/commits?author=audiolion "Documentation") | [<img src="https://avatars0.githubusercontent.com/u/8008023?v=4" width="100px;"/><br /><sub><b>Daniel Sandiego</b></sub>](https://www.dnlsandiego.com)<br />[💻](https://github.com/kentcdodds/react-testing-library/commits?author=dnlsandiego "Code") | [<img src="https://avatars2.githubusercontent.com/u/12592677?v=4" width="100px;"/><br /><sub><b>Paweł Mikołajczyk</b></sub>](https://github.com/Miklet)<br />[💻](https://github.com/kentcdodds/react-testing-library/commits?author=Miklet "Code") | [<img src="https://avatars3.githubusercontent.com/u/464978?v=4" width="100px;"/><br /><sub><b>Alejandro Ñáñez Ortiz</b></sub>](http://co.linkedin.com/in/alejandronanez/)<br />[📖](https://github.com/kentcdodds/react-testing-library/commits?author=alejandronanez "Documentation") | [<img src="https://avatars0.githubusercontent.com/u/1402095?v=4" width="100px;"/><br /><sub><b>Matt Parrish</b></sub>](https://github.com/pbomb)<br />[🐛](https://github.com/kentcdodds/react-testing-library/issues?q=author%3Apbomb "Bug reports") [💻](https://github.com/kentcdodds/react-testing-library/commits?author=pbomb "Code") [📖](https://github.com/kentcdodds/react-testing-library/commits?author=pbomb "Documentation") [⚠️](https://github.com/kentcdodds/react-testing-library/commits?author=pbomb "Tests") |
388+
| :---: | :---: | :---: | :---: | :---: | :---: |
363389

364390
<!-- ALL-CONTRIBUTORS-LIST:END -->
365391

Diff for: package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,4 @@
6161
"url": "https://github.com/kentcdodds/react-testing-library/issues"
6262
},
6363
"homepage": "https://github.com/kentcdodds/react-testing-library#readme"
64-
}
64+
}

Diff for: src/__tests__/__snapshots__/element-queries.js.snap

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`getByTestId finds matching element 1`] = `
4+
<span
5+
data-testid="test-component"
6+
/>
7+
`;
8+
9+
exports[`getByTestId throws error when no matching element exists 1`] = `"Unable to find element by [data-testid=\\"unknown-data-testid\\"]"`;
10+
11+
exports[`queryByTestId finds matching element 1`] = `
12+
<span
13+
data-testid="test-component"
14+
/>
15+
`;

Diff for: src/__tests__/element-queries.js

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import React from 'react'
2+
import {render} from '../'
3+
4+
const TestComponent = () => <span data-testid="test-component" />
5+
6+
test('queryByTestId finds matching element', () => {
7+
const {queryByTestId} = render(<TestComponent />)
8+
expect(queryByTestId('test-component')).toMatchSnapshot()
9+
})
10+
11+
test('queryByTestId returns null when no matching element exists', () => {
12+
const {queryByTestId} = render(<TestComponent />)
13+
expect(queryByTestId('unknown-data-testid')).toBeNull()
14+
})
15+
16+
test('getByTestId finds matching element', () => {
17+
const {getByTestId} = render(<TestComponent />)
18+
expect(getByTestId('test-component')).toMatchSnapshot()
19+
})
20+
21+
test('getByTestId throws error when no matching element exists', () => {
22+
const {getByTestId} = render(<TestComponent />)
23+
expect(() =>
24+
getByTestId('unknown-data-testid'),
25+
).toThrowErrorMatchingSnapshot()
26+
})

Diff for: src/__tests__/fetch.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -37,16 +37,16 @@ test('Fetch makes an API call and displays the greeting when load-greeting is cl
3737
}),
3838
)
3939
const url = '/greeting'
40-
const {queryByTestId, container} = render(<Fetch url={url} />)
40+
const {getByTestId, container} = render(<Fetch url={url} />)
4141

4242
// Act
43-
Simulate.click(queryByTestId('load-greeting'))
43+
Simulate.click(getByTestId('load-greeting'))
4444

4545
await flushPromises()
4646

4747
// Assert
4848
expect(axiosMock.get).toHaveBeenCalledTimes(1)
4949
expect(axiosMock.get).toHaveBeenCalledWith(url)
50-
expect(queryByTestId('greeting-text').textContent).toBe('hello there')
50+
expect(getByTestId('greeting-text').textContent).toBe('hello there')
5151
expect(container.firstChild).toMatchSnapshot()
5252
})

Diff for: src/__tests__/mock.react-transition-group.js

+4-2
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,12 @@ jest.mock('react-transition-group', () => {
3939
})
4040

4141
test('you can mock things with jest.mock', () => {
42-
const {queryByTestId} = render(<HiddenMessage initialShow={true} />)
42+
const {getByTestId, queryByTestId} = render(
43+
<HiddenMessage initialShow={true} />,
44+
)
4345
expect(queryByTestId('hidden-message')).toBeTruthy() // we just care it exists
4446
// hide the message
45-
Simulate.click(queryByTestId('toggle-message'))
47+
Simulate.click(getByTestId('toggle-message'))
4648
// in the real world, the CSSTransition component would take some time
4749
// before finishing the animation which would actually hide the message.
4850
// So we've mocked it out for our tests to make it happen instantly

Diff for: src/__tests__/number-display.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@ class NumberDisplay extends React.Component {
1616
}
1717

1818
test('calling render with the same component on the same container does not remount', () => {
19-
const {container, queryByTestId} = render(<NumberDisplay number={1} />)
20-
expect(queryByTestId('number-display').textContent).toBe('1')
19+
const {container, getByTestId} = render(<NumberDisplay number={1} />)
20+
expect(getByTestId('number-display').textContent).toBe('1')
2121

2222
// re-render the same component with different props
2323
// but pass the same container in the options argument.
2424
// which will cause a re-render of the same instance (normal React behavior).
2525
render(<NumberDisplay number={2} />, {container})
26-
expect(queryByTestId('number-display').textContent).toBe('2')
26+
expect(getByTestId('number-display').textContent).toBe('2')
2727

28-
expect(queryByTestId('instance-id').textContent).toBe('1')
28+
expect(getByTestId('instance-id').textContent).toBe('1')
2929
})

Diff for: src/__tests__/react-redux.js

+11-11
Original file line numberDiff line numberDiff line change
@@ -82,27 +82,27 @@ function renderWithRedux(
8282
}
8383

8484
test('can render with redux with defaults', () => {
85-
const {queryByTestId} = renderWithRedux(<ConnectedCounter />)
86-
Simulate.click(queryByTestId('incrementer'))
87-
expect(queryByTestId('count-value').textContent).toBe('1')
85+
const {getByTestId} = renderWithRedux(<ConnectedCounter />)
86+
Simulate.click(getByTestId('incrementer'))
87+
expect(getByTestId('count-value').textContent).toBe('1')
8888
})
8989

9090
test('can render with redux with custom initial state', () => {
91-
const {queryByTestId} = renderWithRedux(<ConnectedCounter />, {
91+
const {getByTestId} = renderWithRedux(<ConnectedCounter />, {
9292
initialState: {count: 3},
9393
})
94-
Simulate.click(queryByTestId('decrementer'))
95-
expect(queryByTestId('count-value').textContent).toBe('2')
94+
Simulate.click(getByTestId('decrementer'))
95+
expect(getByTestId('count-value').textContent).toBe('2')
9696
})
9797

9898
test('can render with redux with custom store', () => {
9999
// this is a silly store that can never be changed
100100
const store = createStore(() => ({count: 1000}))
101-
const {queryByTestId} = renderWithRedux(<ConnectedCounter />, {
101+
const {getByTestId} = renderWithRedux(<ConnectedCounter />, {
102102
store,
103103
})
104-
Simulate.click(queryByTestId('incrementer'))
105-
expect(queryByTestId('count-value').textContent).toBe('1000')
106-
Simulate.click(queryByTestId('decrementer'))
107-
expect(queryByTestId('count-value').textContent).toBe('1000')
104+
Simulate.click(getByTestId('incrementer'))
105+
expect(getByTestId('count-value').textContent).toBe('1000')
106+
Simulate.click(getByTestId('decrementer'))
107+
expect(getByTestId('count-value').textContent).toBe('1000')
108108
})

Diff for: src/__tests__/react-router.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,11 @@ function renderWithRouter(
4949
}
5050

5151
test('full app rendering/navigating', () => {
52-
const {container, queryByTestId} = renderWithRouter(<App />)
52+
const {container, getByTestId} = renderWithRouter(<App />)
5353
// normally I'd use a data-testid, but just wanted to show this is also possible
5454
expect(container.innerHTML).toMatch('You are home')
5555
const leftClick = {button: 0}
56-
Simulate.click(queryByTestId('about-link'), leftClick)
56+
Simulate.click(getByTestId('about-link'), leftClick)
5757
// normally I'd use a data-testid, but just wanted to show this is also possible
5858
expect(container.innerHTML).toMatch('You are on the about page')
5959
})
@@ -68,6 +68,6 @@ test('landing on a bad page', () => {
6868

6969
test('rendering a component that uses withRouter', () => {
7070
const route = '/some-route'
71-
const {queryByTestId} = renderWithRouter(<LocationDisplay />, {route})
72-
expect(queryByTestId('location-display').textContent).toBe(route)
71+
const {getByTestId} = renderWithRouter(<LocationDisplay />, {route})
72+
expect(getByTestId('location-display').textContent).toBe(route)
7373
})

Diff for: src/__tests__/shallow.react-transition-group.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,15 @@ jest.mock('react-transition-group', () => {
3535
})
3636

3737
test('you can mock things with jest.mock', () => {
38-
const {queryByTestId} = render(<HiddenMessage initialShow={true} />)
38+
const {getByTestId} = render(<HiddenMessage initialShow={true} />)
3939
const context = expect.any(Object)
4040
const children = expect.any(Object)
4141
const defaultProps = {children, timeout: 1000, className: 'fade'}
4242
expect(CSSTransition).toHaveBeenCalledWith(
4343
{in: true, ...defaultProps},
4444
context,
4545
)
46-
Simulate.click(queryByTestId('toggle-message'))
46+
Simulate.click(getByTestId('toggle-message'))
4747
expect(CSSTransition).toHaveBeenCalledWith(
4848
{in: true, ...defaultProps},
4949
expect.any(Object),

Diff for: src/__tests__/stopwatch.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ const wait = time => new Promise(resolve => setTimeout(resolve, time))
4343

4444
test('unmounts a component', async () => {
4545
jest.spyOn(console, 'error').mockImplementation(() => {})
46-
const {unmount, queryByTestId, container} = render(<StopWatch />)
47-
Simulate.click(queryByTestId('start-stop-button'))
46+
const {unmount, getByTestId, container} = render(<StopWatch />)
47+
Simulate.click(getByTestId('start-stop-button'))
4848
unmount()
4949
// hey there reader! You don't need to have an assertion like this one
5050
// this is just me making sure that the unmount function works.

Diff for: src/index.js

+12-2
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,26 @@ function select(id) {
77
}
88

99
// we may expose this eventually
10-
function queryDivByTestId(div, id) {
10+
function queryByTestId(div, id) {
1111
return div.querySelector(select(id))
1212
}
1313

14+
// we may expose this eventually
15+
function getByTestId(div, id) {
16+
const el = queryByTestId(div, id)
17+
if (!el) {
18+
throw new Error(`Unable to find element by ${select(id)}`)
19+
}
20+
return el
21+
}
22+
1423
function render(ui, {container = document.createElement('div')} = {}) {
1524
ReactDOM.render(ui, container)
1625
return {
1726
container,
1827
unmount: () => ReactDOM.unmountComponentAtNode(container),
19-
queryByTestId: queryDivByTestId.bind(null, container),
28+
queryByTestId: queryByTestId.bind(null, container),
29+
getByTestId: getByTestId.bind(null, container),
2030
}
2131
}
2232

0 commit comments

Comments
 (0)