先日、React/Redux-sagaを使用しているプロジェクトに一部テストを導入しました。
そこで色々な知見を得たので、揮発する前にまとめておきます。
※今回E2Eテストは未導入なので本記事では扱っていません。
結論
現状テストは以下の形で行うことにしました。
・reducerのテスト -> 初回はjestで手書きテスト、その後 reducer-tester
, Jest
のsnapshotを利用
・redux-sagaのテスト -> redux-saga-test-plan
・コンポーネントのテスト -> E2Eテストで拾えきれない部分を Enzyme
, React Testing Library
状況に合わせて利用
基本的な部分はJestで決まり
まず使用率/満足率でげきつよです。
https://2019.stateofjs.com/testing/jest/
https://www.npmtrends.com/jest-vs-mocha-vs-jasmine
これだけでJestで決定してもいいような勢いですが、念の為
何が優れているのかの調査を行いました。
| テストフレームワーク | 概要 | テストダブル | アサーション | その他 |
|:-:|:-:|:-:|:-:|:-:|:-:|
| Jest | facebookが作成しており、reactに対応している
スナップショットテストが入っている(!)
カバレッジがコマンド一発で使える
create-react-app に組み込まれている(!) | 🙆♀️ | 🙆♀️ | | |
| mocha | Jestより古く資料が沢山ある
TSとの相性がいい? | Sinon.jsを使う
(スパイはある) | 🙆♀️ | | |
| jasmine | | 🙆♀️ | | | |
| AVA | 下火すぎて採用するにはちょっと... | | | | |
| Cypress | E2Eテストフレームワークだよ | | | | |
| Storybook | コンポーネントのカタログだよ | | | | |
調べていくうちに Jest
だな...と思って調査が中途半端になったところがあるので、若干調べが甘いかもしれません。
使い勝手の良さから、Jestの採用される要因がよくわかりました。create-react-app
にも採用されていることが信頼を後押ししました。
reducerのテスト
reducerは純粋関数ですし、扱っているプロジェクトでは分岐等も持たせていないので正直テストの優先順位は低いと思いましたが、
後学の為、reducer単体のテストをするならどの方法がいいかを考えました。
先ほどの比較表でも紹介していましたが、Jest
にはスナップショットという強力な機能があります。
https://jestjs.io/docs/ja/snapshot-testing
これは、指定していた部分の前回分の結果を記録して、次回テスト時と差異がないかのチェックをしてくれます。
reducerのような純粋関数をコピペしてテストしていくのは冗長な記述になってしまうので、これはかなり画期的に思いました。
// Updated test case with a Link to a different address
it('renders correctly', () => {
const tree = renderer
.create(<Link page="http://www.instagram.com">Instagram</Link>)
.toJSON();
expect(tree).toMatchSnapshot();
});
reducerのテストには、上記のスナップショットをいい感じに設定してくれる便利な
reducer-tester
というライブラリを使用しました。
reducer本体と初期state,actionを指定するだけなので記述量が相当少なく済み、いい感じです。
import reducerTester from 'reducer-tester'
import { initButton, pushBranchBtnData } from '../../src/jsx/reducers/requires/bankReducers'
import { pushBranchBtn } from '../../src/jsx/actions/common'
/**
* reducerのテスト
*/
// bank
describe('ミドルウェアのないreducerのテスト', () => {
reducerTester({
reducer: pushBranchBtnData,
state: initButton,
tests: [
pushBranchBtn('hira', 'あ'),
],
})
スナップショットの是非
強力なJestのsnapshot機能ですが、いくつかの短所があります。
・テストの意図がわかりづらく、結果修正が難しくなる
・一度目に登録する値がそもそも正しいという保証がない
なので、スナップショットを利用する際は、スナップショットを巨大にせず、焦点を絞ってテストすることが重要になります。
また、今回reducerのテストでは、1度目の結果を保証する為、そして確認の意味も兼ねてJestでベタでテストを行い、それに合格したら react-tester
に切り替えるという手法をとりました。
この辺もっといい方法がないかな、とぼんやり思っています。
redux-sagaのテスト
今回メインでテストを入れたかった部分です。
redux-sagaも多くのライブラリがあり、選定に迷いましたが、書きやすさとシェア率を鑑みて redux-saga-test-plan
を採用しました。
testSaga
を利用すれば、generatorの走った順番までテストすることができ、
expectSaga
を利用すれば、generatorが何を発行したのかのみに焦点を当てて(つまり順番を無視して)テストができます。
基本的にはexpectSagaを利用することで壊れにくいテストを書くことができ、 発火順などが重要になる場合はtestSagaを使用するでいいと思います。
コンポーネントのテスト
今回コンポーネントのテストは導入していませんが、今後加えるのであれば
・Enzyme
・React Testing Library
のどちらかだろうと思います。
それぞれの違いについて調べたところ、共通で見かけられる見解は、
Enzyme
・・・TDD寄りの設計思考
React Testing Library
・・・BDD寄りの設計思考
であるということでした。
【参考資料】
Enzyme vs React Testing Library結局どっちがいいのか問題に対する個人的な回答
Enzyme vs. react-testing-library: A mindset shift
つまりE2Eを導入するのであれば、それとの兼ね合いでも決定すべきライブラリが変わるし、
実際にコンポーネントの何をテストしたいかによっても使用するライブラリが変わるかと思います。
コンポーネントに関しては、TSである程度きつめに型を縛っておけば、あとはE2Eテストでカバーで良いのではと個人的には思っています。
(なぜなら一番変化が多い部分で、テストメンテナンスが大変な為)
複雑なコンポーネントが増えてきたり、ライブラリを作成する場合にはE2Eテストとのバランスを加味しながら、両者ライブラリを試していこうと思います。
今後の課題
・sagaとreducerの結合テストも書いた方が良さそう
・redux-thunkのテストも試してみないといけない
・actionのテストも導入したい
恥ずかしながらテストに関してはど素人であったので、間違った見解等あればコメントで指摘いただけると嬉しいです🙇♂️
その他用語や印象に残った文
###テスト用語集
テスト駆動開発(TDD)・・・ 先にテストをかき、後から実装する開発手法
振る舞い駆動開発(BDD)・・・ 振る舞いの記述に特化したテスト。
モック関数・・・ 「自分が何回呼ばれたか」「自分にどんな引数が渡されたか」などを記憶する関数、値を返さない時便利
【ホワイトボックス/ブラックボックス】
ホワイト・・・中身を全て把握して行うテスト。質は高いが大変
ブラック・・・実装の中身は見ないテスト。テストは楽だが潜在的なバグを見落とす
カバレッジ・・・ テストがどれだけソースコードの中を網羅したかを示す値
テストダブル・・・ ソフトウェアテストにおいて、テスト対象が依存しているコンポーネントを置き換える代用品のこと
スタブ・・・ テストダブルの一種。テスト対象が依存コンポーネントを呼び出したときに取得できる値を用意する
モック・・・ テストダブルの一種。テスト対象が依存コンポーネントを呼び出したときに与えた値を記録する。
スパイ・・・ テストダブルの一種。モック+スタブのこと
フェイク・・・ テストダブルの一種。実際のコンポーネント同様の動作をする軽量化されたコンポーネント(仮のデータベースなど)
ダミー・・・ テストダブルの一種。テストするメソッドには関係ないが、コンパイルを通すために必要なコンポーネント
アサーションライブラリ・・・ テスト結果を見やすく出力することに特化したライブラリ。power-assert.jsやChaiなど
ヘッドレスブラウザ・・・ 画面が描画されないテスト用のブラウザ、PhantomJSなどが有名
エンバグ・・・ 作っているプログラムにバグを埋め込んじゃうこと
リグレッションテスト・・・ ソフトウェアテストの一つで、プログラムの一部を変更・修正した際に、その変更によって予想外の影響が現れていないか確かめるもの。
印象に残った言葉
・テストは人間が記述するものなので、やはりどうしても限界は存在します。テストケースを作成する人間が思いつかなかった入力に対しては、単体テストは無力
・あらかじめテストを意識した設計をしておくと、単体テストがスムーズにいきます
・E2Eテストの前に単体テストや、統合テストを行うことで、比較的解決しやすい段階で問題を見つけることができます。そして、E2Eテストでは、初めに重要なスモークテストを行い、次にサニティチェックと他のハイリスクなテストケースを実行します。
ブラウザ環境でIntegration Testを正しく実装していれば、わざわざSeleniumを使ってend to endのテストを実装する必要性は低い。なぜSeleniumを使う必要があるのか?ということを考えると自ずと答えは見えてくると思う。
ユニットテストが書きづらい場合は、設計の悪さを示している。
バックエンド方面では成熟した設計論や開発論が、フロントエンド界隈ではまだ未熟ではないでしょうか? =>心にくる
実世界のほとんどの例では、依存しているコンポーネントのモック関数を見つけ出して構成することが必要となりますが、テクニック自体は一緒です。 こうしたテストを書く場合は、関数の内の直接テストされていないロジックを実装したくなる誘惑を避けるように努めましょう。
Good tests encode the developer's intention, they don't only lock in the test's behavior without editorialization of what's important and why.
その他参考資料
JavaScriptでも単体テストを導入しよう!ってかテストって何?
テストダブルをなんとなく理解する
JavaScriptテスト自動化キホンのキ
ついにjestの軍門に降った
Jest vs Mocha: Which Should You Choose?
フロントエンドのテストに真面目に向き合う
プログラミングにおけるテスト
テスト駆動開発の進め方
Jestでテストを書こう!
公式config
この頃流行りのJestを導入しよう!
itとtestの違いについての質問
公式様の入門
どのようにreduxのreducerをテストするか
enzyme公式
reactのテストをどう書くべきか
react redux テスト考察