Reactのキャッチアップ始めます

はじめに

久しぶりの投稿ですね。やはり習慣的に書き続けるのがよいなと思う今日このごろ。

業務でReactをやりだしたので、そのキャッチアップのメモをまとめて行きたいと思います。

まずはGetting Startedから

おそらく公式ドキュメントのここからスタートするのが良いのかなと思う。

ja.reactjs.org

上記を眺めつつ、チュートリアルにすすむ

ja.reactjs.org

GettingStartedの記事の中で気になたものメモ

Reactを試す

Reactは当初より、既存のプロジェクトに徐々に追加してけるデザインになっている

そうなんだ。それはVue.jsのデザインだと思った。

Web上で試せるオンラインエディタ

  • codepen
    • よくみる、画面上で3panelくらいにわかれるエディタ
    • JSはscriptタグでいれる
  • CodeSandbox
    • package.jsonで依存関係管理できるぽい
  • Stackblitz

軽く、3つとも触ってみたけど、どれも簡単に確認できるんですね。

ローカルで試すだけなら、以下のHTMLファイルだけでも動きを確認できる。 raw.githubusercontent.com

ReactをWebサイトに追加する

  • 1分でReactを導入する
  • JSXを使う

ja.reactjs.org

スクリプトcdnからインポートして、試す方法の紹介

  <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のユニットテストを書けるようになりたいと思います。

v1.test-utils.vuejs.org

インストール

テストランナを選ぶ

  • テストを実行するプログラムであるテストランナを選ぶ
  • vue-test-utilsはどれでも動作する。テストランナにとらわれない
  • jest
    • 最も充実したテストランナ
    • デフォルトでJSDOMを設定する(何もしなくてもDOMが使えるということかな)
    • SFCコンポーネントをインポートできるようにするにはプリプロセッサが必要
      • vue-jestプリプロセッサがあるが、vue-loaderと100%同じというわけではない
        • SFC機能のうち、処理できない書き方とかあるということかな
  • mocha-webpack
    • webpack+Mochaのラッパ
    • vue-loaderを使って、完全なSFCサポートが得られる
    • 多くの設定が必要

ブラウザ環境

  • vue-test-utilsはブラウザ環境に依存する
    • どういうこと?
      • 実際のブラウザで実行することもできる(らしい)
      • でもおすすめはJSDOMを使用して仮想ブラウザ環境でNode.jsでテストすること
      • jestならJSDOMは自動で設定される

単一ファイルコンポーネントをテストする

  • SFCをテストするには、事前コンパイルが必要
  • vue-jestは基本的なSFC機能をサポート
    • スタイルブロック、カスタムブロックは扱えない
  • ガイドがある

v1.test-utils.vuejs.org

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"]

Configuring Jest · Jest

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が標準でインストールされなくなったのか。

Jest v28に上げるためにやったこと

$ 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でアサートする。
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

マウンティングコンポーネント

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-utilsshallowMountを使うと子コンポーネントをスタブによって描画せずにテスト対象コンポーネントをマウントできる
    • なんかうまく動かせなかったな。ただmountをshallowMountに変えただけだと
      • 使い方が違うのかも
    • shallowMountじゃなくてshallow関数に変わってたな

イベントの発行を検証する

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最初の一歩

qiita.com

公式

eslint.org

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を指定する
    • ES Modulesは強制的にstrictモードになったり既存をそのままモジュールとして扱うと壊れる可能性があるため特別扱いされている

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,...がある

エディタ上に表示する

  • vscodeでコードの問題をリアルタイムに表示するとか

プラグインを使う

  • 特定のライブラリやフレームワーク、実行環境に特化した検証はプラグインとして提供される
    • vue専用のチェックとかってことか
  • npm installでインストールして、pluginsプロパティを指定する
    • とあるが、extendsに指定しているのもあるな
      • extendsは推奨設定の指定、必須ではない。pluginをインストールして推奨設定を使うなら、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として存在しますな。以下が公式サイトっぽい。

eslint.vuejs.org

説明あったな。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のルール一覧

eslint.org

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で✔が入っているルール
      • あー、まずはこれだけでもいいのかも
  • eslintはルールをONにしたり、eslint-configをextendしない限りは何もlintしないらしい
  • package.jsoneslintConfigに指定しても良い
  • .eslintrc.jsonがあれば、そっち優先ぽい。eslintConfigは読まれない

eslint.org

cascading and hierarchy

eslint.org

メモ

以下も参考になる

