UdemyのReact講座に学習が完了したので、この勢いでテストコードの書き方を学
ぶことにするよ。
今回は、こちらのReactテストコードのworkshopの視聴した内容から学んだことを書いていきます。
https://www.udemy.com/react-redux/learn/v4/overview
workshopで使ったリポジトリは以下になります。
github.com
テストコードを書く際に使うパッケージ
Vue.jsのときもそうだったんだけれども、テストコードを書くにあたっては、いろんな観点・手法が存在するみたい。
今回は、以下のパッケージをつかっていくよ!
説明はとてもてきとうなので、使用例だったり、公式ドキュメントだったり、動画をみてもらったほうがいいかも。
- jest: javascriptのテストを行うための、テストフレームワーク
- jest-dom:jestのマッチャ関数をDOMの検証用に便利に拡張したもの
- dom-testing-library:ユーザー目線でDOMを検索・取得できるライブラリ
- react-testing-library:dom-testing-libraryをReact用に特化したライブラリ
いろいろ調べると、Reactのテストでは、enzymeという子コンポーネントをモックっぽい感じで使えるツールがよく使われているみたいなんだけど、今回は使われていない。
講師のKent C. Doddsさんが以下の記事で理由を語ってくれているので、どこかでがんばってよんでみる。
blog.kentcdodds.com
環境
さきほどのリポジトリをcloneして、そのままやるのが一番はやい。
なんだけど、自分の勉強もかねてテスト環境は、create-react-app
でつくることにするよ。
とはいえcreate-react-app
でつくってもデフォルトでjest
が使えるのでとくにすることはない。
react-testing-library
とjest-dom
のみあとから追加しよう。
かんきょうじゅんび
$ create-react-app sample-test
$ cd sample-test
$ yarn add react-testing-library jest-dom
こんな感じになりました。
package.json
"dependencies": {
"jest-dom": "^3.0.0",
"react": "^16.7.0",
"react-dom": "^16.7.0",
"react-scripts": "2.1.3",
"react-testing-library": "^5.4.4"
},
※devDependenciesにいれればよかった。
準備
src配下に__tests__
ディレクトリを作成し、その配下にsimple-test.js
を作成する。
jestでは、__tests__
ディレクトリ配下のファイルがテストコードと認識される。
※他にも条件はあるみたいなんだけど割愛するよ!
.
├── README.md
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ └── manifest.json
├── src
│ └── __tests__
│ └── simple-test.js
└── yarn.lock
また、テストコードをどこにおくかについては、同じく以下の記事に言及されてました。
blog.kentcdodds.com
__tests__
よりコンポーネントと同じ階層で運用したほうががわかりやすいのかな?
とりあえずテストを実行してみる
テスト対象がそもそもないんだけど、jestがどんなものか確認するために、こんな感じのテストを書いてみます。
simple-tets.js
test('Basic javascript', () => {
console.log('Hello Test')
const sum = 1+ 1
expect(sum).toBe(2)
})
expect
だったりマッチャ関数であるtoBe()
等については公式ドキュメントを読んでみよう。
jestjs.io
テストを書いたら、実行してみます。
テストを実行
$ yarn run test
✓ Basic javascript (14ms)
console.log src/__tests__/simple-test.js:2
Hello Test
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1.511s
Ran all test suites related to changed files.
Watch Usage
› Press a to run all tests.
› Press f to run only failed tests.
› Press p to filter by a filename regex pattern.
› Press t to filter by a test name regex pattern.
› Press q to quit watch mode.
› Press Enter to trigger a test run.
create-react-app
のテストスクリプトは、デフォルトwatchモードで起動するみたいです。
上記のように、一度テストが実行されたあとに、watchモードのコマンドを叩くことができます。
テスト対象だけをファイル名で絞ったり等できるのですが、テストファイルもひとつしかないので、そのまま使うことにします。
ためしに、テストコードを変更してみると再度テストが走りますね。
テスト対象のコンポーネントを書く、その前に
コンポーネントのテストを書く前に、どうやってテストをするのかを、Reactを使わないで、純粋にDOMを操作して確認してみます。
ということで、テストコードをこんな感じにしてみます。
simple-tets.js
test('Basic javascript', () => {
const div = document.createElement('div')
const p = document.createElement('p')
div.appendChild(p)
const content = document.createTextNode('Hello')
p.appendChild(content)
console.log(div.outerHTML)
})
上記のconsole.log
の結果は以下になります。
出力結果
<div><label>Hello</label><input type="number"></div>
みなれたHTMLが出力されてます。
これに対して、input
タグのtypeがnumberであること、label
タグのテキストがHello
であることテストとして表現するとこんな感じになります。
simple-tets.js
test('Basic javascript', () => {
const div = document.createElement('div')
const p = document.createElement('p')
div.appendChild(p)
const content = document.createTextNode('Hello')
p.appendChild(content)
expect(div.querySelector('input').type).toBe('number')
expect(div.querySelector('label').textContent).toBe('Hello')
})
当たり前ですが上記テストは無事パスします。
これだけだとシンプルすぎて、一体何をテストしたんだろう、と思ってしまいますが、少なくともDOMを通して、想定したHTMLがレンダリングされるだろうということが確認できたよう気がします。
コンポーネントをテストする
早速、テスト対象のコンポーネントを書いていきます。
ろくに機能がありませんが、こんな感じにしました。
Form.js
import React from 'react'
class Form extends React.Component {
render() {
return (
<form>
<label htmlFor="name">Name</label>
<input id="name" type="text" name="name"/>
<label htmlFor="age">Age</label>
<input id="age" type="number" name="age"/>
<button>Submit</button>
</form>
)
}
}
export default Form
作ったコンポーネントを、テストコードでimportするようにします。
expect
を書く前に、Reactにより出力されるDOMがどんな感じか見てみます。
simple-tets.js
import React from 'react'
import ReactDOM from 'react-dom'
import Form from '../components/Form'
test('Form Test', () => {
const div = document.createElement('div')
ReactDOM.render(<Form />, div)
console.log(div.outerHTML)
})
上記のconsole.log
の結果は以下になります。
出力結果
console.log src/__tests__/simple-test.js:8
<div><form><label for="name">Name</label><input id="name" type="text" name="name"><label for="age">Age</label><input id="age" type="number" name="age"><button>Submit</button></form></div>
わかってる方には当然と思われるかもしれませんが、個人的に衝撃でした。
DOMが!できている!
ということは、Reactを使わないでやったときとおなじようなことができてしまいます。
simple-tets.js
import React from 'react'
import ReactDOM from 'react-dom'
import Form from '../components/Form'
test('Form Test', () => {
const div = document.createElement('div')
ReactDOM.render(<Form />, div)
expect(div.querySelector('label').textContent).toBe('Name')
expect(div.querySelector('input').type).toBe('text')
})
おおーんすげえ!
本格的なテストに入る前に
さて、ここからは冒頭で紹介した以下のライブラリを使ってみよう。
- jest-dom:jestのマッチャ関数をDOMの検証用に便利に拡張したもの
- dom-testing-library:ユーザー目線でDOMを検索・取得できるライブラリ
- react-testing-library:dom-testing-libraryをReact用に特化したライブラリ
jest-dom
さきほどのテストコードをさくっとjest-domを使って置き換えて見ます。
simple-tets.js
import React from 'react'
import ReactDOM from 'react-dom'
import 'jest-dom/extend-expect'
import Form from '../components/Form'
test('Form Test', () => {
const div = document.createElement('div')
ReactDOM.render(<Form />, div)
expect(div.querySelector('label')).toHaveTextContent('Name')
expect(div.querySelector('input')).toHaveAttribute('type', 'text')
})
今までは、DOMに対象のテキストが存在かなどを確認する際は、DOMのプロパティを意識する必要がありましたが、置き換え後は、その必要がなくなりましたね。
上記以外にも便利そうなものがいっぱいあるので、公式を参照してみてください。
github.com
dom-testing-library
こちらも、まずは置き換え後のコードを先に貼ります。
simple-tets.js
import React from 'react'
import ReactDOM from 'react-dom'
import 'jest-dom/extend-expect'
import { getQueriesForElement } from 'dom-testing-library'
import Form from '../components/Form'
test('Form Test', () => {
const div = document.createElement('div')
ReactDOM.render(<Form />, div)
const { getByLabelText } = getQueriesForElement(div)
const input = getByLabelText('Name')
expect(input).toHaveAttribute('type', 'text')
})
お?ってなりませんか。
input
のノードを取得する際に、input
そのものを指定するのではなく、それに紐づくlabel
を指定してます。
さらにいえば、そのラベルを取得するのも、label
のテキストを指定しています。
なぜこんな取得の仕方をしているんでしょうか。
こちらですが、今回の講師兼、開発者であるKent C. Doddsさんのフロントエンドのテストに対する考え方が反映されています。
ここで綺麗にまとめることができれば、いいのですが自分もまだすっきりとしていないのですが、以下の記事が大変参考になります。
qiita.com
雑に書くと、
フロントエンドは単体テストよりも、統合テストのほうがコストパフォーマンスもいいよね。ユーザーが意識しない内部の修正を行ったことにより、テストが通らなくなるとか、テストコードをメンテして行くだけで疲弊しちゃうよ。
ユーザー目線のテストを多くしていこうよ。
みたいな感じでしょうか。
上記を受けてこちらのinput
ノードの取得の方法を見ると、なんとなくですが「ユーザーはinput
タグそのものを認識しているわけではなく、input
に用意されているlabel
を見てる」みたいなものが伝わってくる気がします。
simple-tets.js(抜粋)
const input = getByLabelText('Name')
expect(input).toHaveAttribute('type', 'text')
})
label
から取得する以外にも、テキストから検索するgetByText
だったり、コンポーネントの要素にdata-testid
属性を付与しておいて、それをもとに取得するgetByTestId
だったりがあります。
data-testid
で取得と聞くと、もはやclassやid属性で取得すればいいのではと思ったりもしたのですが、クラス名を変えるとテストぶっこわれるでしょ!というところでしょうか。
react-testing-library
最後にreact-testing-library
になります。
こちらはdom-testing-library
をラップしてReact用に特化したものなので、importする際は、react-testing-library
をimportするように切り替えます。
以下は、置き換えたあとのコードになります。
simple-tets.js
import React from 'react'
import 'jest-dom/extend-expect'
import { render } from 'react-testing-library'
import Form from '../components/Form'
test('Form Test', () => {
const { getByLabelText } = render(<Form />)
const input = getByLabelText('Name')
expect(input).toHaveAttribute('type', 'text')
})
上記では、div
をつくってReactDOM.reder
をして〜の流れがなくなり、さきほどりも、すっきりしましたね。
上記ではrender
のみ使っていますが、テスト後にコンポーネントをunmoutするcleanup
だったり、debug用関数があったりします。
テストを行う 長くなったので次回
イベントのテストだったり、肝心な部分を書こうとおもったのですが、長くなったので次回にします。