Reactのキャッチアップ始めます
はじめに
久しぶりの投稿ですね。やはり習慣的に書き続けるのがよいなと思う今日このごろ。
業務でReactをやりだしたので、そのキャッチアップのメモをまとめて行きたいと思います。
まずはGetting Startedから
おそらく公式ドキュメントのここからスタートするのが良いのかなと思う。
上記を眺めつつ、チュートリアルにすすむ
GettingStartedの記事の中で気になたものメモ
Reactを試す
Reactは当初より、既存のプロジェクトに徐々に追加してけるデザインになっている
そうなんだ。それはVue.jsのデザインだと思った。
Web上で試せるオンラインエディタ
- codepen
- よくみる、画面上で3panelくらいにわかれるエディタ
- JSはscriptタグでいれる
- CodeSandbox
- package.jsonで依存関係管理できるぽい
- Stackblitz
- まんまvscode、すごい
軽く、3つとも触ってみたけど、どれも簡単に確認できるんですね。
ローカルで試すだけなら、以下のHTMLファイルだけでも動きを確認できる。 raw.githubusercontent.com
ReactをWebサイトに追加する
- 1分でReactを導入する
- JSXを使う
<script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin></script> <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin></script>
JSXを手軽に試すには、scriptタグでbabelをインポートする
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
Reactもバンドルツールでバンドルしたりコンパイルせずに、そのままブラウザ上で動作させることできるんだぁ
- HTMLにDOMコンテナの追加
- ReactのJSファイルをCDNからインポート
- DOMコンテナを取得して、ReactDOM.createRoot(コンテナ)
- root.render(e(コンポーネント))
みたいなかんじ。
JSXなしだと
const e = React.createElement; return e( 'button', { onClick: () => this.setState({ liked: true }) }, 'Like' );
JSXありだと
return ( <button onClick={() => this.setState({ liked: true })}> Like </button> );
JSX の使用は完全にオプション
らしい。
おわりに
この記事をメモ代わりにキャッチアップをすすめていきたいと思います。
Vue Test Utils のチュートリアルで手を動かす
はじめに
jestのチュートリアルが終わったので、次のステップとしてVueのユニットテストを書けるようになりたいと思います。
インストール
テストランナを選ぶ
- テストを実行するプログラムであるテストランナを選ぶ
- vue-test-utilsはどれでも動作する。テストランナにとらわれない
- jest
- mocha-webpack
- webpack+Mochaのラッパ
- vue-loaderを使って、完全なSFCサポートが得られる
- 多くの設定が必要
ブラウザ環境
- vue-test-utilsはブラウザ環境に依存する
- どういうこと?
- 実際のブラウザで実行することもできる(らしい)
- でもおすすめはJSDOMを使用して仮想ブラウザ環境でNode.jsでテストすること
- jestならJSDOMは自動で設定される
- どういうこと?
単一ファイルコンポーネントをテストする
Jestを使用したSFCのテスト
Jestのセットアップ
# まずはvueのプロジェクトを作る $ vue create vtu_sample $ cd vtu_sample $ npm run serve # で動くことを確認 # ライブラリいれる $ npm install --save-dev jest @vue/test-utils # あー、vueのバージョンを2.6xで入れたから、エラーになるな。。 # @vue/test-utilsがv2になっているので、v1を入れる必要がありそう。 $ npm info @vue/test-utils versions # '1.3.0'が1系の最新 $ npm i -D jest @vue/test-utils@1.3.0 # インストール成功
//package.jsonのscript以下追記 "test" : "jest"
jestに*.vue
ファイルの処理方法を教えるために、vue-jset
プリプロセッサを入れる。
$ npm i -D vue-jest
package.jsonにjestの設定を記載。(jest.config.jsonじゃなくてpacakge.jsonに追記する書き方か)
"jest": { "moduleFileExtensions" : [ "js", "json", "vue" ], "transform": { ".*\\.(vue)$": "vue-jest" } // @を/srcのエイリアスにしたい場合の設定 // jsconfig.jsonのpathsで設定しているやつ。 // webpackなら、webpack.config.jsのresolve.aliasに`"@" : path.resolve(__dirname,"src")`と設置するぽい // TSなら,tsconfig.jsonのcompilerOptions.pathsに` { "@/*" : ["src/*"]}` "moduleNameMapper": { "^@/(.*)$": "<rootDir>/src/$1" } }
jestのためのBabelの設定
- うーん、テストでES Modules構文とstage-x機能を利用するには
babel-jest
のインストールが必要と - Nodeの最新バージョンでもESMは対応しているけど、CJSとの併用で難点ありそうだから、これはいれないとESM使えないのかな
- jestテストはNodeで直接実行されるので、テスト用に babel-jestを有効にする必要がある、と記載があるな
いったん、 入れないでおこうかな動くかもしれないし。
簡単に、jsのテストを書いてみたら、import文動いたな。 人知れずbabelが動いているわけでもなさそうだし。なくてもいいんかな。
いや、動かなくなったなぜだ。。。とりあえず設定したらええんかな。
$ npm i -D babel-jest
package.jsonのjest.transformに以下追記
"^.+\\.js$": "<rootDir>/node_modules/babel-jest"
単純なテストは成功する用になったが、vueコンポーネントのテストではjestのオプションが不正っぽいエラーがでる。
ググって、babel-coreのバージョンを上げる方法がある(issue)、ということで、以下でバージョン上げた。
$ npm i -D babel-core@^7.0.0-bridge.0
テストファイルの配置
- デフォルトは
.spec.js
または.test.js
- package.jsonで定義可能
- 推奨
- テスト対象のコードのすぐとなりに
__test__
ディレクトリを作成
- テスト対象のコードのすぐとなりに
- jsetがスナップショットテストをするとき
- テストファイルのとなりに
__snapshots__
ディレクトリを作成する
- テストファイルのとなりに
これ、大事やん。__test__
をテスト対象コードの同階層に配置するのが推奨なのか。
カバレッジ
- jestの設定に
collectCaverage
オプションでカバレッジ取れる
{ "jest": { // ... "collectCoverage": true, "collectCoverageFrom": ["**/*.{js,vue}", "!**/node_modules/**"] } }
- デフォルトのカバレッジレポーターのカバレッジレポートは有効になる
Default: ["clover", "json", "lcov", "text"]
coverageReporters
でカスタマイズも可能["html", "text-summary"]
Specの例
テスト失敗した。
[vue-test-utils]: window is undefined, vue-test-utils needs to be run in a browser environment. You can run the tests in node using jsdom See https://vue-test-utils.vuejs.org/guides/#browser-environment for more details.
おー、jestのv28からjsdomが標準でインストールされなくなったのか。
$ npm i -D jest-environment-jsdom
package.jsonのjestブロックに以下を追記。
testEnvironment: "jest-environment-jsdom"
これで、テストは実行されるようになった。
だが、以下warningが出た。
console.error [vue-test-utils]: isVueInstance is deprecated and will be removed in the next major version. 5 | it("is a Vue instance", () => { 6 | const wrapper = mount(HelloWorld); > 7 | expect(wrapper.isVueInstance()).toBeTruthy(); | ^ 8 | }); 9 | }); 10 |
このwarning(error?)は、isVueInstance()がdeprecatedということを伝えるメッセージだった。将来的なバージョンでは削除されるらしい。
スナップショットの更新
jest --updateSnapshot
忘れそうなので、メモ。
スナップショットテスト
- vue コンポーネントをmount関数でマウントする
- wrapper.elementがHTMLのDOM要素っぽいので、それを
toMatchSnapshot
でアサートする。- snapshotディレクトリにスナップショットが生成される
test('renders correctly', () => { const wrapper = mount(Component) expect(wrapper.element).toMatchSnapshot() })
試しに、snapshotディレクトリに作成された.snapファイルを少し修正して、再度テストを実行したら、テストがfailした。
なるほどー
vue用?のカスタムシリアライザライブラリもある
$ npm install --save-dev jest-serializer-vue
package.jsonのjestブロックに設定
"jest" : { "snapshotSerializers" : [ "jest-serializer-vue"] }
うーん?スナップショットを再作成してみたけど、何も変わってない気がする。
多分、vueコンポーネントがシンプルだったからなのかな。結構DOM階層が多かったりすると、なにかきれいになったりするのかもしれない。
「インストール」の章はここまでかな。 mochaとかkarmaを使った場合の例もあったから、jestだけでいいや。
ガイド
はじめる
- 以下のデモリポジトリで手を動かしてみる
git clone https://github.com/vuejs/vue-test-utils-getting-started
- counter.js :vueコンポーネント
- test.js:テストファイル
マウンティングコンポーネント
import { mount } from '@vue/test-utils' import Conter from './counter' const wrapper = mount(Counter) // これでコンポーネントがマウントされる const vm = wrapper.vm // vueインスタンスにアクセス wrapper.html() // HTMLの文字列 wrapper.contains('button') // 要素が存在するかどうかの確認 //ユーザーインタラクションのシミュレーション const button = wrapper.find('button') // findはセレクターを受け取るのかな? button.trigger('click') // buttonのクリックイベントの発火
nextTickについて
- VueはDOM更新をまとめて処理し、非同期に適用する
- Vueが実際のDOM更新の実行を待つために普通は
Vue.nextTick
が必要 - 使い方を簡単にするために、
vue-test-utils
は更新を同期的にて適用するVue.nextTick
を使う必要はない- 非同期コールバックやプロミスの解決の操作のため、イベントループを明示的に進める必要があwる場合は、nextTickがまだ必要らしい
- テストファイルでnextTickを使う必要がある場合、nextTickの内部でPromiseを使っているのでnextTick内で発生したエラーはテストランナーによって補足されない
it ('だめパターン', done => { Vue.nextTick(() => { expect(true).toBe(false) // これわざとfailするアサートしているのかな done() }) }) it("OKパターン:グローバルエラーハンドラを指定", done => { Vue.config.errorHandler = done // これ。 Vue.nextTick(() => { expect(true).toBe(false) done() }) }) it("OKパターン:nextTickのpromseを返す", () => { return Vue.nextTIck().then(function() { expect(true).toBe(false) }) })
一般的なヒント
長いな。
何をテストするかを知る
- 完全なラインベースのカバレッジを目指すことはおすすめされない
- 内部をブラックボックスとして扱うことをおすすめ
- 入力:ユーザーのやり取りやプロパティの変更
- 出力:結果の描画やカスタムイベントの出力
- Counterコンポーネントの場合
- 入力:クリックをシミュレート
- 出力:描画された出力が1つ増加したのかを検証
Shallow 描画
- 単体テストはテスト対象のコンポーネントに焦点をあてる
- 子コンポーネントの動作の間接的な検証は避ける
vue-test-utils
のshallowMount
を使うと子コンポーネントをスタブによって描画せずにテスト対象コンポーネントをマウントできる- なんかうまく動かせなかったな。ただmountをshallowMountに変えただけだと
- 使い方が違うのかも
shallowMount
じゃなくてshallow
関数に変わってたな
- なんかうまく動かせなかったな。ただmountをshallowMountに変えただけだと
イベントの発行を検証する
wrapper.vm.$emit('foo') wrapper.vm.$emit('foo', 123) // wrapper.emitted() で実行されたイベント情報が得られる { foo: [ [], [ 123 ] ] }
このemitted()
で返されるイベント情報を使って、アサートする。
コンポーネントの状態を操作する
- setData()関数を使って、vueインスタンスのdataをセットする
- なんかうまくいかない、warrningがでる。
[Vue warn]: Avoid adding reactive properties to a Vue instance or its root $data at runtime - declare it upfront in the data option.
- vueのバージョンでもう許可されなくなったとかそんなもんかな。
- setProps()関数をつかって、プロパティを渡せる
- これはできた。
プロパティをモックする
- propertyを後からセットするんじゃなくて、mount時にわたすこともできる
mount(Component, { propsData: { hoge : "hoge" } })
非同期動作のテスト
- vue-test-utils は同期的にイベントをトリガします。従って、 Vue.nextTick() を実行する必要はない
- コールバックやPromiseのようなコンポーネントの非同期動作をテストする場合のテクニックの紹介
- axiosとか非同期APIを使う場合、普通にアサートしただけだと、アサート後に処理が実行される
- 非同期処理を待つ必要がある
import flushPromises from 'flush-promises'
を使って、可読性を上げることもできる。await flushPromises
することで、promiseを全部流せる
it("hoge test", done => { // 省略 wrapper.vm.$nextTick(() => { expect(wrapper.vm.hoge).toBe("hoge") done() }) })
TypeScriptと一緒に使う、という章はチラ見した。
- vueファイルを使うために、
vue-jest
を入れる - TypeScriptを使うために、
ts-jest
を入れる
くらいかな。
以上、で終わり!
ESLintのキャッチアップ
はじめに
ESLintはJS/TSをやっていく上では必須になっていると思うんだけど、いまいち設定方法とか仕組みをわかってない。(特にプラグインでルール追加とかpresetとか)
ゼロから設定したり、vscodeのプラグインとして使うのではなくて、コマンドラインとして単体で使ってみることで理解を深めたいと思う。
色々設定もありそうなので、ベストプラクティスは何なのかとかも探してみたい。
参考記事
ESLint最初の一歩
公式
ESLintの特色(上の記事の引用)
- すべの検証ルールを自由にon/offできる
- 自分のプロジェクトに合わせたカスタムルールを簡単に作れる
- 豊富なビルトインルールに加えて、たくさんのプラグインが公開されている
インストール
$ npm i -D eslint $ npx eslint -v v8.14.0 $ npx eslint test.js Oops! Something went wrong! :( ESLint: 8.14.0 ESLint couldn't find a configuration file. To set up a configuration file for this project, please run: npm init @eslint/config
.eslintrc.json
ファイルが内とだめっぽいな。
とりあえず、記事に記載のまま.eslintrc.json
を作成。
{ "extends": ["eslint:recommended"], "plugins": [], "parserOptions": {}, "env": {"browser": true}, "globals": {}, "rules": {} }
extendsのrecommended
がよく見るやつだな。設定ファイルを継承しているということかしら。
(eslintが推奨している設定を引き継いでいる的な)
$ npx eslint test.js /home/xxxx/git/eslint_sample/test.js 1:16 error 'name' is defined but never used no-unused-vars 2:42 error 'nama' is not defined no-undef ✖ 2 problems (2 errors, 0 warnings)
ファイル名指定しなかったら、フォルダ内にある*.js
を検証するとあるが、うまく認識してくれなかったな、なぜでしょう。
ルールの追加
.eslintrc.json
のrulesに追加する- セミコロンつけてないとエラー
semi : error
- セミコロンつけてないとエラー
ルール:error | warn | off
- ルールにはオプションを設定できる
- ルールの指定が配列になって、2番目以降がオプション
semi : [ error, never]
みたいない- neverだとセミコロンをつけてたらだめになる。
eslint/lib/rules
以下にsemi.js
とかルール毎にJSファイルがあるっぽい
グルーバル変数を定義する
- ESLintはファイル単位で静的チェックするのでグルーバル変数を教える必要がある
- ローカルで試すと別にエラーにはなってないな
- そんなことなかった
$
とかつけたらエラーになった
- そんなことなかった
- ローカルで試すと別にエラーにはなってないな
- 方法
- コメントで定義する
/* globals $ */
- 設定ファイル(.eslintrc.json)のglobalsに定義する
"$" : false
// falseは書き換え可能かどうか
- コメントで定義する
環境設定をする
env
プロパティで前提条件を設定できるbrowser : true
DOMAPIを有効にするnode : true
Node.js固有の変数や構文が定義されるes6: true
ES6で追加された構文や組み込みオブジェクトが使える- ただし、ES Modulesは有効にならない
- parserOptionsに
soruceType : module
を指定する
- parserOptionsに
- ES Modulesは強制的にstrictモードになったり既存をそのままモジュールとして扱うと壊れる可能性があるため特別扱いされている
- ただし、ES Modulesは有効にならない
ES6を活用するためのルール
- たくさんあるらしい。とりあえずONにしてたらええんか?
- このあたり良し悪しを把握したい
"arrow-body-style": "error", "arrow-parens": "error", "arrow-spacing": "error", "generator-star-spacing": "error", "no-duplicate-imports": "error", "no-useless-computed-key": "error", "no-useless-constructor": "error", "no-useless-rename": "error", "no-var": "error", "object-shorthand": "error", "prefer-arrow-callback": "error", "prefer-const": "error", "prefer-rest-params": "error", "prefer-spread": "error", "prefer-template": "error", "rest-spread-spacing": "error", "template-curly-spacing": "error", "yield-star-spacing": "error"
ES6以降
- envの
es6: true
以外にもparserOptionsでecmaVersion
を指定する必要がある- デフォルトは5
- 5ってなんだ
- 5, 2015, 2016, 2017,2018,2019,...がある
- デフォルトは5
エディタ上に表示する
- vscodeでコードの問題をリアルタイムに表示するとか
プラグインを使う
- 特定のライブラリやフレームワーク、実行環境に特化した検証はプラグインとして提供される
- vue専用のチェックとかってことか
npm install
でインストールして、plugins
プロパティを指定する- とあるが、extendsに指定しているのもあるな
- extendsは推奨設定の指定、必須ではない。pluginをインストールして推奨設定を使うなら、extendsに指定する
- とあるが、extendsに指定しているのもあるな
npm install eslint-plugin-xxxx
みたいな命名規則になっているplugins
にはxxxx
を指定する- 例:
npm i eslint-plugin-node
- rulesにてプラグインのルール設定をする
プラグイン名/ルール名
という命名規則
vue-cliからvue2.xのプロジェクトを生成したらいかのような.eslintrc.jsonが生成されてた。
module.exports = { root: true, env: { node: true, }, extends: [ "plugin:vue/essential", "eslint:recommended", "plugin:prettier/recommended", ], parserOptions: { parser: "@babel/eslint-parser", }, rules: { "no-console": process.env.NODE_ENV === "production" ? "warn" : "off", "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off", }, };
plugin:vue/essential
というのがvue独自のルールかな。pluginの指定がされてませんが。。どうなんだ。
もしかしたら、eslintのバージョンアップでpluginの指定は不要になったのかも?
plugin:vue/essential
と指定されているがnode_modulesにはeslint-plugin-vue
として存在しますな。以下が公式サイトっぽい。
説明あったな。essentialが本当に基本的なもの。推奨されているものがすべて入っているわけではない。recommendedが推奨が全て入っている。って感じか。
"plugin:vue/essential" ... base, plus rules to prevent errors or unintended behavior. "plugin:vue/strongly-recommended" ... Above, plus rules to considerably improve code readability and/or dev experience. "plugin:vue/recommended" ... Above, plus rules to enforce subjective community defaults to ensure consistency
shareable configを使う
- ESLintのルールは多い。
- 組み込みルール:200以上
- プラグイン:200以上公開されている
- shareable configを使うと楽に共有できる。
- 毎回eslintrc.jsonをコピペする必要なし
- npmパッケージで設定をまとめられているので、npm installしてextendsプロパティを指定する
- eslint-config-eslintはESLintチームの設定
eslint-config-xxxx
が命名規則。extendsにはxxxxの部分を指定する
ESLintのルール一覧
OSSプロジェクトのeslintrc.jsonの確認
vue3
- extendsで何も指定されてないな、どういうことrulesに指定されているものしか利用してないのかな?
core/.eslintrc.js at main · vuejs/core · GitHub
TypeScript
- plugins
@typescript-eslit
,jsdoc
,no-null
,import
- extendsはしていない
TypeScript/.eslintrc.json at main · microsoft/TypeScript · GitHub
React
- extends: fbjs, prettier
root:true
これがあると親ディレクトリにeslintrcがあるか見ないらしい- plugins
- jest, no-for-of-loops, no-function-declare-after-return, react, react-internal
- parser
- babel-eslint // ふーむ。parserってどういうとき指定するんだー
react/.eslintrc.js at main · facebook/react · GitHub
なんか、各プロジェクト毎に違ってて、これさえ使えばOKみたいな感じではなさそうな気がするな。
あ、でも「eslint:recomended
」がよいのかな。vue:recomendedとか、typescript:recomendedとか。
ESLintの公式ガイドやってみる
npm init @eslint/config
でconfigファイルを生成できる.eslintrc
ファイルはjs, yml, json形式が可能- config自動生成すると、
eslint:recommended
がextendsに指定される- rulesで✔が入っているルール
- あー、まずはこれだけでもいいのかも
- rulesで✔が入っているルール
- eslintはルールをONにしたり、eslint-configをextendしない限りは何もlintしないらしい
- package.jsonの
eslintConfig
に指定しても良い .eslintrc.json
があれば、そっち優先ぽい。eslintConfigは読まれない
cascading and hierarchy
メモ
eslint --fix
つけると修正して上書きしてくれるな。修正可能なものなら。(セミコロンがないとか)- parserっていつ指定する必要があるだろうか。TSを使うときかな。
- そんな感じがする
- overrideってなんだ
- airbnbのeslitnt-config-airbnbは有名なのかな
以下も参考になる
Node.jsでCommonJSとESMの挙動を調べる
nodejsでコードを書いているとimoprtやrequireの使い分けがよくわからなかったので、使い分けを理解したかった。
以下の公式ドキュメントが参考になる。
Interoperability with CommonJS Modules: ECMAScript modules | Node.js v18.0.0 Documentation
imoprt statements
- importはESMとCJSをインポートできる。
- ただし、CJSはdynamic importでのみ対応している
package.jsonのtype=moduleの指定のありなし
- なし
- js拡張子はcjs扱い
- esmのファイルはmjs拡張子にする
- あり
- js拡張子はesm扱い
- cjsのファイルはcjs拡張子にする
ESMからCJSを読む
- できる
CJSからESMを読む
require("esm module")
はできない- importは制限付きというかdynamic importのみできる
const hoge = await import("esm.mjs")
みたいな。拡張子は省略できない。
export default
されてたモジュールは、defaultで参照できるconst { default as hoge } = await import("esm.mjs")
default
しかないモジュールでも{ default : xxxx }
という形でcjs側ではimportされる
// myEsmModule.mjs export const HOGE = "hoge"; export default { fuga : "fuga" } // cjs async function main() { const myModule = await import("./expEsm.mjs"); console.log(myModule); // { HOGE: 'hoge', default: { fuga: 'fuga' } } } main();
package.jsonは上位ディレクトリを確認し一番近いファイルのtypeが有効
package.json // type:module指定なし hoge.js // cjs module1 - package.json // type:module指定あり - fuga.js // esm
Node.jsのexportsとmodule.exportsのメモ
はじめに
nodejsのモジュール機構がよくわからない。ES6以降だとJavaScriptにモジュール機構の仕様(import)があるが、node.jsで使われるCommonJS(exportsとかrequireとか)の動きがよくわからん。
ということで、メモ。
参考になる記事は沢山あり。以下記事の動作を確認しながらメモをしていく。
一つ目
2つ目
まずは、1つ目から。
Node.jsのexportsとmodule.exports
- Node.jsではCommonJS(CJK)フォーマットが使われる
- モジュールとその依存ファイルの定義には以下が使われる
- require
- exports
- module.exports
- 参考Understanding module.exports and exports in Node.js
require
- モジュールやファイルをインポートする関数
- 引数
- モジュール(ファイル)のパス
- node_moduleディレクトリにあるモジュールの名前
- ビルトインモジュールの名前
- 拡張子は省略可能(ESMでは省略不可らしい)
- 引数
- エクスポートされたモジュールを戻り値として返す
- 変数に代入してその変数を使うことでモジュールを扱える
//// file1.js module.exports = "hoge"; //// main.js // ローカルファイルfile1.jsをインポート const hoge = require("./file1"); console.log(hoge) ; // hoge // node_modulesディレクトリにあるbarをインポート const bar = require("bar"); // node_modules/barディレクトリを作って、 // npm init -y でpacakge.jsonを作って、 // mainに指定されているindex.jsに「module.exports = "bar"」を記載 // これで、モジュールとしてbarが読めた // これが最小構成なのかも // ビルトインモジュールfsをインポート const fs = rquire("fs");
モジュールの検索パス
$ node -e "console.log(module.paths)" [ '/home/xxxx/git/cjs_esm/node_modules', '/home/xxxx/git/node_modules', '/home/xxxx/node_modules', '/home/node_modules', '/node_modules' ]
ビルトインモジュール
node -e "console.log(require('module').builtinModules )" [ '_http_agent', '_http_client', '_http_common', '_http_incoming', '_http_outgoing', '_http_server', '_stream_duplex', '_stream_passthrough', '_stream_readable', '_stream_transform', '_stream_wrap', '_stream_writable', '_tls_common', '_tls_wrap', 'assert', 'assert/strict', 'async_hooks', 'buffer', 'child_process', 'cluster', 'console', 'constants', 'crypto', 'dgram', 'diagnostics_channel', 'dns', 'dns/promises', 'domain', 'events', 'fs', 'fs/promises', 'http', 'http2', 'https', 'inspector', 'module', 'net', 'os', 'path', 'path/posix', 'path/win32', 'perf_hooks', 'process', 'punycode', 'querystring', 'readline', 'repl', 'stream', 'stream/consumers', 'stream/promises', 'stream/web', 'string_decoder', 'sys', 'timers', 'timers/promises', 'tls', 'trace_events', 'tty', 'url', 'util', 'util/types', 'v8', 'vm', 'worker_threads', 'zlib' ]
module
というグローバル変数もあるけど、require('module')
とは別なのか。
exports
- モジュールを他のプログラムで使用できるようにすには以下を使う
- exports
- module.exports
//user.js exports.getName = () => { return "hoge" } // main.js const user = require('./user.js'); console.log(user.getName());
分割代入
const { getName, dob } = require('./user');
module.exports
- 1つだけをエクスポートするモジュールがある場合などはmodule.exportsを使用するのが一般的
class User { xxxx } module.exports = User;
exportsとmodule.exports
- exportsはmodule.exportsのショートカットのようなもの
- 初期状態ではexportsはmodule.exportsへの参照
exports.a = "A"; exports.b = "B"; console.log(exports); console.log(module.exports); console.log(exports === module.exports); // true
exportsとmodule.exportsが異なる場合
- exports( or module.exports) の参照を上書きしてしまうと異なるものを指す
- 参照上書きしてしまうと、module.exportsの方が有効になるぽいな
exports = { hoge: "hoge" }; console.log(exports === module.exports); // false
次は2つ目の記事を読みます
Node.js : exports と module.exports の違い(解説編)
moduleとexports
- module
- 現在のモジュールへの参照
- module.exportsはexportsオブジェクトと同じ
- moduleは実際はグルーバルではなくて各モジュール毎のローカル
- exports
- module.exportsが本体。exportsはlittle helperらしい。
- だから、module.exports = { xxx } みたいに参照を更新したら、exportsは使えないのか。
使い分け
- モジュールを特定のオブジェクト型にしたいならmodule.exportsを使う
module.exports = User
みたいな。- require()の戻り値を、コンストラクター関数や配列・文字列などにしたい場合
- 通常のmoduleインスタンスとして使うなら、exportsを使う
const obj = require("./myobj"); console.log(obj.hoge)
みたいな。
src/node.jsを読む
- `NativeModule.wrapという処理で、モジュール内のソースを無名関数でラップしている
- exports, require, module,filename, dirnameはその無名関数の引数
- いまはESM対応もされているしこのあたりのロジックは結構変わってるのかな
(function(exports, require, module, __filename, __dirname) { モジュールファイル内の JavaScript コード });
最新っぽいソース
- bootstrap/loader.jsとmodues/cjs(or esm)/loader.jsになってる
node/loaders.js at master · nodejs/node · GitHub
node/loader.js at master · nodejs/node · GitHub
記事の擬似コードがとても参考になる。
- 本体のコード(requireが記載されているコード)より先に、moduleが読み込まれる。
- moduleオブジェクトはrequireされる毎にインスタンス化される
- requireからの戻り値はmodule.exports変数。
基本的には、module.exportsが本体なのでこっちが優勢。exportsは参照が上書きされることがある。
ただし、module = { exports : { xxx} }
みたいにすると、moduleはrequire毎にインスタンス化されるので、exportsの方が優勢になる。
よくわかりました。
次は、ESMの方もキャッチアップしたい。
「TypeScriptの型初級」で手を動かす
「TypeScriptの型入門」の続編である「TypeScriptの型初級」の記事を読みながら、プレイグラウンドで手を動かして行きたいと思います。
TypeScriptの型初級の記事の目標
- 実用上必要となるTypeScriptの型の挙動を理解する
- 標準ライブラリに存在する型の使い方を理解する
前回の記事が入門編ということだが、内容的には結構濃かったと思う。 まぁ、全部とまでは言わないが、ほぼほぼ理解はできたと思っているので(忘れていると思うけど)、次のステップにすすみつつ、復習していき、自分のスキルにしていきたいっすね。
union型の復習
- 初級編のひとまずの主役はunion型らしい
- union型は
T1 | T2 | T3
のように複数の型を|
で繋げた型- T1, T2, T3のいずれかである値の型という意味
- union型のいいところ
- if文やswitch文などで実行時に型を判定するコードを書くと型が絞られる
conditional typeにおけるunion distribution
- ことばで説明するのがむずい概念。
- 「union型の条件型」が「条件型のunion型」に変換される
T3 = T1 | T2
で、T3 extends A ? B : C
, みたいな条件の場合に( T1 extends A ? B: C) | (T2 extends A ? B:C)
とunion型が分配される
条件型の結果側における型変数の置き換え
- 条件部だけでなく、結果にも型変数が使える
type NoneToNull<V extends Option<unknown>> = V extends Some<unkown> ? V : null |;
こういう書き方もできるのねー、という感想しかない。。 多分あまり腑に落ちてないだろうな。
分配されるのは型変数のみ
- union distributionが発生するのは条件部分の型が型変数である場合のみ
V extends Some<unknown> ...
みたいな。
- 型引数を持つような型を型関数という
- 型関数をインライン化するだけで結果が変わる。。(非直感的らしい...)
- union distributionさせることを意図しているのかそうじゃないのか人のコードを見るときに考える必要がある
- 型変数で条件分岐したいけど、union型が来ても分配してほしくない場合
何か適当な型で囲む
- なんじゃこりゃ
- 型変数だけになるとunion distributionされる。
- TじゃなくてT[] とか配列にするとunion disributionされない
- 配列型で囲むのが記法が簡単なのでよく使われる
- こんなのがよく使われるのかぁ
V[] extends Some<infer R>[] ? R : undefined
にするとunion distributionされないらしい
never型とunion distiribution
- never型は属する値が無い型。union disributionでは特殊な振る舞いをする
- never型は0個のunion型であるように振る舞う
- ぐぬぬ。どういうことでしょうか。。
T extends never ? X : Y
のとき、Tにneverを代入すると結果はneverになる。- なぞ、これはよくわからん
union distributionのまとめ
- 条件型の条件部分の型が型変数ならばunion型が分配される
- 直感に反するが、これがあるからunion型を便利に使えるらしい
- 後半で説明があると、楽しみ
- exendsの左が型変数になるのを特別扱いする理由は、unionを分配したときに結果側もかきかえなければいけないから
- うーん???
- ちょっとむずいね
プレイグラウンドで手を動かす(union distributionまで)
mapped typeのunion distribution
- mapped typeもunion型を分配する
- 分配が発生する条件はconditional typeのときよりも複雑
{ [P in keyof T] : X }
のTが型変数のとき、Tにunion型が入ると分配される[ P in keyof T]
は頻出する形なので、そのときにunion型だったらどうなるかとか意識する必要がある
mapped typeと配列型
[ P in keyof T ] : A
という形で、T
に配列の型(タプル型も含む)が入る場合も特別な挙動をする- T = number[]とかの場合、forEachなど関数型もあるが、普通にやると、forEachもA型になるため、関数として実行できなくなる。
- 型変数の場合は、forEachは関数型として残してくれる。
- 型変数Tが配列型の場合、プロパティをマップするのではなくて、要素の型のみをマップしてくれる
type Strify<T> = { [ P in keyof T ] : string } // 本来の挙動は、T型のすべてのプロパティをstring型にする type NumArr = number[] type StrArr = Strify<NumArr>; // StrArr は string[] 型になる。要素の型(string)だけがマップされた // number型のそれぞれの要素がstring型になるので、string[]になるということかな
- Strify
は普通のmapped type定義で、オブジェクト型にそのまま使える - 配列を特別扱いしなくてもよいのがいい感じってことかな
ちなみに、この機能がTが型変数の場合に制限されているのは、そうしないとマップ後の配列の要素の型を正しく求められない場合があるからでしょう。{[P in keyof T]: X}という型(Xは型変数とは限らない任意の型)で配列型U[]をマップした場合、要素の型はX内のT[P]をUで置換したものになります。この形のmapped typeならば、要素の型をマップする際にXの内部にもともとの要素の型はシンタクティックにT[P]という形で現れるためそれをUに置換すればいいわけです(ほかにT[number]とかもあるかもしれませんが、それは置換せずそのままでOKです)。Tが一般の型になってしまうとこのような変換が難しくなります。
この部分、わかんねー、
- タプル型も配列型の一種なので同じルールが適用
- オブジェクト型じゃなくて、タプル型に変換される???
うーん、なんとなく理解できているが、説明は難しいというレベルの理解度だな。
readonly配列型との変換
- 標準の「Readonly
」を利用して、オブジェクトのプロパティをreadonlyにできるけど、配列やタプル型を指定してもreadonlynになる - うーん、この理屈がまだ理解できてないな。
標準ライブラリの型
- いつでも使える便利な型ライブラリ
- ユーティリティ型が定義されているlib.es5.d.ts
最新のlib.es5.d.ts
- Omitとか追加されている
Record
- 辞書型として使える
const dict: Record<string, number>
- 存在しないキーがundefinedを返す可能性を無視していることに注意
Record<string, number | undefined>
とするかMapを使う
K extends keyof any
のkeyof anyはstring | number | symbol
と同値Partial
- プロパティをすべて任意にする
[P in keyof T]? : T[P]
Required
- プロパティをすべて必須にする
[P in keyof T]-?
: T[P]`-?
で任意指定を削除できる
Readonly
- プロパティを参照専用にする
readonly [P in keyof T] : T[P]
Pick<T, K>
- 一部のプロパティを取得する
- 既存の型をちょっといじった新しい型を作りたい場合によく使われる
Exclude<T, U>
- union distributionを前提としている
- Tに何らかのunion型が入ったとき、その構成要素のうちUの部分型を除外する
T extends U ? never : T
T = 'foo' | 0
の場合- ('foo' extends string ? never : 'foo') | ((0 extends string ? never : 0)
- condtional typeに合致したらneverなので、部分型なら除外される
Extract<T, U>
- union distributionを前提としている
- Tに何らかのunion型が入ったとき、その構成要素のうちUの部分型を残す
これらは、union型で代数的データ型ぽいことをやる場合に役立つ
Omit<T, K extends keyof T>
- 指定したプロパティを除外したオブジェクト型を作る
- TypeScript3.5で追加になったっぽい
- PickとExcludeを駆使して実現されている
Pick<T, Exclude<keyof T, K>>
NonNullable
- Tからnullまたはundefinedを除外
- 別にnullやundefinedのプロパティを除外するというわけではない
- union型の要素としてnull/undefinedがあれば、それは除外
- Tからnullまたはundefinedを除外
Parameters
- 関数に関わる型
- Tは関数型でなければならない
- Tの引数の型一覧をタプル型の形で取得できる
T extends (...args: infer P) => any ? P : never
- イマイチだな理解がこれは
- Tが関数の型だったらPを返す。Pはタプル型になるんぁ?
ReturnType
- 関数に関わる型
- 返り値の型を取得する
T extends (...args: any[]) => infer R ? R : any
- Tが関数の型ならその戻り値の型Rを返す。
関数のオーバーロード
- TypeScriptでは関数のオーバーロードを定義することができる
- JavaScriptにはその機能はないので、型だけオーバーロードできる
- 関数本体が無い関数宣言だけ書く(オーバーロード)
- 関数本体の宣言では型はオーバーロードされた関数の全てに当てはまる包含的な型の必要がある
arg2!
の!
はTypeScript独自の演算子。nullやundefinedを取り除くダウンキャスト演算子- 使いにくいから筆者はあまりオーバーロードされた関数定義は好きでないと
- ただし、既存のJSライブラリに型を付ける際によく登場する
- オブジェクト型を用いて関数の型を宣言するときも、同様に関数シグネチャを複数ならべてオーバーロードされた関数の型を表現できる
- interfaceの関数型でもオーバーロードできる
参考 【TypeScript】オーバーロードの様々な書き方 - Qiita
this
- 関数定義や関数の型を書くときに「this」の型を明示することができる
this
の型は最初の引数に書いて明示する- ただし本当の引数でないため、
this
を渡すわけではない
- ただし本当の引数でないため、
- ただしあまり使われないらしい
this型
- クラス(やインターフェース)のメソッド内で使うことができる特殊な型
- cloneメソッドを実装するときに使われる
- 継承元でcloneを実装して返り値の型が親クラスになると都合が悪い
public clone(): this
というように、thisを型として指定できる
- うーむ、まぁ、あるのか。
カスタム型ガード
- if文とtypeofを組み合わせて型の絞り込みができる
- 型の絞り込みを行う用の関数を自分で定義できる
isFooObj(arg: any): arg is FooObj
みたいに定義可能- 返り値はboolean
- そんなにたくさん使われるものでもない
- 標準ライブラリの
Array.isArray
に使われているisArray(arg: any): arg is Array<any>;
まとめ
- union型は強力
- union型を積極的に使っていこう
次のステップ
色々あるので、時間を見つけて見ていこうかな。
途中だれて、時間かかっちゃったけど、一通り読めた!
理論系は一旦一区切りして、なにか実装したりTypeChallengeとかやっていこうっと。
jestのチュートリアルをやってみる
はじめに
JavaScriptのユニットテストフレームワークでよく名前を聞くのが、jestですよね。
筆者はまだjestでユニットテストを書いたことが無いですが、ここ最近jest触ってみたいなと思っていたので、チュートリアルをやりたいと思います。
以下の公式チュートリアルで手を動かしていきます!
始めましょう
インストール
$ npm install -D jest
チュートリアルのソースはnodejsのソースぽいな。requireを使う。 import形式だとまた違うのかな。
//// sum.js function sum(a, b) { return a + b; } //// sub.test.js const sum = require('./sum'); // test, expectはrequireしなくても使える test('adds 1 + 2 to equal 3', () => { expect(sum(1, 2)).toBe(3); }); //// packge.json "scripts" : { "test" : "jest" }
$ npm run test > jest_sample@1.0.0 test > jest PASS ./sum.test.js ✓ adds 1 + 2 to equal 3 (4 ms) Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total Time: 0.533 s, estimated 1 s Ran all test suites.
おー、テストが実行されましたね。簡単なもんだな。 これで、初jestテストの実績できたわ。
toBe()
はマッチャー、他のマッチャーは以下参照。
コマンドラインからの実行
- グローバルインストールしたら、jestコマンドでテスト実行できる
// なんかエラーになったな、何指定していいかよくわかってない。 // configファイルは必須なのかな。 $ jest ./jest_sample/ Error: Could not find a config file based on provided
エラーになったが、一旦良しとしよう。
追加設定
ここで、設定ファイルを作っていくのね。
基本の設定ファイルを生成する
jest --init
で設定ファイルを生成- 結構色々聞かれた。
- configファイルにTSを使うか、実行環境はnodeかとか。
- よくわかってないが。基本はデフォルト値にした
jest --init The following questions will help Jest to create a suitable configuration for your project ✔ Would you like to use Typescript for the configuration file? … no ✔ Choose the test environment that will be used for testing › node ✔ Do you want Jest to add coverage reports? … no ✔ Which provider should be used to instrument code for coverage? › v8 ✔ Automatically clear mock calls, instances and results before every test? … no
オプションなのか、以下の説明があるが、必ずしも使う必要は無いのか。。
- Babelを使用する
- webpackを使用する
- Parcelを使用する
- TypeScriptを使用する
- TypeScriptをBabel経由で使用する
- TypeScriptをts-jest経由で使用する
ちょっと、jestと上記の関係がわからんな。上記と一緒に使うこともできるよ、くらいか?
jestはテスティングフレームワークなので、babelとかでES5に変換してテストするとか?、テストランナーを他のツールから呼べるようにして、そのときにコードのトランスパイルが必要ならよろしくやるよ、ってことなのかな。
Matcherを使用する
Macherの完全なリファレンスはこちら
一般的なマッチャー
test('two plus two is four', () => { expect(2 + 2).toBe(4); });
expect(2 + 2)
は"expectation"オブジェクトを返す。toBe(4)
の部分がマッチャー- Jestが実行されると、失敗したマッチャーをすべて追跡し、エラーメッセージを表示することができる
わざとFailさせてみた。
$ npm run test > jest_sample@1.0.0 test > jest FAIL ./sum.test.js ✕ adds 1 + 2 to equal 3 (12 ms) ● adds 1 + 2 to equal 3 expect(received).toBe(expected) // Object.is equality Expected: 4 Received: 3 3 | 4 | test('adds 1 + 2 to equal 3', ()=>{ > 5 | expect(sum(1, 2)).toBe(4); | ^ 6 | }) at Object.<anonymous> (sum.test.js:5:23) Test Suites: 1 failed, 1 total Tests: 1 failed, 1 total Snapshots: 0 total Time: 1.019 s, estimated 2 s
toBe
はObject.is
を使用して厳密な等価性をテストするtoEqual
は オブジェクトの値を確認する- deep equality らしいので、再帰的に確認するのかな。
not
で反対のテスト可能。expect(a + b).not.toBe(0)
- Object.isの比較は参照比較するぽい。
Object.is([],[]) => false
Object.is() - JavaScript | MDN
jestではexpectの引数はreceived
って読んでるんだね。actual
はjUnitの文化なのかな。
真偽値(およびそれらしく思える値)
undefined
,null
,false
を区別したい場合と、一緒として扱いたい場合がある- それに対応するマッチャーがある
toBeNull
:null
のみに一致toBeUndefined
:undefined
のみに一致toBeDefined
: toBeUndefinedの反対。(undefinedじゃなければ一致)toBeTruthy
: ifステートメントが真であるもとみなすものに一致toBeFalsy
: ifステートが偽であるとみなすものに一致
数値
toBeGreaterThan(x)
: より大きいtoBeGreaterThanOrEqual(x)
: 以上toBeLessThan(x)
: より小さい- `toBeLessThanOrEqual(x): 以下
toBe(x)
: 等しいtoEqual(x)
: 等しいtoBeCloseTo
: 丸め誤差に依存しないように
文字列
toMatch
: 正規表現でマッチするかを確認できる
配列と反復可能なオブジェクト
toContain()
: 配列や反復可能なオブジェクトに特定のアイテムが含まれているかどうかを確認できる
例外
toThrow
: ある関数が呼び出し時に例外を投げることをテストする- 例外をスローする関数は、ラッピング関数内で呼び出される必要がある
非同期コードのテスト
- JSでは非同期コードをよく書く。
- jestはテスト対象コードがいつ完了したかを別のテストに進む前に知る必要がある
- いくつか方法がある
コールバック
- fetchData(callback)のように、処理が終わったらcallback関数を呼び出す場合
- jestのテストはデフォルトでは一度最後まで実行したら完了
- callback関数の中でアサートしても呼ばれない
- テストがpassしたことになってしまう。
- callback関数の中でアサートしても呼ばれない
- テスト関数にdoneが引数として渡されるので、expectの後にdone()を呼ぶ
- 引数にdoneを書いた時点でテストの挙動が変わるな
- コールバック内でdone()を呼ばなかったら、タイムアウトする
- 引数にdoneを書いた時点でテストの挙動が変わるな
test('the data is peanut butter', done => { function callback(data) { try { expect(data).toBe('peanut butter'); done(); } catch (error) { done(error); } } fetchData(callback); });
Promises
- promiseを使用するコードなら、非同期テストをもっと簡単に処理できる
- むしろ、これが本筋というか、あるべきか?
- fetchData関数がpromiseを返すようにする
- テスト関数の中でも、promiseを返す。
test('the data is peanut butter', () => { return fetchData().then(data => { expect(data).toBe('peanut butter'); }); });
- promiseがrejectされることを期待するケース
test('the fetch fails with an error', () => { expect.assertions(1); // expectが1回呼ばれたことの確認? でもこの行なくてもエラーにならんな return fetchData().catch(e => expect(e).toMatch('error')); });
expect.assertions(1)
がよくわからん。この行なくてもエラーにならんぞ。でも、assertions(2)とかにするとエラーになるのか。だから、この行なくてもfailはしないけど、assertions(1)にして、assertがされなかったらエラーになるので、ちゃんとassertはされていることは確認が必要、ということね。
.resolves / .rejects
- expect宣言で、
.resolves
マッチャを使うこともできる- テストメソッドからreturnするのは必須
- resovle, rejectじゃなくて、resolves, rejectsで複数形なのが間違えそう
test('the data is peanut butter', () =>{ return expect(fetchDataPs()).resolves.toBe('peanut butter'); // returnは必須 }) test('the fetch fails with an error', () => { return expect(fetchDataPsError()).rejects.toMatch('error'); })
Async/Await
- asyncとawaitをテストで使用できる
- testに渡すアロー関数前にasyncキーワードを記述するだけ
- 圧倒的に、わかりやすい気がする。
- testに渡すアロー関数前にasyncキーワードを記述するだけ
- .resolves、.rejectsと組み合わせて使える
- returnでpromiseを返していたのをawaitキーワードつけて呼び出す
- うーむ。単純な例だとasyncを書く分、簡潔って感じではないな
- returnでpromiseを返していたのをawaitキーワードつけて呼び出す
test("the data is peanut butter (async/await ver)", async () => { const data = await fetchDataPs(); expect(data).toBe("peanut butter"); }); test('the fetch fails with an error (async/await ver)', async ()=> { expect.assertions(1); try { await fetchDataPsError() } catch (e) { expect(e).toMatch('error') } }) // resolveと併用 test("the data is peanut butter, (async/await and resove)", async () => { await expect(fetchDataPs()).resolves.toBe("peanut butter"); });
セットアップと破棄
- junitでいうところの、@beforeEachみたいな機能
- 共通的なセットアップ作業をまとめれる
テストごとにセットアップ作業を繰り返す実行する
beforeEach
関数、afterEach
関数を使用する- junitと同じやん
- 非同期コードでセットアップする場合はpromiseを返すかdoneパラメータを使う
ワンタイムセットアップ
- ファイルの先頭/最後で1回だけ実行されるセットアップ
beforeAll
,afterAll
スコープ
describe
を使って、複数のテストをグループ化できる。- describeの中で、beforeEach, afterEachを使うと、グループ内だけに適用される
- describeの外側で定義されているbeforeEach,afterEachはdescribe内のtestにも有効っぽい
- describeの外側のbeforeEachとかの方が、describe内よりも先に実行される
- describeの外側で定義されているbeforeEach,afterEachはdescribe内のtestにも有効っぽい
- describeの中で、
beforeAll
,afterAll
も使える
describeブロックとtestブロックの実行順序
- describeハンドラを実際の全てのテストを実行する前に実行する
- describeが全て実行された後に、testが実行されるということ
一般的なアドバイス
- テストが失敗した場合、テストが単体で実行した場合にも失敗するかどうかを確認する
test
をtest.only
に変更するとそのテストだけを実行できる
モック関数
- モック関数でできること
- 関数をモックする方法は2つある
- テストコードの中でモック関数を作成する
manual mock
を作成してモジュールの依存性を上書きする
モック関数を利用する
- モックコールバック関数の生成
mockCallback = jest.fn(x => 42 + x)
- 呼ばれた回数
- mockCallback.mock.calls.length
- 引数
- mockCallback.mock.calls[0][0] は 1回目の呼び出しの第一引数
- 返り値
- mockCallback.mock.results[0].valueは1回目の呼び出しの返り値
.mockプロパティ
- mock関数の生成
const myMock = jest.fn()
- 何もしない関数ということかな
- すべてのモック関数には
.mock
プロパティがあり、モック関数呼び出し時のデータと、関数の返り値が記録されている - thisの値も記録されている
一個上で列挙したプロパティか。
モックの戻り値
myMock.mockReturnValueOnce(1)
myMock()
の呼び出しで、定義した値が変える。
- 継続渡しスタイルで書ける
- `myMock.mockReturnValueOnce(10).mockReturnValueOnce('x')
モジュールのモック
import axios from 'axios' jest.mock('axios') // これでモック化される axios.get.mockResoledValue(xxx) // これでgetを呼ばれたときの返り値を定義できる
部分的なモック
ESMのサンプルコードになっているがローカルで動かない。。
stack overflowの記事と以下の公式ドキュメント見てみる。
結局、部分的なモックはよくわからんかったな。エラーになるし。。
モックの実装
- 指定された値を返すという能力を超えて、完全に実装をモック化する
jest.fn
かmockImplementationOnce
メソッドを使うconst myMockFn = jest.fn(cb => cb(null, true))
- myMockFnという関数を
cb => cb(null, true)
という実装でモック化するということか?
- myMockFnという関数を
mockImplementationOnce
メソッドは他のモジュールによって作成されたモック関数のデフォルトの実装を定義したいときに便利
jest.mock('../foo'); // これでrequire('../foo')はモック化される const foo = require('../foo'); foo.mockImplementation(() => 42); foo() // => 42
mockImplementationOnce
- 関数への複数回への呼び出しで異なる結果を得るようにしたい場合に利用
test(" test multi mockImplementationOnce", () => { const myMockFn = jest .fn() .mockImplementationOnce(cb => cb(null, true)) .mockImplementationOnce(cb => cb(null, false)); myMockFn((erro, val) => expect(val).toBe(true)) myMockFn((erro, val) => expect(val).toBe(false)) })
- モック関数が
mockImplementationOnce
によって定義された実装をすべて使いきった場合jest.fn
のデフォルトの実装を実行する
test(" test default impl", () => { const myMockFn = jest .fn(()=> 'default') .mockImplementationOnce(() => 'first call') .mockImplementationOnce(() => 'second call'); expect(myMockFn()).toBe('first call'); expect(myMockFn()).toBe('second call'); expect(myMockFn()).toBe('default'); });
.mockReturnThis
this
を返すモックを実装するときの糖衣APIjest.fn().mockReturnThis()
モック名
jest.fn().mockName("hoge name")
でモック名をつけれる- テスト結果でエラーを出力しているモック関数を迅速に特定できる
試してみたけど、エラー時にモック名が出てるようには見えなかったなぁ。assertの種類によってことなるのかな。
カスタムマッチャ
- モック関数がどのように呼ばれたか検査するためのカスタムマッチャ
- これじゃない場合は、「mock」プロパティを検査する必要あある
mockFunc
がjest.fn()
で定義されている場合に、mockFunc.mock
に検証用の情報が格納されるやつ
snapshotだけよくわからんかったな。
test(" test custom matcher", () => { const myMockFn = jest.fn((arg1, arg2)=> console.log(`${arg1}_${arg2}`)); myMockFn(1,2) myMockFn(2,3) expect(myMockFn).toHaveBeenCalled(); expect(myMockFn).toHaveBeenCalledWith(1, 2); expect(myMockFn).toHaveBeenLastCalledWith(2, 3); expect(myMockFn).toMatchSnapshot(); });
てか、このカスタムマッチャーの方を普通は使いそう。.mock
プロパティを使うとだるすぎる。
Jestプラットフォーム
なんか、Jestを利用したツールとかを作るためのAPIぽいな。
- jest-changed-files
- jest-diff
- jest-docblock
- jest-get-type
- jest-validate
- jest-worker
- prettyFormat
- なんかモジュール読み込めなかったな。
jest community
へー、awesome jestなんてものがあったのか。
GitHub - jest-community/awesome-jest: 🕶Awesome Jest packages and resources
一旦備忘メモ
chromeでデバッグ
$ node --inspect-brk node_modules/.bin/jest --ru nInBand sum.test.js
chromeを開いて「chrome://inspect
」を開く。
スナップショットテスト
- UIが予期せず変更されてないかを確かめるのに非常に有用なツール
- 2つのスナップショットが一致しない場合は失敗する
たぶん、スナップショットって、文字列(HTMLとか)なんだと思う。
Jestにおけるスナップショットテスト
- reactの場合は
import renderer from 'react-test-renderer';
を使う - 初めて実行したときにsnapshot fileが生成される
- snapshot fileはレビューされるべき
- スナップショットテストができた経緯
- スナップショットテストの感覚を掴む
- 読んでない。あとで読むか。
スナップショットの更新
jest --updateSnapshot
- 失敗するすべてのスナップショットテストのスナップショットを更新する
--testNamePattern
で更新対象を限定できる
インタラクティブスナップショットモード
- 一つ一つ差分みながら、更新できる。
インラインスナップショット
- スナップショットを.snapファイルに出力するのではくてコード上に追記してくれる
Property Matchers
- IDとか、日付とか、実行毎に値が変わるものがある場合
- 具体的な値の代わりに、
Any<Number>
とかスナップショットに記載するとエラーにならない。- mockしたら良いのでは、と思った
ベストプラクティス
スナップショットをコードとして扱う
- レビューする
- テストがfailしたときに、何も考えずにスナップショットを再作成しないようにする
べき等性のあるテストを書く
- 何回実行しても同じ結果になるべき
Date.now = jest.fn(()=> 1482363367071);
のようにモック化するなど- 何回実行しても1482363367071を返すようになる
叙述的なスナップショット名を使う
- 「~である」という説明のような書き方
exports[ <username /> should render Alan Turing
] みたいな。- 何が描画されるか読んでわかる
よくある質問(で気になったもののメモ)
- スナップショットファイルはバージョン管理する
- スナップショットは出力が正しいかをテストするという目的でいつでも使える
- ビジュアル回帰テストとの違いは、ビジュアル回帰はwebページのスクリーンショットをとってピクセル単位で比較する、スナップショットはテキストファイルを比較する、目的が違う
- 2つともUIをテストするという点では同じ
- (目的が違う????)
- スナップショットを手動で作成することもできるがやりやすいものではない
- テスト駆動的に使うものではなくて、テスト対象のモジュールの出力が変更されたかをわかりやすくるもの
- コードカバレッジはスナップショットテストでも機能する
非同期の事例
- 事前準備としてjestでbabelのサポートを有効にする
$ mkdir jest_babel_sample $ cd jest_babel_sample $ npm init -y $ npm i -D jest $ npx jest --init #これはjest.config.jsを作るため、選択肢は適当に選んだ。てか結局中身は消した気がする。(module.exports = {}だけ残した) $ npm run test # jestが動くようになる。jest --initでそのように選択したら $ npm i -D babel-jest @babel/core @babel/preset-env ## babel.config.js # module.exports = { # presets:[['@babel/preset-env', {targets: {node: 'current'}}]], # }; # ./srcフォルダ以下に、sum.js, sum.test.jsを作成 # ESM形式で記述(import / export) $ npm run test # sum.test.jsが実行される。
あー、なるほど。Babelの設定がプロジェクト内にあれば、自動的にファイルをトランスパイルして、テストを実行してくれるみたい。
babel-jest
がそれを担っているのかな。
自動的にファイルをトランスパイルされたくない場合はjest.config.js
にtransform:{}
のようにtransformの設定を明示的にリセットする。
たしかに、webpackは不要か、バンドルした結果をテストするわけではなくて、テスト対象モジュールだけをトランスパイルできればよいので、jest側でトランスパイル処理をbabel経由でするのね。(TypeScriptもそうなのかも)
nodeコマンドを--input-type=module
指定で実行したらESMを実行できるかと思ったら、そんなことはなかったな。(できると書いてある記事もあったが、できなかった)
まぁ、そんなに困ることはないのか?
- プロダクションコード:webpackでバンドルするときにトランスパイル
- テスト:babel-jestでトランスパイル
という使い分けになるのかしら。。(package.jsonにtype=module指定したのを、src直下に置くとかもあるかも??)
非同期の事例
__test__
と__mock__
はモジュールと同階層に置く- jestでテスト実行時は勝手によみこんでテストを実行し、モックがあればモックも読む
- モック化するには
jest.mock("../request")
のように、モック化指定する
- 非同期ロジックのテストを書くときはアサーション(expect)が呼ばれたかをアサートする
expect.assertions(1)
タイマーモック
- jestはタイマー関数を自分で時間経過をコントロールできる関数に置き換えることができる
- setTimeout, setInterval, clearTimeout, clearInterval
- タイマーを任意時間で発火できるということかな?
jest.useFakeTimers(); //タイマーをコントロールするだけならこれだけでよさげ jest.spyOn(global, "setTimeout"); //setTimeoutが呼ばれたかなど検証したいならspyOnする // 全てのタイマーを実行する jest.runAllTimers // こういうふうにモック化して渡すから、toBeCallledとかで検証できるのかな const callback = jest.fn(); expect(callback).toBeCalled(); // 待機中のタイマーを実行する jest.runOnlyPendingTimers() // 1回待機中のタイマーを実行する、呼ぶたぶにタイマーが進むっぽいな。 // 指定した時間でタイマーを進める jest.advanceTimersByTime(msToRun)
マニュアルモック
- マニュアルモックはモックデータを返す機能をスタブするために使う
ユーザモジュールのモック
- モジュールのディレクトリ直下のmocksサブディレクトリにモックモジュールを作成
- ./src/user.jsというモジュールをモックするなら
./src/__mocks__/user.js
- mocksは小文字
Nodeモジュールのモック
- node_modulesと並列にmocksフォルダを作る
- lodash.jsをモックするなら
.__mocks__/lodash.js
という感じかな- モック実装する
- 簡単に実装する、
export default jest.createMockFromModule("lodash");
- 独自実装する
export default { isString : jest.fn() }
とか返すと、isStringだけモック化される- isString以外は、他のメソッドとかも消えている.
- 簡単に実装する、
- モック実装する
- lodash.jsをモックするなら
- 上記のモックモジュールを実装しなくてもテストケース側でimport前に以下実行でモック化できる
jest.mock("lodash");
- spyOnでもできる
- ES module importするときは、importの前にjest.mock("モジュール")しなきゃいけない
- jestが自動的に、jest.mockをコードの先頭に移動してくれる
- JSDOMに実装されていないメソッドのモック
ES6クラスのモック
- モック関数を使用することでモックを作成できる
- クラス構文はいくつかのコンストラクタ関数の糖衣構文らしい
自動モック
- jest.mock("モジュールファイル名")でモックコンストラクターになる
- これで、newとかできるんすねぇ。
マニュアルモック
- mocksディレクトリに
モジュールファイル名.js
で配置する - モックの実装としては
jest.fn().mockImplementation(() => { return {xxx} }) みたいに、クラスで生成されるインスタンスの形式のオブジェクトを返す、ただし、オブジェクトのプロパティで関数があったら、
jest.fn()`でモック化する
jest.mock()
をモジュールファクトリ引数で呼ぶ
- 第二引数にモジュールファクトリを渡す
- モジュールファクトリはモック関数を返す関数
jest.mock('./モジュールパス', () => { return jest.fn().mockImplementation(() => { return { xxxxx:yyyy} }); }); // mockImplementationは新規オブジェクトを返す関数を定義
jest.mock()は先頭に引き上げられるから、jest.mockより上で定義した変数を参照するとエラーになるよ、みたいなこと書いてある気がする。
クラスメソッドをモック化するシンプルな例
const playSoundFileMOck = jest .spyOn(SoundPlayer.prototype, 'playSoundFileI) .mockImplementation(()=>{ console.log("mocked function") });
クラス(関数)のプロトタイプメソッドをモック化する。
モックをスパイする
jest.fn()
でモック関数を作る
//default exportの場合 import Hoge from './hoge' jest.mock('./hoge', () => { return jest.fn().mockImplementation(() 0 => { return { fuga : () => {}} }) ) // default export以外を持っくする import { Hoge } from './hoge' jest.mock('./hoge', () => { return { Hoge : jest.fn().mockImplementation( () => { return { fuga: () => {}} }), } })
クラスのメソッドをスパイする
- クラスメソッドが期待した引数でよばれているかを確認するには、クラスメソッドをスパイする必要がある
テストでモックコンストラクタが呼ばれると、その都度新しいオブジェクトが生成される。すべてのオブジェクトのメソッド呼び出しをスパイするには、モックオブジェクトが参照するモック関数インスタンスを同一にする
const mockFuga = jest.fn() jest.mock('./hoge', () => { return jest.fn().mockImplementation(() 0 => { return { fuga : mockFuga} })
テスト間でのクリーンアップ
[モックオブジェクト].mockClear()
- 呼び出し記録をクリアする
備忘メモ
- モック関数は
jest.fn()
で定義する必要がある。- そうしないと、呼ばれたかどうかとかのアサートができない
- モックの実装をするには
- spyOnでスパイ実装、スパイはロジックを戻せる
- jest.mock("axios")でモジュールをモック
axios.get.mockResolveValue(xxxx)
でaxios.getが返す値を定義できる
参考リンク
モジュールモックのバイパス
jest.mock('hoge')
でモジュールをモックするが全部モックされてしまう- hogeモジュールのfuga関数だけは実際のロジックを利用ということもできる
jest.mock('hoge') import hoge from 'hoge' const { fuga } = jest.requireActual('hoge')
ECMAScript Modules
- experimantalな機能らしい。
- code transformsを明示的にdisableする。
transform: {}
node
を実行時に--experimenta-vm-modules
を付ける
こまかいところはちょっとわからんが。babelやTypeScriptで実行した方がサポートされているから良さそう。
Using with webpack
- webpackと利用する場合、ちょっと情報古そうな気もする。
- どういうケースで利用できるのかよくわからん
DOM操作
- 直接DOM操作するコードはテストが難しいとされる
- ボタンを押下したらバックエンドあらfetchしてdomにhtml追加とか
jest-environment-jsdom
を使うnpm i -D jest-environment-jsdom
これだけでいいんかな- testEnvironmentプロパティにjsdomを指定しないといけないっぽい
- テストメソッドの上部にコメントでenvironmentを指定する方法もあるみたいだけど、うまく認識してくれなかった。なぜでしょう
jsdomはJS上でDOMを再現(イベントの監視とか、domのデータ構造の再現とか)してくれるみたい。
サンプルコード実行してみたら、動いたな。テストコードではjsdomは意識しなくても良いみたいだ。testEnvironmentで指定しているからか、テストコード上でdocument.body.innerHTML
でHTMLを突っ込んだら、それがDOMとして認識してくれるみたい。
つまり、テストコード上でブラウザで実行しているのと同じ感覚で実装できるということか。
-
- ホワイトボードで説明してくれている動画、英語だからちょっと厳しい。。
よし、終わりにしよう!ながかった、気長にちょこちょこやってましたね。 jestだけでなくて、nodeでのモジュール機構のキャッチアップにもなったし、結構ためになった。
やっぱり、手を動かして動作確認しつつやってたのが良かった。