zenn.dev

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を読む

  • できる
    • `import hoge from "cjsmodule.js"
    • cjsってモジュール内でmodule.exportsにセットしたオブジェクト(とか関数)の参照を返す仕組みだから、hogeにその参照が渡ってくる
      • オブジェクト参照ならオブジェクトだし、関数参照なら関数

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();

import - JavaScript | MDN

package.jsonは上位ディレクトリを確認し一番近いファイルのtypeが有効

  • package.jsontype : moduleだけ追記してディレクトリに配置したら、その階層はjs拡張子でもESMとして処理される。
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とか)の動きがよくわからん。

ということで、メモ。

参考になる記事は沢山あり。以下記事の動作を確認しながらメモをしていく。

一つ目

www.webdesignleaves.com

2つ目

jovi0608.hatenablog.com

まずは、1つ目から。

Node.jsのexportsとmodule.exports

  • Node.jsではCommonJS(CJK)フォーマットが使われる
  • モジュールとその依存ファイルの定義には以下が使われる
    • require
    • exports
    • module.exports
  • 参考Understanding module.exports and exports in Node.js
    • モジュールフォーマット?には以下がある
      • Asynchronus Module Definition(AMD)
      • CommonJS(CJS)
      • ES Module(ESM)
      • System.register(なんだこれ?)
      • Universal Module Definition(UMD)
        • ブラウザと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
    • 現在のモジュールのすべてのインスタンス間で共有されるオブジェクト
      • exportsオブジェクトはシングルトン?
      • 複数のファイルからrequireしてたら、同じオブジェクトを参照している?
        • 実験してみたら、複数ファイルからrequireしたら同じインスタンスだった
    • requireを通じてアクセス可能になる
    • 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の型初級」の記事を読みながら、プレイグラウンドで手を動かして行きたいと思います。

qiita.com

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があれば、それは除外
  • 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】オーバーロードの様々な書き方 - Qiita

プレイグランド(関数オーバーロード)

this

  • 関数定義や関数の型を書くときに「this」の型を明示することができる
  • thisの型は最初の引数に書いて明示する
    • ただし本当の引数でないため、thisを渡すわけではない
  • ただしあまり使われないらしい
    • contexualな型推論のために使われることが多いとのこと
      • jQueryみたいに、コールバック関数内でthisの値を書き換える場合にthisの型を正しく推論させるために型指定するみたいな
    • jQueryだとelementが引数にcallbackが呼ばれることがあるが、thisがelementだと推論できるとかか。

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触ってみたいなと思っていたので、チュートリアルをやりたいと思います。

以下の公式チュートリアルで手を動かしていきます!

jestjs.io

始めましょう

インストール

$  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()はマッチャー、他のマッチャーは以下参照。

jestjs.io

コマンドラインからの実行

  • グローバルインストールしたら、jestコマンドでテスト実行できる
// なんかエラーになったな、何指定していいかよくわかってない。
// configファイルは必須なのかな。
$  jest ./jest_sample/
Error: Could not find a config file based on provided

エラーになったが、一旦良しとしよう。

Jest CLIオプションは以下 jestjs.io

追加設定

ここで、設定ファイルを作っていくのね。

基本の設定ファイルを生成する

  • 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の完全なリファレンスはこちら

jestjs.io

一般的なマッチャー

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
  • toBeObject.isを使用して厳密な等価性をテストする
  • toEqual は オブジェクトの値を確認する
    • deep equality らしいので、再帰的に確認するのかな。
  • notで反対のテスト可能。
    • expect(a + b).not.toBe(0)
  • Object.isの比較は参照比較するぽい。Object.is([],[]) => false Object.is() - JavaScript | MDN

jestではexpectの引数はreceivedって読んでるんだね。actualjUnitの文化なのかな。

真偽値(およびそれらしく思える値)

  • 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したことになってしまう。
  • テスト関数にdoneが引数として渡されるので、expectの後に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されることを期待するケース
    • .catchを利用する
    • 想定した数のアサーションが呼ばれたことを確認するためexpect.assertionsを必ず呼ぶ
      • これ忘れそー
    • rejectの引数にerrorメッセージを入力できる
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キーワードを記述するだけ
      • 圧倒的に、わかりやすい気がする。
  • .resolves、.rejectsと組み合わせて使える
    • returnでpromiseを返していたのをawaitキーワードつけて呼び出す
      • うーむ。単純な例だとasyncを書く分、簡潔って感じではないな
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関数を使用する
  • 非同期コードでセットアップする場合はpromiseを返すかdoneパラメータを使う

ワンタイムセットアップ

  • ファイルの先頭/最後で1回だけ実行されるセットアップ
  • beforeAll, afterAll

スコープ

  • describeを使って、複数のテストをグループ化できる。
  • describeの中で、beforeEach, afterEachを使うと、グループ内だけに適用される
    • describeの外側で定義されているbeforeEach,afterEachはdescribe内のtestにも有効っぽい
      • describeの外側のbeforeEachとかの方が、describe内よりも先に実行される
  • describeの中で、beforeAll, afterAllも使える

describeブロックとtestブロックの実行順序

  • describeハンドラを実際の全てのテストを実行する前に実行する
    • describeが全て実行された後に、testが実行されるということ

一般的なアドバイス

  • テストが失敗した場合、テストが単体で実行した場合にも失敗するかどうかを確認する
    • testtest.onlyに変更するとそのテストだけを実行できる

モック関数

  • モック関数でできること
    • コード間の繋がりをテストできる
    • 関数がもつ実際の実装を除去できる
    • 関数の呼び出しをキャプチャできる
    • new によるコンストラクタ関数のインスタンス化をキャプチャできる
  • 関数をモックする方法は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')

モジュールのモック

  • APIのテストをAPIにアクセスせずにテストする
import axios from 'axios'
jest.mock('axios') // これでモック化される

axios.get.mockResoledValue(xxx) // これでgetを呼ばれたときの返り値を定義できる

部分的なモック

  • モジュールを部分的にモックすることが可能、残りのぶ部は実際の実装そのまま
  • junitで言うところのspy

ESMのサンプルコードになっているがローカルで動かない。。

stack overflowの記事と以下の公式ドキュメント見てみる。

jestjs.io

結局、部分的なモックはよくわからんかったな。エラーになるし。。

モックの実装

  • 指定された値を返すという能力を超えて、完全に実装をモック化する
  • jest.fnmockImplementationOnceメソッドを使う
    • const myMockFn = jest.fn(cb => cb(null, true))
      • myMockFnという関数をcb => cb(null, true)という実装でモック化するということか?
  • 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を返すモックを実装するときの糖衣API
    • jest.fn().mockReturnThis()

モック名

  • jest.fn().mockName("hoge name")でモック名をつけれる
  • テスト結果でエラーを出力しているモック関数を迅速に特定できる

試してみたけど、エラー時にモック名が出てるようには見えなかったなぁ。assertの種類によってことなるのかな。

カスタムマッチャ

  • モック関数がどのように呼ばれたか検査するためのカスタムマッチャ
    • これじゃない場合は、「mock」プロパティを検査する必要あある
    • mockFuncjest.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」を開く。

スナップショットテスト

スナップショットテスト · Jest

  • UIが予期せず変更されてないかを確かめるのに非常に有用なツール
  • 2つのスナップショットが一致しない場合は失敗する

たぶん、スナップショットって、文字列(HTMLとか)なんだと思う。

Jestにおけるスナップショットテスト

スナップショットの更新

  • 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をテストするという点では同じ
    • (目的が違う????)
  • スナップショットを手動で作成することもできるがやりやすいものではない
    • テスト駆動的に使うものではなくて、テスト対象のモジュールの出力が変更されたかをわかりやすくるもの
  • コードカバレッジはスナップショットテストでも機能する

非同期の事例

$ 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.jstransform:{}のようにtransformの設定を明示的にリセットする。

たしかに、webpackは不要か、バンドルした結果をテストするわけではなくて、テスト対象モジュールだけをトランスパイルできればよいので、jest側でトランスパイル処理をbabel経由でするのね。(TypeScriptもそうなのかも)

nodeコマンドを--input-type=module指定で実行したらESMを実行できるかと思ったら、そんなことはなかったな。(できると書いてある記事もあったが、できなかった)

まぁ、そんなに困ることはないのか?

  • プロダクションコード:webpackでバンドルするときにトランスパイル
  • テスト:babel-jestでトランスパイル

という使い分けになるのかしら。。(package.jsonにtype=module指定したのを、src直下に置くとかもあるかも??)

非同期の事例

非同期の事例 · Jest

  • __test____mock__はモジュールと同階層に置く
    • jestでテスト実行時は勝手によみこんでテストを実行し、モックがあればモックも読む
  • モック化するには
    • jest.mock("../request") のように、モック化指定する
  • 非同期ロジックのテストを書くときはアサーション(expect)が呼ばれたかをアサートする
    • expect.assertions(1)

タイマーモック

jestjs.io

  • 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以外は、他のメソッドとかも消えている.
  • 上記のモックモジュールを実装しなくてもテストケース側で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()で定義する必要がある。
    • そうしないと、呼ばれたかどうかとかのアサートができない
  • モックの実装をするには
    • jest.fn().mockImplementation*1
      • 省略記法もある、jest.fn*2
  • spyOnでスパイ実装、スパイはロジックを戻せる
    • var spy = jest.spyOn(モジュール, "メソッド名").mockImplementation*3
      • spy.mockRestore()で、実際のロジックに戻せる
  • 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

よし、終わりにしよう!ながかった、気長にちょこちょこやってましたね。 jestだけでなくて、nodeでのモジュール機構のキャッチアップにもなったし、結構ためになった。

やっぱり、手を動かして動作確認しつつやってたのが良かった。

*1:) => console.log("hoge"

*2:) => console.log("hoge"

*3:)=>console.log("hoge"