Closed16

The Art of Unit Testing, Third Edition with examples in JavaScript を読んでみて心に残ったことをまとめる

bun913bun913

Praise for the second edition

  • Robert C. Martin からこの書籍のSecond Edition への褒めの言葉が出てきている
  • 他にも凄そうなメンバーがたくさんいるし、これは楽しみだぜ・・・!?
bun913bun913
  • Unit test の Unit は「単一のメソッド」ということを指すわけじゃない
  • メソッドのように小さかったり、複数のクラスのように大きかったりする

という感じで first edition の頃から、さらにこの辺りを掘り下げてくれていそうな気配を感じる。(まだChapter1にも入っていない)

ちなみに著者の Roy Osheove さんの他の活動についても、 ABOUT THIS BOOK の中で紹介されている。(トレーニングの紹介とかが主みたい)

https://www.artofunittesting.com/

例えばユニットテストの定義として作者が考えていることなども書かれていて非常に興味深い

https://www.artofunittesting.com/definition-of-a-unit-test

ちなみにこの書籍の共著の Vladimir Khorikov さんは Unit Testing Principles, Practices, and Patterns: の著者でもある。日本語版しかみていないが、とても良い本だったので楽しみだ。

https://amzn.asia/d/0nT4OaV

本の読み進め方とか、著者の紹介とか、この本を推す人たちの声で書籍の1/10くらいが終わってしまったぞ。

洋書は初めてだけど洋書ってこうなのか?

bun913bun913

The basics of unit testing

  • 以下を定義づけるよということが最初に書かれている
    • 「ユニット」とはなにか?
    • 「良いユニットテストとはなにか」
  • 書籍はJSとTSで書かれているが、他の言語から入った人でも問題ない旨が書かれている
    • JSでは手続き的、関数型的な書き方をするときに使う
    • TSはOOPのような書き方をするときに使う

