Jestとreact-testing-libraryを使ったTypeScript + ReactのテストをCircleCIで回す
ReactのテストをCircleCIで自動化したかったので、ネットで色々調べつつ簡単に環境を構築してみた。
やったこと
- サンプルのテストを作成(jest + react-testing-library + TypeScript)
- CircleCIで自動テスト
- テスト結果をSlackに通知
やった内容
CircleCIの設定は公式のドキュメントを読みつつ、実際に動かすテストについてはこのガイドの通りにやってみた(一部改変したりしてはいる)。
.circleci/config.yml
version: 2.1 jobs: test: docker: - image: circleci/node:10.15.3 steps: - checkout - run: yarn - run: yarn test workflows: version: 2. test: jobs: - test
job1個だけだとworkflowにする意味ってあんまりなさそうだなと思いつつ、workflowにまとめてる。
今回は使ってないが、workflowにはこういうjobの依存関係を決められるようなオプションもあるらしくて便利そうだなと思った。
Slack通知設定
公式ドキュメントのここをみながらすればOK
テストコード
テスト対象のソース
import React from 'react' import { render, fireEvent } from '@testing-library/react' import LoginForm, { Props } from '../LoginForm' const renderLoginForm = (props: Partial<Props> = {}) => { const defaultProps: Props = { onPasswordChange() { return }, onRememberChange() { return }, onUsernameChange() { return }, onSubmit() { return }, shouldRemember: true, } return render(<LoginForm {...defaultProps} {...props} />) } describe('<LoginForm />', () => { test('should display a blank login form, with remember me checked by default', async () => { const { findByTestId } = renderLoginForm() const loginForm = await findByTestId('login-form') expect(loginForm).toHaveFormValues({ username: '', password: '', remember: true, }) }) test('should allow entering a username', async () => { const onUsernameChange = jest.fn() const { findByTestId } = renderLoginForm({ onUsernameChange }) const username = await findByTestId('username') fireEvent.change(username, { target: { value: 'test' } }) expect(onUsernameChange).toHaveBeenCalledWith('test') }) test('should allow entering a password', async () => { const onPasswordChange = jest.fn() const { findByTestId } = renderLoginForm({ onPasswordChange }) const username = await findByTestId('password') fireEvent.change(username, { target: { value: 'test' } }) expect(onPasswordChange).toHaveBeenCalledWith('test') }) test('should allow toggling remember me', async () => { const onRememberChange = jest.fn() const { findByTestId } = renderLoginForm({ onRememberChange, shouldRemember: false, }) const remember = await findByTestId('remember') fireEvent.click(remember) expect(onRememberChange).toHaveBeenCalledWith(true) fireEvent.click(remember) expect(onRememberChange).toHaveBeenLastCalledWith(false) }) test('should submit the form with username, password and remember', async () => { const onSubmit = jest.fn() const { container, getByTestId } = renderLoginForm({ onSubmit, shouldRemember: false, }) // こうすれば、querySelectorを使える const username = container.querySelector('input[name="username"]') as Element const password = await getByTestId('password') const remember = await getByTestId('remember') const submit = await getByTestId('submit') fireEvent.change(username, { target: { value: 'test' } }) fireEvent.change(password, { target: { value: 'password' } }) fireEvent.click(remember) fireEvent.click(submit) expect(onSubmit).toHaveBeenCalledWith('test', 'password', true) }) })
テストコード
import React from 'react' import { render, fireEvent } from '@testing-library/react' import LoginForm, { Props } from '../LoginForm' const renderLoginForm = (props: Partial<Props> = {}) => { const defaultProps: Props = { onPasswordChange() { return }, onRememberChange() { return }, onUsernameChange() { return }, onSubmit() { return }, shouldRemember: true, } return render(<LoginForm {...defaultProps} {...props} />) } describe('<LoginForm />', () => { test('should display a blank login form, with remember me checked by default', async () => { const { findByTestId } = renderLoginForm() const loginForm = await findByTestId('login-form') expect(loginForm).toHaveFormValues({ username: '', password: '', remember: true, }) }) test('should allow entering a username', async () => { const onUsernameChange = jest.fn() const { findByTestId } = renderLoginForm({ onUsernameChange }) const username = await findByTestId('username') fireEvent.change(username, { target: { value: 'test' } }) expect(onUsernameChange).toHaveBeenCalledWith('test') }) test('should allow entering a password', async () => { const onPasswordChange = jest.fn() const { findByTestId } = renderLoginForm({ onPasswordChange }) const username = await findByTestId('password') fireEvent.change(username, { target: { value: 'test' } }) expect(onPasswordChange).toHaveBeenCalledWith('test') }) test('should allow toggling remember me', async () => { const onRememberChange = jest.fn() const { findByTestId } = renderLoginForm({ onRememberChange, shouldRemember: false, }) const remember = await findByTestId('remember') fireEvent.click(remember) expect(onRememberChange).toHaveBeenCalledWith(true) fireEvent.click(remember) expect(onRememberChange).toHaveBeenLastCalledWith(false) }) test('should submit the form with username, password and remember', async () => { const onSubmit = jest.fn() const { container, getByTestId } = renderLoginForm({ onSubmit, shouldRemember: false, }) // こうすれば、querySelectorを使える const username = container.querySelector('input[name="username"]') as Element const password = await getByTestId('password') const remember = await getByTestId('remember') const submit = await getByTestId('submit') fireEvent.change(username, { target: { value: 'test' } }) fireEvent.change(password, { target: { value: 'password' } }) fireEvent.click(remember) fireEvent.click(submit) expect(onSubmit).toHaveBeenCalledWith('test', 'password', true) }) })
このガイドでは、各input
に埋め込んだdata-testid
の値で各input
を特定しているが、個人的にはこれがすごくめんどくさかった。
コード書いてる時にテストのためだけの属性を書くのは絶対忘れる。
なので、代わりにcontainer
を使って、各input
要素のname
属性をquerySelector
で引っ掛けられるようにした。
個人的にはこっちのほうが実装が楽になりそうな気がする。
所感
- config.ymlの書き方はもっといい方法があるかもしれないのでこれから研究
- フリープランでやってるから有料プランではもっと色々できるかもしれない
- 以前は業務でEnzymeを使っていたが、react-testing-libraryも結構いいなあ。