fumi-sagawa / next-simple-template Goto Github PK
View Code? Open in Web Editor NEWNext.jsのアプリ開発用テンプレートとドキュメント。
Next.jsのアプリ開発用テンプレートとドキュメント。
Storybook→カタログ専門
結合テスト→Jest, testing-library
E2E→Playwright
に寄せるのが疎結合な感じがして良いなぁと思うので、上記を前提に構築し書き直したい。
現在の主流とは逸れる感じがあるので、導入する場合ドキュメントを豊富に用意する。
@fumi-sagawa issueを拝借して、どのようにテストを進めていくのがいいかまとめました。佐川さんが書いた内容とかぶる部分もあるかもしれないですが、参考にしていただければと思います。
工数がとれない、人員もテスト経験も限られる状況では全部のテストを実行していくのは難しいと思いますので、どのテストを優先するか考えました。
(依存関係)----.
|
引数------------->f(x)---->返値(検証対象)
test('now', () => {
const now = new Date() // 現在時刻はテスト実行ごとに異なるので、`now`変数によるテストは冪等でない
expect(now).toBe(/* ... */)
})
https://github.com/boblauer/MockDate
データソース--------.
|
(ヘッドレス)ブラウザ----------->画面操作(検証対象)
flakyなテストとは、成功したり失敗したり結果が一定でないテストのことです。E2Eテストではタイミングによっては必要なDOMが生成するより先に操作が進んでしまうことがあります。たいていの場合は修正可能なのでしっかり潰しておくことが重要です。
フロントエンドでは統合テストを厚くするのがいいとされますが、現実的には一番実行難易度が高いテストなので、テスト経験が少ない状況では無理に行う必要はないと考えます。
また、例えば情報の取得・表示がメインのプロジェクトで本当に必要なのか等、案件の性質も考慮する必要があると思います。
難易度の側面から統合テストは私もあまり書いたことがなく、これと言えるような知見はもっていないというのも正直なところです。
(依存関係)------.
|
(データソース)----|
|
(上位のフック)----|
|
(プロパティ)-------->f(x)---->返値(検証対象)
^
|
`-------状態変更
単純なフックであればユニットテストと同等にテストは容易です。依存関係が少ないフックを選んでテストすることは可能と思います。
一方、windowオブジェクト(テストはNode.jsで実行されますので存在しません)・非同期通信・上位のフックなどが関わるフックは、モック関数の検討が必要なので実行難易度が上がります。書けるに越したことはないのですが、最初に挑戦するのは避けたほうがいいと思います。
# テストが簡単なフック
f(x)---->返値(検証対象)
^
|
`-------状態変更
コンポーネントについてもフックと同様、簡単に実行できるものと実行難易度が高いものがあります。テストに慣れてからのほうがいいと思います。
テストをデプロイプロセスに組み込んで、テストに失敗するコードをリリースしないようにすることもよく行われます。しかしながら、テストに未習熟な状態でこれを行ってしまうと、緊急時に無理やりレッドなテストをコメントアウトしたり、果てはテストが邪魔者とみなされ放置されるといった最悪のケースになってしまうこともあります。テストのメリットを理解するまではそれぞれのローカル環境での実行でも十分と思います。
言葉だけでは何のことかわからないという方もいるかも知れないので、少し実例を挙げておきます。
a
、b
、c
はDateに相当するようなモックが必要なライブラリと仮定します。calcA
〜calcC
は互いに依存しない処理とします。
// common.js
import A from 'a'
import B from 'b'
import C from 'c'
export const calcA() { /* ... Aを使用する処理 */ }
export const calcB() { /* ... Bを使用する処理 */ }
export const calcC() { /* ... Cを使用する処理 */ }
これをテストするにはすべてのライブラリをモックする必要があります。
// common.test.js
import { calcA, calcB, calcC } from 'common'
jest.mock('a')
jest.mock('b') // ?
jest.mock('c') // ??
test('calcA', () => { /* ... calcAのテスト */ })
test('calcB', () => { /* ... calcBのテスト */ })
test('calcC', () => { /* ... calcCのテスト */ })
極端な例ですが、互いに必要ないモックを定義しなければならずテストを書くとき負荷になります。common.js
といった共通処理を集めたファイルがこのような状態になることがあります。
// aUtils.test.js
import { calcA } from 'common'
jest.mock('a')
test('calcA', () => { /* ... calcAのテスト */ })
このように、可能な限り関心事を少なくするように考えます。
あるいは、実際に散見されますが以下のような例。
// iKnowEverything.js
export function iKnowEverything(type, wantToGet) {
if (type === 1) {
if (wantToGet === 'category') {
return 'cat1'
}
if (wantToGet === 'label') {
return 'label1'
}
if (wantToGet === 'text') {
return 'text1'
}
} else if (/* ... */) {
// ...
}
}
これでもテストはできるかもしれませんが、複雑なテストになります。何でもできる関数というのは不用意に関心が集まっている可能性があり注意が必要です。
// iKnowEverything.test.js
test.each(`
a | b | expected
1 | category | cat1
1 | label | label1
1 | text | text1
...
`)('i know everything', (expected, a, b) => {})
このような複雑性が本当に必要なのか、問題を小さくできないか、単純な関数で代替できないか考えます。
const dictionary = {
'1': {
category: 'cat1',
label: 'label1',
text: 'text1',
},
// ...
}
export function getCategoryByType(type) {
return dictionary[type]['category']
}
export function getLabelByType(type) {}
export function getTextByType(type) {}
テストも単純になります。
test('get category by type', () => {
expect(getCategoryByType(1).toBe('cat1');
})
test('get label by type', () => {})
test('get text by type', () => {})
これはあくまで例なので、これが良いかどうかは状況によります(本当に条件分岐が複雑なロジックもありえます)。
なんでもできる関数と似ていますが、いろんな処理をいっぺんに1つの関数で行うと内部の処理がテストしにくい状況になることがあります。
const getXxx = () => {
return new Promise((resolve) => {
const xxxIDs: string[] = [];
state.aaa.bbb.forEach((xxx: any) => {
xxx.item.forEach((x: any) => {
// ... リファクタリングのためテストしたいが外部から参照できない
});
});
if (xxxIDs.length > 0) {
axios.post()
.then(({ data }) => {
// ...
})
.catch(() => {
// ...
});
} else {
// ...
}
});
};
適切に関数を分割することでテストが書きやすくなります。
// この単位であればテストできる(実際書くかどうかは別にして)
function getXxxIdsFrom(xxxs: Xxx[]): string[] {
return xxxs
.map((xxx) => {
return xxx.item.map((x) =>/* ... */);
})
.flat() // or _.flatten()
.uniq(); // or _.uniq()
}
// データ取得の詳細は知らなくてもよさそう
function promiseXxxs(xxxIds: string[]): Promise<Response> {
return axios.post();
}
// ここまでする必要はないかもしれないが、vuexにならってmutationを定義してみる
function mutateXxxs(state, data: Xxx[]): void {}
// ユースケースあるいは vuex における action
async function loadXxxs({ state }): void {
const xxxIds = getXxxIdsFrom();
const response = await promiseXxxs(xxxIds).catch(() => {});
mutateXxxs(state, response.data);
}
このように、テストしやすいコードを考えることと関心の分離について考えることは表裏の関係にあり、テストを書くことはコード品質の向上に寄与すると考えます。
関数についてばかり書いていますが、コンポーネントやフックのレベルになると依存関係が多くなりがちで、何を考えるにも複雑性が伴います。良いコードを考えるにあたっては関数という単位はとても扱いやすいのです。
プログラムというのは書いたとおりにしか動きませんので、予測可能なはずなのですが、複雑になっていくにつれ認識の漏れやブラックボックスが増え、予測不可能な動きをするようになります。
function sum(a, b) {
return a + b // 期待通り動く。入力に対して出力が一意
}
アプリケーションをある種の関数と捉えると以下のようなイメージになります。
function bigApp(a, b, c, d, e, /* ... たくさんの依存関係 */) {
sum(a, b) // ここは予測可能
// ... いろいろな処理
// ... 認識していない条件
// ... 中身はよくわからないけど便利なライブラリを使った処理
return unexpectable // 期待していない動作
}
フロントエンドのアプリケーションは様々な依存ライブラリから成り立っており、言ってみればブラックボックスの塊です。テストによって不透明な部分を減らし、期待通り動く部分を増やすことができれば、アプリケーションは全体的に(sum
関数のような)予測可能な状態に近づきます。アプリの予測可能性を保ち、予測可能な範囲を増やすことはテストの大きな意味での役割と思います。
テストが書いてあり、常に実行されていればデグレーションを恐れることなく安心して変更ができるという側面もあります。最初に期待されるテストの効果はこちらかもしれません。
テストを書いていない処理を変更する場合は、対象のテストを先に書いてグリーンにしておき、コードの修正前後でグリーンであることを確認してデグレが起きていないことを保証することができます(エッジケースに気をつける必要はありますが)。
一つにまとめちゃう
@types/reactのバージョンがReact18系のものになっており、npm installに失敗する模様でした。
@types/reactのバージョンを使われているライブラリとの兼ね合いで、
17系までダウングレードするとnpm installは成功するようでした。
エラーログを貼らせていただきます。
atobe_tatsuya@atobetatsuyanoiMac next-simple-template % npm i
npm ERR! code ERESOLVE
npm ERR! ERESOLVE could not resolve
npm ERR!
npm ERR! While resolving: @testing-library/[email protected]
npm ERR! Found: @types/[email protected]
npm ERR! node_modules/@types/react
npm ERR! dev @types/react@"^18.0.9" from the root project
npm ERR! peer @types/react@"*" from @design-systems/[email protected]
npm ERR! node_modules/@devtools-ds/themes/node_modules/@design-systems/utils
npm ERR! @design-systems/utils@"2.12.0" from @devtools-ds/[email protected]
npm ERR! node_modules/@devtools-ds/themes
npm ERR! @devtools-ds/themes@"^1.2.0" from @devtools-ds/[email protected]
npm ERR! node_modules/@devtools-ds/object-inspector
npm ERR! @devtools-ds/object-inspector@"^1.1.2" from @storybook/[email protected]
npm ERR! node_modules/@storybook/addon-interactions
npm ERR! 1 more (@devtools-ds/tree)
npm ERR! 1 more (@types/react-syntax-highlighter)
npm ERR!
npm ERR! Could not resolve dependency:
npm ERR! peerOptional @types/react@"^16.9.0 || ^17.0.0" from @testing-library/[email protected]
npm ERR! node_modules/@testing-library/react-hooks
npm ERR! dev @testing-library/react-hooks@"^8.0.0" from the root project
npm ERR!
npm ERR! Conflicting peer dependency: @types/[email protected]
npm ERR! node_modules/@types/react
npm ERR! peerOptional @types/react@"^16.9.0 || ^17.0.0" from @testing-library/[email protected]
npm ERR! node_modules/@testing-library/react-hooks
npm ERR! dev @testing-library/react-hooks@"^8.0.0" from the root project
npm ERR!
npm ERR! Fix the upstream dependency conflict, or retry
npm ERR! this command with --force, or --legacy-peer-deps
npm ERR! to accept an incorrect (and potentially broken) dependency resolution.
npm ERR!
npm ERR! See /Users/atobe_tatsuya/.npm/eresolve-report.txt for a full report.
npm ERR! A complete log of this run can be found in:
npm ERR! /Users/atobe_tatsuya/.npm/_logs/2022-07-28T02_19_38_114Z-debug-0.log
nextから提供されているものを追加
https://zenn.dev/thiragi/articles/555a644b35ebc1
preview.jsからmsw addonを除こうかな
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.