1.2

  • Unit TestUnit って何よ?っていう話が書かれている
    • 作者にとっては unit of work or use case とのこと
    • SUT ( subject, system, Under Test)をテストするのであって、 CUT(Components, Class, Code` をテストするのではない )

これは私の感想だが Unit Testing Principles にも「実装の詳細ではなく、観察可能な振る舞いをテストする」ということが繰り返し書かれていた。

つまり関数自体をテストするというよりかは、その裏にある「ユーザーストーリーや受け入れ基準で定義されている何かしら」に対してテストするということかな?

1.3

  • Unit of work における Entry points と exit points について
  • Unit of works は単一の関数とも、複数の関数の集まりともなりうる
    • 何かしらのトリガーで外側から呼び出される(Entry Points)
      • Entry Points は一つとは限らない
        • 関数内から呼ばれたり、単独の関数としても呼べたりとかね
    • 何かしら役に立つことをして終わる(Exit Points)
      • Exit Points も一つとは限らない

ここではまさに Unit Testing Princples で出てくる「Behavior(ふるまい)」を使っていないのは「外部から見える」という意味を込めて Exit Ponts という言葉を使っているぽい。

やはり、「実装の詳細」ではなく「外部から観察可能な振る舞い」的なところをテストしていこうぜ。というのが昨今の「ユニットテストの基本」的な感じで語られることが多いなという印象を受けた。

また、Command Query 分離の原則について読む事を進めている。テスタビリティが高いってことはそもそもソフトウェアが良い設計になっているってことだから、知っておいたほうが当然良いよね。

https://martinfowler.com/bliki/CommandQuerySeparation.html

1.4 Exit Points Types

  • Exit Points の種類について語られている
  • これが後の1.5の Exit Points が違えば違う種類のテクニックが必要という点に繋がってくる
    • システムが保有する状態を変えないけど、なんらかreturnを返すのか
    • システムが保有する状態を変えて、returnをしないのか
    • logger みたいに自分では完全にコントロールできないもので処理をしているのか(サードパーティーのライブラリとかね)

ここでは、以下サイトを紹介さているがHTTPSに対応していないぽいので注意が必要

https://xunitpatterns.com/

日本語で調べればいくつかでるので見てみると良さそう

https://qiita.com/hgsgtk/items/a3186a250d36d3b224d9

1.5 Different exit points, different techniquest

1.4 で話したように exit points が異なるなら異なるテクニックを使おう的なことが書かれている。

ちなみにこの話は値ベースのテスト、状態ベースのテストといった感じで Unit Testing Principles の書籍でも述べられていたことに近いと思う。

例えばサードパーティーのロガーについてテストする場合、モックなどを使うことも考えられるが、作者としてはあまりモックを使わないとのこと。(5%いかないとかなんとか)

これは私の感想であるが、これも Unit Testing Principles にもちょろっと書かれていた、モックを誤って使うことで実装の詳細と結びついてしまって、テストのメンテンナンスが難しくなるからなのかなぁ?などと思っている。
(リファクタリングへの耐性がなくなる的な文脈なのかしら?)

1.7.1 What is a good unit test?

こちらに作者の良いユニットテストに関する定義が書かれている。

上で紹介したサイトにも少し乗っているけど、この書籍の方ではより詳しく乗っている。(良い自動テストとはという定義もある)

ネタバレになるので全部は書かないけど、とても良い。

https://www.artofunittesting.com/definition-of-a-unit-test

こちらの作者さん的には in memory DB を使うくらいなら、本物のDBのを統合テスト用に使って、単体テストにはstubを使え的なことが書いてある。

うーん。

私の感想だけど、一部の重いDBをのぞいて docker で簡単にDBを構築できる昨今、自分たちのシステムしか使わない(コントロールできる)DBをモックにする必要はあまり感じないなぁ。

だからあくまでUnit Testはメモリ内で高速に動作してほしいから、DBなどはコントロール可能な共有依存として Integ Test として実行するべきなのかなぁ。

Unit Test は極端な話、 VSCode の jest プラグインとかでコードの保存のたびに走っても、秒速で終わる・処理が軽いものとしたほうが良いのかもしれないね。

1.8 Integration tests

このセクションでは Integration tests について触れられていた。

作者的に Intergration tests とは上の章で述べていた良いユニットテストの条件に一つでもあたっていないものになるそう。

やはり上で述べたように仮にDockerを使っていても、DBを巻き込んだテストは Integration tests になるもよう。時間かかるし、ボタン一つで即時実行!いつでもなんでもすぐに動かせるぜ!っていうのとはちょっと違うもんね。

それらを理解したうえで、大事なのは重要なビジネスロジックをDBとかの都合から切り離して、DDD的にやっていくことが重要そう。

そうすればDBとかのなどの依存からロジックを切り離してメモリ内で素早く Unit test を動かせるし、DBを使うテストを Integ Test 側に寄せて Unit Test よりも実行頻度を落としながらもすぐに実行できるようにする。みたいに分けておくと良さそうだ。

Jestとかvitestでいい感じに @unit みたいなアノテーションをつけられたりしたら、ディレクトリ構成が一緒でも問題はないけど、分けるのが自然かな。

bun913bun913

2 A first unit test

ここからJestを使って実際にユニットテストを作っていく作業に入るみたい。

2.4 Creating a test file

特になんということはないが途中でjestがデフォルトで見る __tests__ ディレクトリの使用をやめる旨が書かれていてちょっとワクワクしている。

私はテストコードはビジネスロジックの横に置いておきたいマンだからだ。統合テストのディレクトリ配置とかも気になっているからワクワクしてきたぜ!!

あんまり言及されていないけどヘェ〜ポイントがたくさんあった。

  • 作者は spec.js も test.js も両方使う
    • test は spec の最も簡単なバージョンだと思っているから
    • だから とってもシンプルなことに test.js を使うらしい(後で深掘りしよう)
  • テストファイルの配置について
    • 2パターンある
      • 幾らかの人はテスト対象のファイルやモジュールのすぐ隣に配置するのを好む(ワシ)
      • そして test ディレクトリに配置する人もいる
    • どっちでもあまり問題はないらしい(プロジェクトで一貫性を保てば)
    • 作者的にはtestディレクトリに配置することで、テスト用のヘルパーファイルが近くにおけるメリットがある。
    • IDEにテストとテスト対象コードを移動するショートカットがあるしね的な

今までテスト対象のファイルの横に置いていたけど、結構心変わりしているw

単純に /integ とか /unit とかディレクトリ配置を分けることができて、実行タイミングを変えたりするのが容易だから良いよねっていうのを思い始めてきたから。

だけど、それをするなら開発者が「これって統合テスト?ユニットテスト?」っていうことに悩まないで済むように定義やガイドラインを合意しておかないといけないなと思った。

bun913bun913

2.5 Use Naming

jestで(に限らずだけど)テストを書くときのテストタイトルの付け方にあんがあった。

以下のように テスト対象のもの(SUT) シナリオ もしくは 入力 期待する振る舞い or Exits Points をカンマ区切りで書くというもの。

以下でいうとSUTは verifyPassword という関数が対象で、何をどう振る舞うのか分かり易いよね。

test("verifyPassword, given a failing rule, return errors", () => {})

こういうルールは結構気になるので他のところの事例とかも聞いてみたいな。

ちなみにこのルールは書籍のレビュー中に Tyler Lemke さんが教えてくれて作者も気に入ったとのこと。

2.6 Using describe

この後に↑のテストを describe で論理的なコンテキストでまとめることを推奨された。(僕もこれがいいと思う)

describe("verifyPassword", () => {
    test("given a failing rule, return errors")
})

ちなみに作者は describe をネストしてテストを論理的にまとめるタイプを好むとのこと。

私の観測では結構ネストを好まない人が多かったので、他にも同じ考えの人がいることが素直に嬉しかった。

describe("verifyPassword", () => {
    describe("with a faling rule", () => {
        it("returns erros")
    })
})

こんな感じの。

2.9 Refactoring to parameterized tests

Jestの it.eachtest.each のようなパラメーター化テストについて記載されている。

作者的にも「同じシナリオの異なる入力値をまとめる」のに使うとのことで、違うシナリオならわけそうだった。

この辺りの感覚は以前書いたこのブログとそう違わないかも。

https://dev.classmethod.jp/articles/vitest-parameterized-test/

bun913bun913

BDDタイプのテストシンタックスといわゆる昔からあるシンタックスについて作者の意見が書いてある。

基本的にテスト対象がシンプルで理解しやすいなら、BDDを使わずに良さそう。
一方一つのエントリーポイント(呼び出し元的な)から複数の結果が期待される場合などはBDDで書くなど工夫が必要そう。

bun913bun913

Breaking dependencies with stubs

Types of dependencies

2種類の依存について説明がある。

  • Outgoing dependencies
    • Logger, Databaseへの保存, Emailの送信、APIの実行・・・
    • Exit Point になるような行為のこと
    • 動詞となっている言葉 保存する 送信する 実行する
  • InComing dependencires
    • 最終的な動作に対する要求ではない、
    • データベースクエリの結果、ファイルシステムのファイルの内容、ネットワーク応答の結果、などをインジェクトするようなものたち
    • 以前の操作の結果として流入させようとしている受動的なデータ

ちなみに書籍でも参照されているが、スタブやモックの違いはXUnit Patterのサイトにも記載されている。(外部サイト)

http://xunitpatterns.com/Mocks, Fakes, Stubs and Dummies.html

ここから関数型のDI,モジュールのDI,オブジェクト指向的なDIの解説に入る。

いずれも、「依存する(処理、モジュール、クラス)は外からインポート、インジェクトするようにして中で直接インポートしない」ことが肝要だなと感じた。

この本でもヘキサゴナルアーキテクチャについて触れらている。

https://fintan.jp/page/397/

tsによる例ではinterfaceを注入するという王道が使われていて、この辺りは概ね予想通りだった。

bun913bun913

4 Interaction testing using mock objects

  • mock
    • 依存の中でも外に向かう挙動を模倣したりアサートしたりするもの
    • ロガーの出力だったり、Exit Point にあたる挙動
  • stub
    • 依存の中でもうちに向かう挙動を模倣したり、fakeしたりするもの
    • stubはexit point ではないので、stubの値をassert したりすることはない
    • DBに尋ねた結果の返り値とか、外部のシステムのAPIの返り値とかが当たりそう

3章でも述べられたように、単体テストをするためにもOSSなどの外部のモジュールを直接import してつかうのではなく、関数の外から注入するようなことが基本となる模様。(ほんそれな)

4.6.1 Working with a curriying style

ここではカリー化のテクニックが述べられている。

正直カリー化を効果的に使えた試しがないのだが、関数の部分適用というより遅延適用?の側面もあるかもしれないが、これは使いこなしたら便利そうだなぁ。と思った。

https://www.geeksforgeeks.org/why-is-currying-in-javascript-useful/

もうちょっと深い話が欲しい。

ちなみにこの後に、curry化までしなくても高階関数で引数を順次渡すカリー化のテクニックも述べられている。

私が関数型のスタイルにあまり詳しくないので、こういうテクニックとpros・consを知っていきたいなぁ。

4.7 Mocks in an object-oriented style

ここからオブジェクト指向的な形でのモックの注入方法について書かれているが、基本としては constructor による引数の注入部分で渡しておくことっぽい。そこから説明が始まっている。

ちなみに作者のコツとしてモックやスタブのクラスや関数を作るときに名前として mockstub は使わずに fake という接頭辞を使うことが多い模様。(stubとして使ったりmockとして使ったりと色々使えるしね)

test double よりもわかりやすくて良いと思う。

4.7.2 Refactoring production code with interface injection

パラメーターとして依存性を注入するときに、実装ではなく interface を注入するようにリファクタリングする手法が書かれている。

ただし interface を使うなら、以下のような条件を両方満たしていることをお勧めするとのこと。

  • interface をコントロールできる(サードパーティーが作ったものではない)
  • unit of work やコンポーネントの需要にピッタリである

何でもかんでもinterfaceを渡せば良いというわけではなく、サードパーティ製のものを使う場合にも必要な特徴だけを interface に持たせるように自作するのが大事そうだね。

4.9 Partial mocks

クラスや関数を全てfakeに置き換えるのではなく、一部の関数や挙動だけを置き換えるテクニック。

依存を外部から注入せずに実行している場合は、これをしていくしかないよね。(スパイと呼んでいる)

bun913bun913

5 Isoslation frameworks

ここまでは、モックやスタブを手動で作成してどのようにテストするかという観点で書かれていた。

この章では jest や substitute.js のようなフレームワークを使って、効率的にスタブ・モックを使う方法を話してくれる模様。(それらのことを Isolation frameworks と表現されている模様)

5.2 Faking modules dynamically

Jestなどのフレームワークで読み込んだモジュールなどを動的にモック、スタブ化することについての概要が語られている。

その中で、コマンドクエリ分離の原則について言及があり、以下の Martin Fowler のブログが紹介されていた。

https://martinfowler.com/bliki/CommandQuerySeparation.html

本来これらの動的なモックやスタブの機能を使わずに済めばよいのだが、既存のコードが直接モジュールを呼び出していたりすると困難なので使う機会は多い。

ちなみに、オニオンアーキテクチャやヘキサゴナルアーキテクチャについても記載があり、推奨され知恵た記事があったが現在は閉鎖中だった。

以下のような記事を読んでみると良いかも。

https://medium.com/ssense-tech/hexagonal-architecture-there-are-always-two-sides-to-every-story-bc0780ed7d9c

5.4 Object-oriented dynamic mocks and stubs

substitute.js が利用されているが、どうやらしばらく更新されていない様子。対抗馬として人気っぽい ts-auto-mock もどうやらもう新機能開発は止まっているようだし、swcとかだと使えない模様。

https://github.com/Typescript-TDD/ts-auto-mock

https://npmtrends.com/@fluffy-spoon/substitute-vs-ts-auto-mock-vs-ts-mocks

でも jest でもこういう書き方ができるなら、jestやvitestを使ったほうが良いかもね。ちょっと面倒だけど。

https://knmts.com/as-a-engineer-207/

5.6.1 You don't need mock objects most of the time

ここまでIsolation Frameworkの良いところを説明してきた作者が言ってきた大事なこと。

スタブを必要ないとは言わない。だけど。モックはユニットテストでスタンダードな手順みたいに使うのは違うぜ。って書いてある。

そもそもコード全体の5%もいかないくらいにしか使わないらしい。

「どの関数を呼んだ」ということをチェックしようとするけど、それはExit Pointではない。

bun913bun913

6 Unit testin asynchronous code

6.2.2 The Extract Adapter pattern

他のWeb API に対して fetch してその結果によって処理を変えるタイプの関数のテストについて。

これまでは「fetchした結果とそれを使うロジック」を分けて、重要なロジック部分を単体テストとして切り出す(fetchするところは統合テストのまま)。という手法を説明していた。

ここからは Adpter pattern により、fetch などの実行をアダプターとして別のモジュールに分けて、そこの関数だけが唯一fetchをインポートして使うモジュールにしている。

そのおかげで他の関数は jest を使って そのAdapterをスタブしたり、あるいはそのアダプターの返り値を引数として注入することによりテストがしやすくなるみたい。

そのような完全にコントロールできな依存は極力一箇所にまとめた上で、それをラップすることで変更も用意になるし、テストもしやすくなるよね。っていうことかな。

bun913bun913

6.4.2 Delaing with click events

例えばボタンをクリックした後に、「Click」みたいな表示がされる機能であれば、この「表示される」というのがシステムに期待するふるまいであるためここをちゃんと確認しようぜ的なことが書いてある。(UIの変化が Exit Pointだよねっていう)

このあたりはReact のテストでも同じようなことが言えると思っていて、コンポーネントのテストをするときにはユーザーのイベントに対するシステムの挙動みたいなのを確かめる必要があるよね。っていう感じ。

7 Trustworhry tests

テストについて以下の3つの特性を持っている必要があると作者は述べている。

  • Trustworthiness(信頼性)
    • テスト自体がバグを持たず、正しいことをテストする
  • Maintainablity (保守性)
    • 保守性が良くないと継続的にテストを書き続けることができない
  • Readablitiy(可読性)
    • ただ単にテストが読めるということではなく、テストが間違っていたらそれを見つけ出すことあできるということ。

7章では信頼性、8章では保守性、9章では可読性を話すようである。どれか一つでも欠けるとみんなの時間を失いかねない的な感じのことが書いてある。

7.2 Why tests fail

理想的にはテストが失敗するときは良い理由で失敗して欲しい。
良い理由はもちろん本当のバグがプロダクションコードにあったことを発見すること

このあたりは単体テストの考え方/使い方 にも偽陽性的な形で書かれていたな。

バグが多い不安定なテストを見つける良い手段の一つがTDDであるとのこと。

7.3 Avoiding logic in unit tests

テストコードの中に if とか switch とか書かない方が良いことが書かれている。これも単体テストの使い方でも語られていたな。

ここの節では他にも以下のようなテクニックが紹介されていた。

  • 動的に作った期待値によるアサーションは避けてできればハードコーディングしよう
    • 「同じことを書くことになるのに違和感がある」とか考えていたら以下の記事を読もうなって書いてあって、心を読まれているかと思った

https://enterprisecraftsmanship.com/posts/dry-damp-unit-tests

// テストコード
expect(result).toBe("hello" + name)
// 本番コード
retrun "hello" + name
// これでは本番コードをなぞっただけなので、バグがあっても普通に通ってしまう可能性があるとのこと

また、ユニットテストの中にロジックは持たない。持っていいのはヘルパー関数とか、テストダブル用の関数とかに限定しよう。

7.4 Smelling a false sense of trust in passing tests

偽陽性のあるテストを可能な限り入れないようにレビューなどで取り除いていく。

その中で「こいうテストはあまり信用できない」というリストを書いてくださっているのだが、その中に Unit tests are mixed with flaky integration tests と書いてあった。

やはり単体テストとしてはいつ何回やっても同じ結果になって、かつ早く動くものと考える方が良さそうだ。

だから、DBとかも含めて状態に依存して壊れやすいものは単体テストとは分けて実行のタイミングはずらすべきかもしれないなと思った(単体テストはコードを保存するたびに走る、統合テストはコミット前や任意のタイミング)

7.4.3 Mixing unit tests and faly interation tests

上で書いた「ユニットテストは不安定な統合テストとは分けようね」っていうことが書いてあった。

同じフォルダにテストコードが配置されていたり、同じコマンドで同じタイミングで実行されたりするとちょっと怪しいとのこと。(やってます。ごめんなさい)

フォルダを分けて「ユニットテストはいつ実行しても成功する」状態を維持することで、素早いフィードバックをいつでも得られるというのがありそう。
(しかし、ビジネスコードの近くにテストがいてくれるの好きなんだよな。どういうふうにディレクトリを切るべきか難しいわ。)

// こんなのが近くにいてくれて好きだった
- domain/
  - users/
    - users.ts
    - users.spec.ts

ディレクトリを分けるのは簡単だし、ユニットテストと統合テストをファイル名で分けるのも簡単なんだが、うーん。どうするのがいいのかなぁ。

7.5.1 What can you do once you've found a flaky test?

フレイキーなテストが組織にとってコストであることを述べつつ、どのように向き合うかというヒントが書かれている。

  • 定義する
    • 「フレイキーとは」ということを同意しておく、10回やって10回とも成功するなら違うよねみたいな。
  • 欠陥があると判断されたテストは特別なカテゴリやフォルダに分けて、個別に実行できるようにする
    • 通常のCIから外して特別なディレクトリに入れて、別のパイプラインとして隔離することを推奨されている
    • 以下のゲームを行う
      • 修正
        • 可能であれば依存関係をコントロールしてフレーキーじゃないようにする
        • DBが必要ならDBびデータを挿入する
      • 変換
        • 依存関係を削除して制御することで、低レベルなテストにする
      • Kill
        • もたらす価値が保守コストに見合わなければ消してしまう
        • その方がいいこともある。テストを削除するより残しておく方がコストになるかもしれないから

7.5.2 Preventing flakiness in higher-level tests

もし外部依存が違う会社の管理するものである場合、非常に難しいものになる。そのような場合、いかを検討するべきとのこと。

  • ハイレベルなテストを排除してローレベルなテストでシナリオをカバーする
  • いくつかのハイレベルなテストをローレベルのテストのセットに変換する
  • 新しいテストを書くときに、パイプラインフレンドリーなテスト戦略に則っているか考慮する

これはあるよねぇ・・・

低レベルなテストでほとんどを確認しつつ、「無事にエンドポイントから返事が来ている」くらいを flakyなテストとして登録するべきなのかしら。

定常的に確認したいことではあるけど、自動テストでやることかと言われると違う気がするよねぇ。

難しい。どっちかっていうと、「相手から定常的にレスポンスを受け取れているか」というのをモニタリング側でするべきことなのかもしれないね。自動テストの範疇じゃない気がしてきた。

bun913bun913

8 Maintaineability

この章では保守性の話を進める模様

8.1.3 Changes in other tests

他のテストを変更したから、別のテストが壊れるみたいな作りにはしないことが基本コンセプトであるとのこと。

それな。まじで気をつけよう。

Constrained TEST ORDER

  • 作者が「制約されたテスト順序」と表現している症状について
    • 前のテストの実行が成功しないと、次のテストが成功しないとかの状態のこと。それはあかんよね。
    • 前のテストの状態を引き継ぐのではなく、ちゃんとヘルパー関数を作ろうぜっていうことだよね・・・

8.2 Refactoring to increase maintainability

8.2.1 Avoid testing private or protected methods

  • private や procted の関数に対してテストしたくなる時があるよね
    • だけどそれは「外から観察できる振る舞い」ではなく「内部の契約」になるよね
    • テストの目的としても「実装の詳細」や「内部の契約」に対してテストするのではなく、外から観察できる振る舞いに対してテストをすることなので、ここは無視していいのかなと思った
    • 逆にprivateに対してテストしたいというときにはモデリングや設計に対して見直しをするいいチャンスなのかもしれない

ここからprivate関数に対して「テストをする価値があるかもしれない」というときに対するいくつかの方法を書いてくださっている

そのまま書き写したいくらい良かったが、ネタバレになるので伏せておく。とてもいい・・・
今度発表しよう。

8.2.3 Avoid setup methods

  • こちらの作者は beforeEach などのセットアップ関数のファンではないとのこと
    • 前の章でも beforeEach にあれこれするよりも ヘルパー関数として切り出して、エディターを上から下に動かなくていいようにする方を好んでいたようだった
      • 確かにね。言われてみたらそうだなぁと思いました・・・
      • 重複を排除しようとして、describe に囲った test1test2 に対して同じ beforeEach を当てたとして、beforeEach でやっていることを変えたら どっちかのテストが失敗するとか影響を共有することにもなっちゃうもんなぁ

8.2.4 Use parameterized tests to remove duplication

これに関しては前にブログを書いたなぁ。

https://dev.classmethod.jp/articles/vitest-parameterized-test/

この本にも「同じシナリオに適用する」みたいなことを書いてくださっていて、とても共感している。

bun913bun913

9 Readablity

  • 可読性は「テストを書いた人と数ヶ月後あるいは数年後にそれを読まなければならない哀れな人との間を繋ぐ糸である」という表現が結構ツボった

9.1 Naming unit tests

作者が意識しているユニットテストの命名について述べられていた

  • unit of works のエントリポイント(またはテストされる機能の名前)
  • エントリポイントをテストするシナリオ
  • unit of works の Exit Point の期待される動作

これらの情報を ittest のタイトルにすることもできるし、 describe でネストさせてもよい。

// これでもいいし
test("verifyPassword, with a failing rule, returns error based on reul.reason")
// これでもいい
describe("veirifyPassword", () => {
  describe("with a failing rule", () => {
    it("returns error based on the rule.reason")
  })
})

読み手に情報が伝わるようにしようと思った。
↓こういうのはやめよう。

describe("verifyPassword", () => {
    describe("正常系", () => {
        test("正常な動作")
    })
})
  • 何を
  • どういう状況でしたら
  • どうなるのか

という期待される動作を書くような感じかな?

9.4 Setting up and tearing down

  • 作者はずっと beforeEach などのセットアップ関数などの乱用を危険視しているっぽい
describe("verifyServiceName", () => {
  let mockLog
  beforeEach(()) => {
    mockLog = Substitu.for<IComplicatedLogger>()
  })

  test("省略")
  test("省略2")
  // 省略
  test("省略3")
})

こんな感じでネストしていたら test("省略10") くらいまで行ったときにはそのテストの内容を見るだけでは、mockLogが何しているかわからないから上にスクロールしないといけなくなる。

あとこのモックを複数のテストで共有することになるから、影響も受けやすくなっちゃうなどの問題もあるよね。

それならヘルパー関数を用意して以下のようにtestの中で簡潔にわかるようにしてあげれば良さそう。

describe("verifyServiceName", () => {
  let mockLog
  test("verify, with logger & passing , calls logger with PASS", () => {
    const mockLog = makeMockLogger()
  })
})
bun913bun913

Developing a testing strategy

10. 1Common test types and levels

一般的に ユニットテスト、統合テスト、APIテストなどと呼ばれるテストを作者が以下の5つの指標でスコアづけしている。

  • Complexity(複雑性)
  • Flakiness(フレーキーさ?でいいんかな)
  • Confidence when passes(テストがパスしたときに得られる自信かな)
  • Maintainablity(保守性)
  • Execution speed(実行速度)

これらをみたときに、作者はユニットテストを「依存を含まない」と表現し統合テストを「本当のDBなどをいくつか含める」と表現する。

これらか見ても私が一般的に表現する「Dockerで立ち上げたDBを使ってModelやControllerのテストをする」ことは統合テストと言えそう(この定義なら)

確かにこれらは少なくともユニットテストとは役割が違うものとして分けて考えたり、実行するのが良さそうだなぁ。

10.2 Test-level antipatterns

冒頭から「テストレベルのアンチパターンは技術的なものではなく、組織的なものである」というブッ刺さる言葉から始まる。

まずはE2Eテストだけを実施するアンチパターンについて割と詳しめに書かれている。

色々と理由が書かれているが、その中で好きだったのは「ビルドの結果とかテストの結果がQAの部門に任せきりになり、開発者が気にしなくなること」みたいなことが書いてあった。そうなのよね。

10.2.2 The low-level-only test antipattern

省略しているが、上の節ではE2Eテストを完全に無視することは良くないと思うけど、最小化するべきてきなことを紹介してくれていた(E2Eテストオンリーのアンチパターンを添えて)

この節では逆に低いレベルのテストだけのアンチパターンについて書かれている。

もしくはユニットテストだけ開発者がやって、あとはQA部門がE2Eテストだけやるパターンとか・・・

10.3 Test recipes as a strategy

作者的にテストレベルなどのバランスを取るために提案するのが「テストレシピを使うこと」らしい。

10.3.1 How to write a test recipe

  • 最低でも2人でテストレシピを作る
    • できれば一人は開発者の視点、一人はテスターの視点で
    • テストの部門がなければ2人とも開発者でもOK
  • 各シナリオについて、どこのテストレベルで何を確かめるかをマッピングしていく
    • ぜひ本を買って図で見てほしい・・・
  • 機能を作り始める前がレシピを作る一番良いタイミング
  • テストレシピを公式な感じ(仰々しくみたいなかんじかな?)で取り扱わない
    • 成果物みたいな感じで使わない的なことかな
    • ユーザーストーリみたいに使わない的なことが書いてある

これはいいなぁ。まさにQAやテスト技術者の方とのコラボレーションって感じがする。

ペアリング作業だよな。「どこで」「何を」見るかっていうのがあらかじめ可視化されるのが非常に良い。

10.4 Managing delivery pipelines

多くの組織でユニットテスト・統合テスト・E2EテストをPull Request やリリースの前に実行して、リリースの可否を確かめている。これにかんして作者は言いたいことがある模様。

104.1 Delivery vs discovery pipelines

2つの種類のパイプラインを持つ。

  • Devivery pipeline
    • デリバリーをブロックするためのテスト。(失敗したらデリバリーできない的な)
    • Build → Unit tests → API/E2E Tests → Security Test → デプロイ
  • Discovery Pipeline
    • Lint → Code quality → Peformance → Load → レポート・ダッシュボードに反映
    • リファクタリングが必要?などの気づきをチームに与える

10.4.2 Test layer parallelization

早いフィードバックが大事なので、並列化すると良い的なお話。

例えばデリバリーパイプラインもユニットテストを待って統合テストではなく、同時並行で行うような。

早いフィードバック大事ですよね。開発者としてもテストが終わるまで5分かかるなら集中は切れるし、もう晩御飯のことしか考えられなくなるし。

bun913bun913

11. Integratin unit testing into the organization

組織にユニットテストの文化を作るためのTIPSをこの章に書いてくれている。(ここまでしてくれるのか・・・!?)

変化を組織にもたらすために取るべきメトリクスのヒントもあるので困ったらここを見ると良さげ。

12. Working with legacy code

12.1 Where do you start adding tests?

どこからテストコードを書き始めればいいか?何から始めればいいか?3つの要素がある。

  • Logical complexity (論理的複雑度)
    • サイクロマティック複雑度など。ツールで自動で収集できるやつ。
  • Dependency level(依存レベル)
    • コンポーネントの中にどれだけ依存を内包しているか
  • Priority (優先度)
    • プロジェクトの優先度

複雑度と依存レベルの2軸マトリクスで可視化するなど良さそう。

12.2 Choosing a selection strategy

  • Easy-first strategy
    • 簡単なものから先にやる戦略
    • 最初に手をつけやすいが、後になればなるほど依存をいっぱい含んでいるテストになるため難しくなっていく
  • Hard-first strategy
    • ↑の逆

12.3 Writing integration test before testing

もうタイトルから「それな」なことが書いてある。

そうなのよ。リファクタリングって「外部から見た挙動を変えずに内部ロジックを改善する」ことだと思うのよ。

だから「外部の挙動を変えずに」を担保するには先に統合テストがないと難しいはずなのよ。

ちなみにこちらの章でも 単体テストの考え方/使い方の7章を参照するように記載されている。

最後にAppendix としてjestでmockやstubを取り扱う具体的な方法について記載されていた。

このスクラップは2ヶ月前にクローズされました