JavaScript
React
TypeScript
public
テスト
React のテスト
React Testing Library とは
React Testing Library | Testing Library
React のテストは一般的には Jest テストフレームワークの中で React Testing Library というテストライブラリを使ってコンポーネントのテストを行うらしい。
create-react-appでプロジェクトを作成している場合、パッケージのインストールは不要。
試してみる
スクリプトを用意する
好みでスクリプトを編集しておく。
package.json {
"scripts" : {
"test_bak" : "react-scripts test" ,
"test" : "react-scripts test --watchAll=false" ,
"test:cov" : "react-scripts test --coverage --watchAll=false"
}
}
実行する
npm run testしてみると以下の警告が出る。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 PASS src/App.test.tsx
● Console
console.error
Warning: ` ReactDOMTestUtils.act` is deprecated in favor of ` React.act` . Import ` act` from ` react` instead of ` react-dom/test-utils` . See https://react.dev/warnings/react-dom-test-utils for more info.
4 |
5 | test( 'renders learn react link' , () = > {
> 6 | render( <App />) ;
| ^
7 | const linkElement = screen.getByText( /learn react/i) ;
8 | expect( linkElement) .toBeInTheDocument() ;
9 | }) ;
at printWarning ( node_modules/react-dom/cjs/react-dom-test-utils.development.js:71:30)
at error ( node_modules/react-dom/cjs/react-dom-test-utils.development.js:45:7)
at actWithWarning ( node_modules/react-dom/cjs/react-dom-test-utils.development.js:1736:7)
at node_modules/@testing-library/react/dist/act-compat.js:63:25
at renderRoot ( node_modules/@testing-library/react/dist/pure.js:159:26)
at render ( node_modules/@testing-library/react/dist/pure.js:246:10)
at Object.<anonymous> ( src/App.test.tsx:6:9)
at TestScheduler.scheduleTests ( node_modules/@jest/core/build/TestScheduler.js:333:13)
at runJest ( node_modules/@jest/core/build/runJest.js:404:19)
at _run10000 ( node_modules/@jest/core/build/cli/index.js:320:7)
at runCLI ( node_modules/@jest/core/build/cli/index.js:173:3)
@testing-library/reactのパッケージを更新したら良いらしい。(ncu、ncu -uを使うことにする)
テスト対象のコンポーネントを作成する
src/Button.tsx import React from "react" ;
interface ButtonProps {
onClick : () => void ;
children : React.ReactNode ;
}
export function Button ( props : ButtonProps ) {
return < button onClick = { props . onClick }>{ props . children }</ button >;
}
テストコードを書く
src/Button.test.tsx 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 import { fireEvent , render , screen } from "@testing-library/react" ;
import { Button } from "./Button" ;
test ( "Buttonのイベント発火を確認する" , () => {
// イベントのモック関数
const handleClick = jest . fn (). mockImplementation (() => {
console . log ( "Mock called" );
});
render (< Button onClick = { handleClick }> Click Me </ Button >);
// テキストでHTML要素を取得する
const element = screen . getByText ( /Click Me/i );
// 要素がドキュメント上に存在するか確認する
expect ( element ). toBeInTheDocument ();
// クリックする
fireEvent . click ( element );
// イベントが呼ばれたか確認する
expect ( handleClick ). toHaveBeenCalled ();
});
テストしてみる
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29 $ npm run test:cov
> react-test-example2@0.1.0 test:cov
> react-scripts test --coverage --watchAll= false
PASS src/Button.test.tsx
● Console
console.log
Mock called
at src/Button.test.tsx:7:13
PASS src/App.test.tsx
--------------------| ---------| ----------| ---------| ---------| -------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
--------------------| ---------| ----------| ---------| ---------| -------------------
All files | 15 .38 | 0 | 50 | 15 .38 |
App.tsx | 100 | 100 | 100 | 100 |
Button.tsx | 100 | 100 | 100 | 100 |
index.tsx | 0 | 100 | 100 | 0 | 7 -19
reportWebVitals.ts | 0 | 0 | 0 | 0 | 3 -10
--------------------| ---------| ----------| ---------| ---------| -------------------
Test Suites: 2 passed, 2 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 1 .429 s
Ran all test suites.
網羅率も確認できることがわかった。
状態を確認する
useState()を使ったコンポーネントをテストしてみる。React Testing Library ではuseState()の中身にアクセスする方法を提供していないらしい。理由は React のテストがコンポーネントの出力を検証することに焦点を当てるためらしい。
状態を持つコンポーネントを作成する。
src/Counter.tsx import { useState } from "react" ;
export function Counter () {
const [ count , setCount ] = useState ( 0 );
function handleClick () {
setCount (( c ) => c + 1 );
}
return < button onClick = { handleClick }>{ count }</ button >;
}
src/Counter.test.tsx 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24 import { fireEvent , render , screen } from "@testing-library/react" ;
import { Counter } from "./Counter" ;
test ( "Counterの動作を確認する" , () => {
render (< Counter />);
// テキストでHTML要素を取得する
const element = screen . getByText ( /0/i );
// 要素がドキュメント上に存在するか確認する
expect ( element ). toBeInTheDocument ();
// クリックする
fireEvent . click ( element );
// 状態が変わったか確認する
expect ( screen . getByText ( /1/i )). toBeInTheDocument ();
// クリックする
fireEvent . click ( element );
// 状態が変わったか確認する
expect ( screen . getByText ( /2/i )). toBeInTheDocument ();
});
テスト用に環境変数を差し替える
以下のようにテスト実行前にdotenv等で環境変数をセットするようなスクリプトを組めば良いと思う。(動作未確認)
{
"scripts" : {
"test" : "dotenv -e .env.test -- react-scripts test --watchAll=false"
}
}
fetch()とか外界をモックする
fetch()についてはモックサーバーを用意する方法か、fetch()をモック関数に置き換える方法が考えられる。
モックサーバーを用意するのは面倒だ。テストコードを実行する前に、サーバーを起動しないといけない。
テストコードでfetch()のモックはしない想定で考える。理由は以下である。
複数のコンポーネントで URL の異なる fetch()を呼んでいるとき、どの fetch()をどのモックにつなげるかをうまく実装する必要があるが、これが難しそうである。
コンポーネントが 1 つであったとしても、URL の異なる fetch()を呼ぶなら同様の問題がある。
そもそもコンポーネントが HTTP 通信という具象に依存しているのは設計の点でイケてない。外部とのやり取りは専用のモジュールなり関数なりに任せる設計になるはず。
そこで、実装は以下を想定する。
src/fetch-data.ts export function fetchData () : Promise < Response > {
return fetch ( "http://localhost:8080" );
}
src/Request.tsx 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31 import { useEffect , useState } from "react" ;
import { fetchData } from "./fetch-data" ;
export interface Data {
message : string ;
}
export function Request () {
const [ data , setData ] = useState < Data | null >( null );
useEffect (() => {
( async () => {
try {
const response = await fetchData ();
if ( ! response . ok ) {
setData ({ message : "ステータスコードが200番台じゃない" });
}
const body = ( await response . json ()) as Data ;
setData ( body );
} catch ( error ) {
setData ({ message : "サーバーに繋がらない" });
}
})();
}, []);
if ( ! data ) {
return < div > 読み込み中 </ div >;
}
return < div >{ data . message }</ div >;
}
src/Request.test.tsx 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28 import { render , screen , waitFor } from "@testing-library/react" ;
import { Request } from "./Request" ;
import * as fetchDataModule from "./fetch-data" ;
test ( "Requestの動作を確認する" , async () => {
const spy = jest . spyOn ( fetchDataModule , "fetchData" ). mockResolvedValue ({
ok : true ,
status : 200 ,
json : () => new Promise (( resolve ) => resolve ({ message : "OK" })),
} as Response );
render (< Request />);
// waitForを使うとリトライすることができる
await waitFor (
() => {
expect ( screen . getByText ( "OK" )). toBeInTheDocument ();
},
{
timeout : 3000 , // 3sでタイムアウトする
interval : 100 , // 100ms間隔でリトライする
}
);
spy . mockRestore ();
});
テスト対象のコンポーネントの命令網羅は満たされていないが、これはテストケースを別途作成して、それぞれモックを色々用意してやれば網羅できる。
その他
Jest did not exit one second after the test run has completed.
テストの終了が早すぎたために怪しまれている。無視して良い。
1
2
3
4
5
6
7
8
9
10
11
12
13
14 Jest did not exit one second after the test run has completed.
This usually means that there are asynchronous operations that weren't stopped in your tests. Consider running Jest with `--detectOpenHandles` to troubleshoot this issue.
One of your dependencies, babel-preset-react-app, is importing the
"@babel/plugin-proposal-private-property-in-object" package without
declaring it in its dependencies. This is currently working because
"@babel/plugin-proposal-private-property-in-object" is already in your
node_modules folder for unrelated reasons, but it may break at any time.
babel-preset-react-app is part of the create-react-app project, which
is not maintianed anymore. It is thus unlikely that this bug will
ever be fixed. Add "@babel/plugin-proposal-private-property-in-object" to
your devDependencies to work around this error. This will make this message
go away.
Jest はテスト実行が完了してから 1 秒経っても終了しませんでした。
これは通常、テストで停止されなかった非同期操作があることを意味します。この問題のトラブルシューティングを行うには、Jest を `--detectOpenHandles` で実行することを検討してください。
依存関係の 1 つである babel-preset-react-app が、依存関係で宣言せずに「@babel/plugin-proposal-private-property-in-object」パッケージをインポートしています。これは現在機能していますが、これは「@babel/plugin-proposal-private-property-in-object」が無関係の理由で既に node_modules フォルダーにあるためですが、いつでも機能しなくなる可能性があります。
babel-preset-react-app は create-react-app プロジェクトの一部であり、メンテナンスは行われていません。したがって、このバグが修正される可能性は低いです。このエラーを回避するには、devDependencies に「@babel/plugin-proposal-private-property-in-object」を追加します。これにより、このメッセージは表示されなくなります。
Warning: An update to Home inside a test was not wrapped in act(...).
element.click()ではなくfireEvent.click(element)を使う。
import { fireEvent } from "@testing-library/react" ;
fireEvent . click ( button );