两周前,我写了一个新的图书馆!我已经想了很久了。但两周前我开始相当认真地对待它。
继续阅读,了解我所说的 "破坏性做法 "是什么意思。
简单而完整的React DOM测试工具,鼓励良好的测试实践。
问题是
你想为你的React组件编写可维护的测试。作为这个目标的一部分,你希望你的测试避免包括你的组件的实现细节,而是专注于使你的测试给你的信心,他们的目的是什么。作为这个目标的一部分,你希望你的测试库从长远来看是可维护的,所以你的组件的重构(对实现的改变,但不是功能)不会破坏你的测试,也不会拖累你和你的团队。
这个解决方案
react-testing-library 是一个非常轻量级的解决方案,用于测试React组件。它在react-dom 和react-dom/test-utils 的基础上提供了轻量级的实用功能,以鼓励更好的测试实践。它的主要指导原则是。
因此,与其处理渲染的反应组件实例,你的测试将与实际的DOM节点一起工作。这个库所提供的工具有助于以用户的方式查询DOM。通过标签文本找到表单元素(就像用户那样),通过文本找到链接和按钮(就像用户那样)。它还公开了一种推荐的方法,即通过data-testid ,作为元素的 "逃生舱",在文本内容和标签没有意义或不实用的情况下,可以找到元素。
这个库鼓励你的应用程序更容易访问,并允许你让你的测试更接近于以用户的方式使用你的组件,这使得你的测试给你更多的信心,当一个真正的用户使用它时,你的应用程序将会工作。
这个库是酶的替代品。虽然你可以使用enzyme本身来遵循这些准则,但由于enzyme提供的所有额外的实用程序(方便测试实现细节的实用程序),执行起来比较困难。请在FAQ中阅读更多这方面的内容。
另外,虽然React测试库是为react-dom设计的,但你可以使用React Native测试库,它的API非常相似。
这个库不是什么。
- 一个测试运行器或框架
- 特定的测试框架(尽管我们推荐Jest作为我们的首选,但该库可用于任何框架,甚至可用于codesandbox!)。
实例
基本例子
// hidden-message.js
import * as React from 'react'
// NOTE: React Testing Library works with React Hooks _and_ classes just as well
// and your tests will be the same however you write your components.
function HiddenMessage({children}) {
const [showMessage, setShowMessage] = React.useState(false)
return (
<div>
<label htmlFor="toggle">Show Message</label>
<input
id="toggle"
type="checkbox"
onChange={e => setShowMessage(e.target.checked)}
checked={showMessage}
/>
{showMessage ? children : null}
</div>
)
}
export default HiddenMessage
// __tests__/hidden-message.js
// These imports are something you'd normally configure Jest to import for you automatically.
// Learn more in the setup docs: https://testing-library.com/docs/react-testing-library/setup#skipping-auto-cleanup
import '@testing-library/jest-dom/extend-expect'
// NOTE: jest-dom adds handy assertions to Jest and is recommended, but not required
import {render, screen} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import * as React from 'react'
import HiddenMessage from '../hidden-message'
test('shows the children when the checkbox is checked', () => {
const testMessage = 'Test Message'
render(<HiddenMessage>{testMessage}</HiddenMessage>)
// query* functions will return the element or null if it cannot be found
// get* functions will return the element or throw an error if it cannot be found
expect(screen.queryByText(testMessage)).toBeNull()
// the queries can accept a regex to make your selectors more resilient to content tweaks and changes.
userEvent.click(screen.getByLabelText(/show/i))
// .toBeInTheDocument() is an assertion that comes from jest-dom
// otherwise you could use .toBeDefined()
expect(screen.getByText(testMessage)).toBeInTheDocument()
})
实例
// login.js
import * as React from 'react'
function Login() {
const [state, setState] = React.useReducer((s, a) => ({...s, ...a}), {
resolved: false,
loading: false,
error: null,
})
function handleSubmit(event) {
event.preventDefault()
const {usernameInput, passwordInput} = event.target.elements
setState({loading: true, resolved: false, error: null})
window
.fetch('/api/login', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
username: usernameInput.value,
password: passwordInput.value,
}),
})
.then(r => r.json())
.then(
user => {
setState({loading: false, resolved: true, error: null})
window.localStorage.setItem('token', user.token)
},
error => {
setState({loading: false, resolved: false, error: error.message})
},
)
}
return (
<div>
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="usernameInput">Username</label>
<input id="usernameInput" />
</div>
<div>
<label htmlFor="passwordInput">Password</label>
<input id="passwordInput" type="password" />
</div>
<button type="submit">Submit{state.loading ? '...' : null}</button>
</form>
{state.error ? <div role="alert">{state.error.message}</div> : null}
{state.resolved ? (
<div role="alert">Congrats! You're signed in!</div>
) : null}
</div>
)
}
export default Login
// __tests__/login.js
// again, these first two imports are something you'd normally handle in
// your testing framework configuration rather than importing them in every file.
import '@testing-library/jest-dom/extend-expect'
import {render, screen} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import * as React from 'react'
import Login from '../login'
test('allows the user to login successfully', async () => {
// mock out window.fetch for the test
const fakeUserResponse = {token: 'fake_user_token'}
jest.spyOn(window, 'fetch').mockImplementationOnce(() => {
return Promise.resolve({
json: () => Promise.resolve(fakeUserResponse),
})
})
render(<Login />)
// fill out the form
userEvent.type(screen.getByLabelText(/username/i), 'chuck')
userEvent.type(screen.getByLabelText(/password/i), 'norris')
userEvent.click(screen.getByText(/submit/i))
// just like a manual tester, we'll instruct our test to wait for the alert
// to show up before continuing with our assertions.
const alert = await screen.findByRole('alert')
// .toHaveTextContent() comes from jest-dom's assertions
// otherwise you could use expect(alert.textContent).toMatch(/congrats/i)
// but jest-dom will give you better error messages which is why it's recommended
expect(alert).toHaveTextContent(/congrats/i)
expect(window.localStorage.getItem('token')).toEqual(fakeUserResponse.token)
})
这个例子中最重要的收获是。
测试是以类似于用户如何使用你的应用程序的方式来编写的。
让我们进一步探讨这个问题...
假设我们有一个GreetingFetcher 组件,为用户获取问候语。它可能会呈现一些像这样的HTML。
<div>
<label for="name-input">Name</label>
<input id="name-input" />
<button>Load Greeting</button>
<div data-testid="greeting-text" />
</div>
所以功能是。设置名称,点击 "加载问候语 "按钮,服务器就会请求加载带有该名称的问候语文本。
在你的测试中,你需要找到<input /> ,这样你就可以把它的value 到一些东西。传统的观点认为你可以在一个CSS选择器中使用id 属性:#name-input 。但用户会这样做来找到这个输入吗?他们会看着屏幕,找到标签为 "姓名 "的输入,然后填入。所以这就是我们的测试对getByLabelText 。它根据标签来获得表单控件。
通常在使用酶的测试中,为了找到 "Load Greeting "按钮,你可能会使用一个CSS选择器,甚至通过组件displayName 或组件构造器来寻找。但是当用户想加载问候语时,他们并不关心这些实现细节,相反,他们会找到并点击写着 "加载问候语 "的按钮。而这正是我们的测试用getByText 帮助器所做的事情!
此外,wait ,与用户所做的完全相似。他们等待问候文本的出现,无论需要多长时间。在我们的测试中,我们正在模拟,所以它基本上是即时发生的,但我们的测试实际上并不关心它需要多长时间。我们不需要在测试中使用setTimeout 或任何东西。 我们只是说。"嘿,等到greeting-text 节点出现。"(注意,在这种情况下,它使用了一个data-testid 属性,这是一个逃生舱门,用于通过任何其他机制找到一个元素都没有意义的情况。一个data-testid 肯定比其他方法更好。
高级别的概述API
最初,该库只提供了queryByTestId ,作为我的博文"让你的UI测试对变化有弹性"中的建议。但由于Bergé Greg对该博文的反馈,以及Jamie White精彩(简短!)的演讲的启发,我又增加了几个,现在我对这个解决方案更加满意了。
你可以在官方文档中阅读更多关于这个库和它的API。 下面是这个库给你带来的高层次概述:
Simulate:从Simulate实用程序中重新导出,从react-dom/test-utilsSimulate对象中重新导出。wait: 允许你在测试中等待一段非确定的时间。 通常你应该模拟出API请求或动画,但即使你处理的是立即解决的承诺,你也需要你的测试来等待事件循环的下一次勾选,而wait在这方面真的很好。(要感谢ŁukaszGozda Gandecki,他将其作为(现已废弃的)flushPromisesAPI的替代品)。render: 这是该库的核心部分。它相当简单。它用document.createElement来创建一个div,然后用ReactDOM.render来渲染到那个div。
render 函数返回以下对象和实用程序:
container: 你的组件被渲染到的div。unmount: 在ReactDOM.unmountComponentAtNode上的一个简单的包装器,用来卸载你的组件(例如,为了方便测试componentWillUnmount)。getByLabelText: 获取一个与标签相关的表单控件getByPlaceholderText: 占位符不是标签的适当替代品,但如果这对你的用例更有意义,它是可用的。getByText: 通过文本内容获取任何元素。getByAltText: 通过它的alt属性值来获取一个元素(如<img)。getByTestId: 通过它的data-testid属性来获取一个元素。
如果找不到任何元素,每个get* 工具都会抛出一个有用的错误信息。还有一个相关的query* API,它将返回null,而不是抛出一个错误,这对断言一个元素不在DOM中很有用。
另外,对于这些get* ,要找到一个匹配的元素,你可以传递:
- 一个不区分大小写的子串:
lo world匹配Hello World - 一个反义词:
/^Hello World$/匹配Hello World - 一个接受文本和元素的函数:
(text, el) => el.tagName === 'SPAN' && text.startsWith('Hello')将会匹配一个内容以字母开头的跨段。Hello
自定义Jest匹配器
感谢Anto Aravinth Belgin Rayen,我们也有一些方便的自定义Jest匹配器:
toBeInTheDOM断定一个元素是否存在于DOM中。toHaveTextContent:检查给定的元素是否有文本内容。
注意:现在这些已经被提取到jest-dom,由Ernesto García维护。
结论
这个库的一大特点是,它没有测试实现细节的工具。它专注于提供鼓励良好测试和软件实践的实用程序。我希望,通过使用react-testing-library你的React测试库会更容易理解和维护。