Open21

テストについて(テスト思考・方針、Typescript/Goのテストコード、エラーハンドリングなど)

ピン留めされたアイテム
じん@酒酔いエンジニアじん@酒酔いエンジニア

題目

【全体項目】

  1. 【記事要約】テストコード導入奮闘記~私はこうやってプロジェクトにテストコードを導入しました~ - Qiita
  2. 【文献まとめ】テストコードとは?
  3. 【記事要約】【初心者向け】テストコードの方針を考える(何をテストすべきか?どんなテストを書くべきか?) - Qiita
  4. 【記事要約】初めてのテストコードの書き方
  5. 【記事要約】テストコードの改革を進めている話 | メルカリエンジニアリング
  6. 【記事要約】Golangのエラーハンドリングの基本 - Qiita
  7. 【記事要約】Go言語でユニットテスト, テストしやすいコードとモックを書く - Qiita
  8. 【記事要約】【考察】テストコードのきれいな書き方
  9. 【記事要約】速習 AAA : Arrange-Act-Assert による読みやすいテスト
  10. 【記事要約】テストコードをただ書くだけで満足している人々をこの記事でビンタしたい
  11. 【記事要約】TypeScript の単体テストで Jest 使おう
  12. 【記事要約】OpenStack Docs: Code testing policy
  13. 【記事要約】Angularのテストコードを学ぶ
  14. 【記事要約】【書き起こし】Scenario-Based Integration Testing Platform for Microservices – 森 健太【Merpay Tech Fest 2021】
  15. 【記事要約】Go言語でのテストの並列化 〜t.Parallel()メソッドを理解する〜
  16. 【論文要約】Code Readability Testing, an Empirical Study
  17. 【記事要約】3-1.コンポーネントのテスト - Angular After Tutorial(Zennのスクラップ)
  18. 【文献まとめ】React / JavaScript でのテストコードの概要
  19. 【記事要約】Goでモックを作成してテストをする
  20. 【記事要約】React Testing Libraryの使い方
じん@酒酔いエンジニアじん@酒酔いエンジニア

1. 【記事要約】テストコード導入奮闘記~私はこうやってプロジェクトにテストコードを導入しました~ - Qiita

テストコード導入奮闘記~私はこうやってプロジェクトにテストコードを導入しました~ - Qiita
https://qiita.com/papi_tokei/items/5960a4e031229fe44eb1

メリット

【メリット】 開発と動作確認の素早いサイクルを構築することで、開発効率、開発体験の向上

  • テストコードを動かす環境はリッチでなくて良い
  • テストコードをコードを実行する環境としてとらえると、コードを実装、動作確認というサイクルをこれまで以上に迅速に回すことができるようになる

【メリット】作成したテストコードは動作確認で使えるだけではなく、コードを修正した場合のリグレッション(後退)の確認としても使える

  • 修正した関数のインターフェイス(入力と出力の関係)が変わっていれば、エラーになるので、追加した修正が他のロジックに影響を及ぼす可能性がある

【メリット】開発効率と成果物の品質向上に貢献できる可能性

  • 開発効率については最初以降の2回目からのテスコード実装による指針が決まっているため,リファクタリングの後に,変更された部分が保管されているのか,さらなる観点による異常系のテストができる(自分の意見)

デメリット

【デメリット】テストコードの実装での工数がかかるのではないか

  • テストコードの根幹となる部分の環境構築,実装は一回で済む
  • リファクタリングを考えるのであれば,それに追加で実装,変更を行うことができる
  • テストコードの指針を考える必要がある
    • テストコード実装の工数は開発工数の40%以内とする(この数字は暫定であり、目安の数字)
      • これは開発工数の中で異常系のテストに対する無限ケースの量を限るため
    • 異常系、正常系のテストを少なくとも1つ以上作成する、ただし異常系はない場合も許容する
    • メンバー間で処理ロジックの優先度を認識合わせし、優先度が高いロジックには詳細にテストコードを記載する(例えば、課金処理など)

【デメリット】メンテナンスが必要になる

  • テストコード間でデータを共有しない
  • ロジック内部に依存するようなテストは書かない、入力と出力に着目したテスト(もしくはその関数のメイン処理にのみ着目したテスト)を書く
じん@酒酔いエンジニアじん@酒酔いエンジニア

2. テストコードとは?

自分の書いたコードが想定通りに動いているか確認するためのコード[1][2]

テストコードは,実際に動くコードが「適切に,正しく」動くのかどうかを確かめるためのコードです.この「適切に,正しく」というのはテストコードにおいて,どういったことなのでしょうか.この記事においては,テストコードが通ることは

仕様どおり実装されていること

です.ホワイトボックステスト[3]の指向でテストコードの実装をしている人が多いようですが,本来は,ブラックボックステスト[4]の指向の両方の指向を含めて実装するべきなのです[5]
簡単にまとめると,

「適切に,正しく」仕様通りに実装できているのかを確かめるためのコードで,ホワイト/ブラックボックステストの観点が両方含まれているもの

といえるでしょう.

脚注
  1. なぜテストコードを書くのか? ↩︎

  2. 【初心者向け】よく分からん「テストコード」についてまとめてみた。 | Tech Beginner ↩︎

  3. システム内部の構造を理解した上でそれら一つ一つが意図した通りに動作しているかを確認する、プログラムのテスト方法。プログラムの全ての部分が、プログラム記述者の意図通りに動作していることを確認するテストである。システムの機能よりも内部構造の整合性を重視したテストとも言える。(参考:ホワイトボックステストとは -IT用語辞典 e-Words↩︎

  4. ソフトウェアやシステムのテスト手法の一つで、テスト対象の内部構造を考慮に入れず、外部から見た機能や振る舞いが正しいかどうかだけを検証する方式。(参考:ブラックボックステスト(機能テスト / FT)とは - IT用語辞典 e-Words↩︎

  5. テストコードをただ書くだけで満足している人々をこの記事でビンタしたい - Qiita ↩︎

じん@酒酔いエンジニアじん@酒酔いエンジニア

3. 【記事要約】【初心者向け】テストコードの方針を考える(何をテストすべきか?どんなテストを書くべきか?) - Qiita

【初心者向け】テストコードの方針を考える(何をテストすべきか?どんなテストを書くべきか?) - Qiita
https://qiita.com/jnchito/items/2a5d3e15761fd413657a

この記事の前提

  • 基本的にドキュメントは書かない
  • 開発メンバーは少数(最小で1名)
  • 不具合が出た場合は、すぐに修正してすぐにリリース可能

戦略面

なぜテストを書くのか、どういうときにテストを書くのか

  • テストコードはアプリケーションの命綱、安全ネット、防弾チョッキ
  • コマンド一発でこれまで書いてきたコードの動作確認ができる! 速いし、楽ちん!!
  • 将来、自分のコードをメンテするかもしれない他のメンバーのために書く
  • 将来、Railsやライブラリ(gem)をアップデートするときのために書く
  • 不具合を修正するときに書く

テストコードの対象になるコード

  • 今の自分、または将来の自分が信頼できないところ
  • 将来、コードの他の部分を変更したときに、不具合が発生しそうな予感がするところ
  • セキュリティ上、重要なところ
  • 不具合があると致命的なところ
  • そのシステムの重要度の高いユースケース
  • 複雑なロジックやトリッキーなコードを書いてしまったところ
  • やむを得ずモンキーパッチを当てたところや、問題回避のために無理矢理なハックをしたところ、非公開APIを利用して実装したところ
  • 手作業で何度もテストするより、自動化した方が明らかに速いところ
  • 例外処理

テストコードの対象にならないコード

そもそもの話として、無理にカバレッジ100%を目指す必要はない。
価値の低いテストコードは開発効率を下げる原因になる(テストの実行が遅くなったり、仕様変更時のテストコード修正が大量発生したりする)。

いったん後回しにできるケース

  • 仕様がまだ固まっていない場合
  • 本当にリリースを急いでいる場合

自動テストを諦めるケース

スキルによってはかけないことや手動テストを採用する規約上,場合がある

その場合でも以下の点に気を付けたい。

  • 本当に自動化する方法がないのかどうか、周りの同僚に確認・相談する
    リリース前は必ず手動テストを実施する
  • テストを自動化できていない箇所を明文化する(または何らかの方法で、手動テストが必要であることを他のメンバーがわかるようにする)

戦術面

テストコードを書くときに意識すべきこと

  • ヌケ・モレのない、必要最小限のテスト項目を抽出する
  • データの更新や削除を伴うテストを書く場合は、before/afterの両方を検証する。(afterだけ検証して満足しない)
    • 更新なら、AがBに変わったことを検証する(つまり、最初はAだったことを検証しておく)
    • 削除なら、Aが消えたことを検証する (つまり、最初はAが存在していたことを検証しておく)
    • afterだけ検証するテストコードは「最初からBで、なおかつ何も更新できていなかった可能性」や「最初からAが存在せず、なおかつ削除はできていなかった可能性」を否定できない
  • 上から下へ、素直に読み下せるテストコードを書く
    • 頭の中で変数を何度も置換したり、視線が頻繁に上下するものを書かない
  • テストコード全体がなるべく1画面に収まるようにする
  • 複雑なテストコードのデメリットを理解する
    • 複雑なテストコード = 過度にDRYなテストコードや、if文やループ処理が頻繁に登場するテストコード、テスティングフレームワークの機能を多用しすぎるコード、など
    • テストコード自体がロジカルになり、「テストコードのバグ」が発生する。「テストコードのテスト」が必要になるのは本末転倒。
    • テストコードのロジックにバグがあると誤検知が発生する。本来失敗すべきテストがパスしたりするのはテストとして致命的。なので、ロジカルなテストコードは避ける。
    • 第三者が見たときに、対象のロジックがどんな仕様なのかぱっと理解できなくなる。
      アプリ側のコードを見て仕様がぱっとわからなかったのでテストコードを見てみたのに、テストコードを読んでも意味がわからない、といった状況になると悲惨。
  • テストコードのコーディングルールを厳しくしすぎない
    • 「必ずsubjectを使う」「itの中のexpectは必ず1つだけにする」といった制約を設けない
    • 制約が厳しいと、ひねくれたテストコードが出来上がったり、テストコードを書くのにやたらと時間がかかったりする
    • コーディングルールに沿ったテストコードを書くために頭を悩ますぐらいなら、自由にテストコードを書いて、どんどんアプリ側のコードの実装を進めた方が良い
  • 実際のユースケースが想像できるテストデータを用意する
    • 「あああ」とか「テスト1」ではなく、「西脇太郎」や「株式会社イグザンプルドットコム」にする
    • 新しくプロジェクトに参加したメンバーにとっては、リアルなテストデータの方が圧倒的に仕様がわかりやすい
    • ユーザの名前付けに迷ったらアリスとボブに登場してもらう
  • いつでも、どこでも、誰がどんな順番で何度実行しても、パスするテストを書く
    実行順序や実行環境、データベース上のid、システム日時といった要素に依存したテストを書くのはNG
  • privateメソッドは直接テストしない
    • テストしたいprivateメソッドを通るような条件下で、publicメソッドをテストする
  • AAA(Arrange、Act、Assert)を意識したテストコードを書く
    • Arrange(準備)、Act(実行)、Assert(検証)

テストコードのコードレビュー

  • Pull Requestをレビューする際は、アプリ側のコードの変更内容に対して、テストコードが適切に追加/修正されていることを確認する

    • 明らかにテストコードが不足している場合はその旨コメントする
  • 加えて、上記の「テストコードを書くときに意識すべきこと」で挙げた観点でもテストコードをレビューする

    • 怪しい点を見つけた場合はその旨コメントする
  • コードレビューする際は自分のことは棚に上げて良い

    • 「これ、僕もできてないからな・・・」と指摘を躊躇するのはNG

テストの実行スピードについて

プロジェクトの初期は愚直で素直なテストコードを書く。許容しづらい遅さになってきたら、そのときにボトルネックを調査し、対策を考える

他人の書いたテストコードが読みづらい場合

  • プロジェクトメンバーでのコードレビューを行い,すり合わせをする
  • プルリクにて指摘をする
じん@酒酔いエンジニアじん@酒酔いエンジニア

4.【記事要約】 初めてのテストコードの書き方

初めてのテストコードの書き方
https://zenn.dev/zundaneer/articles/f07f477464d305

テスト対象コード(設計別)

  • MVC
    MVC の場合はコントローラー層が最適です。

  • ドメイン駆動設計
    ドメイン駆動設計の場合はアプリケーションサービス層が最適です。

  • クリーンアーキテクチャ
    クリーンアーキテクチャの場合はユースケース層が最適です。

テストコードの置き場所

テストの置き場所に関しては、意見が色々割れます。言語的な理由で置き場所が制限される可能性もあります。

一般的には以下の 3 つの候補があります。

<tsの場合>

  • src ディレクトリと同じ階層に test ディレクトリを作る
  • テスト対象のファイルが有るディレクトリ内に__test__ディレクトリを作る
  • テスト対象のファイルが有るディレクトリにそのままテストファイルを入れる(.spec.ts のような拡張子になる)

<golangの場合>

  • 対象のソースコードと同じ階層にxxx_test.goを作成する必要がある
じん@酒酔いエンジニアじん@酒酔いエンジニア

5. 【記事要約】テストコードの改革を進めている話 | メルカリエンジニアリング

テストコードの改革を進めている話 | メルカリエンジニアリング
https://engineering.mercari.com/blog/entry/20230623-f8bcbed100/

メルカリでの状況

  • 複数のデータ集計を伴う処理は前提条件が複雑になりがちで通常のテストだけでは動作を担保することが難しく、時折トラブルにより集計データの修正や整合性の確認をエンジニアが手作業で行うケースが発生し、品質面の改善が必要な状況
  • 変更したコードが既存の動作を破壊していないことを担保するテストコードは非常に重要ですが、前述した複雑さによりテストの網羅性が低くまたテストの可読性が低いことも相まって安全なリアーキテクチャーに自信を持てていないのが現状

解消のためのアプローチ

テスト粒度とその責務を分類する

  • googleの昔のブログでは,「Small, Medium, Largeと3つに分類しそれぞれでどの機能をどこまで使うのかについて定義」をしていた.
  • テスト範囲が狭い順に「ユニット」「コンポーネント」「インテグレーション」という説明的な命名を採用し、解釈のバラつきはドキュメントの作成と丁寧な説明でカバーすることにした
  • テスト

記述スタイルを統一する

値の検証にはアサーションを使用する

golangには比較検証し,エラーを吐くことのできるアサーションの標準パッケージもあるhttps://go.dev/doc/faq#assertions

https://pkg.go.dev/github.com/stretchr/testify/assert

原則としてテーブル駆動で記述する

複雑なセットアップは以下のように各ケース毎にfuncで定義することにしました
参考:https://ema-hiro.hatenablog.com/entry/2018/08/10/032411
参考:https://github.com/golang/go/wiki/TableDrivenTests

テストでも命名を省略しない

まず最初のケースですが下記サンプルのk, vとは何でしょうか?testCasesはテーブル駆動のケース定義と考えられるので、大概map[string]struct{ … }型と類推はできますが必ずしもそうとは限りません。vという変数は生存期間が極短い使い捨て変数として使いたくなる名前なので、下に続く検証部分が長くなった中でうっかり別の用途として使ってしまうと混乱を引き起こすかもしれません。

for k, v := range testCases {
    t.Run(k, func(t *testing.T) {.

代わりにname, tc等とするとどうでしょう。明らかに理解が容易になったことがわかるかと思います。k, vとタイプの手間はほぼ変わらないのでこの僅かな手間は惜しまない方がよいです。

なお、必ずしも直接的な命名を採用する必要は無く、可読性が担保できるのであればどのようなものでも問題ありません。IDEやVSCodeの自動生成の場合テストケースは tt とされることが多いようなのでこういった慣例やチームの標準に倣うのもよいでしょう。

for name, tc := range testCases {
    t.Run(name, func(t *testing.T) {.

次のケースです。このretはreturn(戻り値)から命名されたのだと思いますがさて何が入っているのでしょうか?errorかもしれませんし新規発行されたユーザーIDか、はたまたUser型のstructのポインターかもしれません。

ret := createUser(ctx, userName)
assert.NotEmpty(t, ret)

これもやはり説明的な変数を採用し、何が入っているか一目でわかるようにするべきです。

userID := createUser(ctx, userName)
assert.NotEmpty(t, userID)

流石にプロダクションコードでこういった命名がされることはないのですが、特に小規模なテストではついやってしまいがちです。しかし、書いた時点では正しく認識できていても2,3日もすれば書いた当人すら忘れてしまいますし、コードは日々成長していくものなので少々の手間は惜しまず、将来に渡って理解容易性を損ねないよう常に細部まで気を配るべきです。

じん@酒酔いエンジニアじん@酒酔いエンジニア

6. 【記事要約】Golangのエラーハンドリングの基本 - Qiita

Golangのエラーハンドリングの基本 - Qiita
https://qiita.com/immrshc/items/13199f420ebaf0f0c37c

ポイント

fmt.Errorfの問題点

しかし、これで問題なしかというとそうではありません。理由はfmt.Errorfは元のerrorインターフェースを実装するある型と値を消失させるからです。
fmt.Errorfの実装を見ると、内部でerrors.Newを呼び出していることがわかります。

// Errorf formats according to a format specifier and returns the string
// as a value that satisfies error.
func Errorf(format string, a ...interface{}) error {
    return errors.New(Sprintf(format, a...))
}

errors.Newとは、errorインターフェースを実装したエラーメッセージだけを持つ構造体を返します。

// cf. https://golang.org/pkg/errors/#New

// New returns an error that formats as the given text.
func New(text string) error {
    return &errorString{text}
}

// errorString is a trivial implementation of error.
type errorString struct {
    s string
}

func (e *errorString) Error() string {
    return e.s
}

しかし、ライブラリによって、独自のerrorインターフェースを実装した構造体を定義して、エラーメッセージ以外の情報を付与していることがよくあります。これによって、受け取った(errorインターフェース型に抽象化された)エラーを型アサーションして元の型に戻して、付加された値を取り出すことができます。fmt.Errorfは上記のerrorString構造体に作り直してしまうのでこれをできなくしてしまいます。

例えば、JSONのKey, Valueの対応はSliceで表現することはできないのでUnmarshalするとエラーになりますが、そのエラーはJSONのバイト列におけるエラーが起こった位置やUnmarshalしようとした型の名前をエラーメッセージ以外を持っています。

具体的には、以下のUnmarshalTypeErrorがerrorインターフェースを実装しており、それが返ってきます。

// cf. https://golang.org/pkg/encoding/json/#UnmarshalTypeError

// An UnmarshalTypeError describes a JSON value that was
// not appropriate for a value of a specific Go type.
type UnmarshalTypeError struct {
    Value  string       // description of JSON value - "bool", "array", "number -5"
    Type   reflect.Type // type of Go value it could not be assigned to
    Offset int64        // error occurred after reading Offset bytes
    Struct string       // name of the struct type containing the field
    Field  string       // name of the field holding the Go value
}

func (e *UnmarshalTypeError) Error() string {
    if e.Struct != "" || e.Field != "" {
        return "json: cannot unmarshal " + e.Value + " into Go struct field " + e.Struct + "." + e.Field + " of type " + e.Type.String()
    }
    return "json: cannot unmarshal " + e.Value + " into Go value of type " + e.Type.String()
}

実際に、対応していない型にUnmarshalするとUnmarshalTypeError型に型アサーションして、エラーメッセージ以外を取り出してみます。

import (
    "encoding/json"
    "fmt"
)

var jsonData = []byte(`
{
    "name": "user",
    "password": "pass"
}
`)

func main() {
  // []string型はJSONのKey, Valueの内容を持つことができないのでUnmarshalするとエラーになる
    valueOfInvalidType := make([]string, 0)
    err := json.Unmarshal(jsonData, &valueOfInvalidType)
    switch err := err.(type) {
    case *json.UnmarshalTypeError:
        fmt.Printf("type: %s\n", err.Type)
        fmt.Printf("offet: %d\n", err.Offset)
        fmt.Printf("Error(): %s\n", err)
    default:
        fmt.Println(err)
    }
}
$ go run main.go
type: []string
offet: 2
Error(): json: cannot unmarshal object into Go value of type []string

このようにしたくとも、fmt.Errorfが既存のError()の結果以外の情報を切り捨ててしまうので、以下のような問題に直面します。

  • errorインターフェースの元の型に応じて条件分岐ができなくなる
  • errorインターフェースの元の型の持つコンテキスト情報が消失する

これらの問題を解決する手段でかつ、現在(2019/01/22)でデファクトとなっているエラーパッケージとして、pkg/errorsがあります。

pkg/errorsを使う

では、具体的にpkg/errorsで上記問題を解決できるのかというと、以下のWrapCauseを用います。

func Wrap(err error, message string) error
func Cause(err error) error

まず、Wrapを使うことで元のerrorインターフェースを実装した型と値を保持して、エラーメッセージだけコンテキスト情報を追加した新しいものにできます。

if err := json.Unmarshal(data, &jsonMap); err != nil {
    // failed to unmarshal src.json: unexpected end of JSON input
        return nil, errors.Wrap(err, "failed to unmarshal src.json")
}

この結果だけ見れば、fmt.Errorf("failed to unmarshal scr.json: %s", err)した結果と同じですが、%+vでフォーマットするとStackTraceを出力することもできます。

// cf. https://godoc.org/github.com/pkg/errors#hdr-Formatted_printing_of_errors
if err := json.Unmarshal(data, &jsonMap); err != nil {
    fmt.Printf("%+v", err)
}
main.unmarshalToMap
        /go/src/github.com/shoichiimamura/error-handling-example/main.go:30
main.main
        /go/src/github.com/shoichiimamura/error-handling-example/main.go:18
runtime.main
        /usr/local/go/src/runtime/proc.go:198
runtime.goexit

さらにCauseを使うことでWrapされたエラーから元のerrorインターフェースを実装した型と値を取り出すことができます。
より厳密に言うと、causerインターフェースを実装していない一番最後のerrorインターフェース型を取り出します。

// cf. https://github.com/pkg/errors/blob/master/errors.go#L269
func Cause(err error) error {
    type causer interface {
        Cause() error
    }

    for err != nil {
        cause, ok := err.(causer)
        if !ok {
            break
        }
        err = cause.Cause()
    }
    return err
}

これを使うと、以下のように元のエラーの型に応じた条件分岐が可能になります。

switch err := errors.Cause(err).(type) {
case *json.UnmarshalTypeError:
  fmt.Println(err.Offset)
case *json.InvalidUnmarshalError:
  fmt.Println(err.Type)
default:
  fmt.Println(err)
}

このCauseでエラーの型に応じた条件分岐ができることがわかりました。

しかし、実際のAPIを持つアプリケーションサーバーの開発では、エラーの種類に応じてHTTPステータスコードを変えたいことがよくあります。これを実現するためには、pkg/errorsを拡張したエラーパッケージを作る必要があります。次はpkg/errorsを利用して、エラーの種類の応じたHTTPステータスコードを返す実装をしてみます。

じん@酒酔いエンジニアじん@酒酔いエンジニア

7. 【記事要約】Go言語でユニットテスト, テストしやすいコードとモックを書く - Qiita

Go言語でユニットテスト, テストしやすいコードとモックを書く - Qiita
https://qiita.com/hiroyky/items/4a9be463e752d5c0c41c

Go言語でのユニットテストの書き方

  • 関数はTestXxxxという名称で引数に*testing.T型を取る必要がある。
    特別に関数で日本語を使用する場合もある。
func TestGetUser_正常系(t *testing.T) {
}
  • 以下のような場所にテストコードの配置する
├── calc.go
└── calc_test.go
  • アサーションを使用する(よく使用されるものを抜粋)
// 値が等しいかどうかを確認
assert.Equal(t, expected, actual)

// 値がNilであるか確認
assert.Nil(t, actual)

// 値がNilでないか確認
assert.NotNil(t, actual)

// エラー発生であることを確認
assert.Error(t, err)

// エラー発生でないことを確認
assert.NoError(t, err)

// スライスの要素が同じ事を確認、ただし順序を問わない
assert.ElementsMatch(t, expectedSlice, actualSlice)

テストしやすいコードの書き方

テストしやすいコードとは?

書くときに心がけると良いこと(TDDの思想に近い)

  • 関数に外部依存の要素を作らない.
    • 外部依存の要素は引数で注入できるようにする.
    • 外部依存を含む関数はまとめる.
  • 一つの関数でたくさんの処理をしない.
    • これは保守性の高いコードを書く上で基本.
  • モック化できるようにコードを書く

モック化できるコード

  • ユニットテスト時においてモック化とはテストに依存する外部依存の関数の動作をテストのためにふるまいを置き換えること
    • 例えば,テスト対象の関数内でデータベースに接続してユーザ情報を取得する処理を行っている場合,モック化することでユニットテストでは実際にはデータベースに接続しませんが,接続した体で値を返してテストを進めると言ったこと

Go言語においてモック化できるコード

  • 構造体に関数を実装する
  • その構造体のインターフェイスを外部公開する

構造体の置き換えはできませんが,インターフェイスであれば同じ入出力の関数を持てば置き換えができます.

モックの作成

モックの生成コマンド

モックの生成にはmockgenコマンドを使用します.
例えば,以下のようにすることで,user_db.goのモックがmocks/配下に作成されます.最後のpackageオプションで生成するモックが属するパッケージを指定しています.

go get github.com/golang/mock/mockgen
mockgen -soruce user_db.go -destination mocks/user_db.go -package mocks

※ 【じん追記】go1.17からgo getでのパッケージのインストールは非推奨となっています。
上記の場合、go install github.com/golang/mock/mockgen@latestで推奨の形となる。
参考:go1.17からgo getでのパッケージ導入が非推奨になった
https://hodalog.com/use-go-install-instead-of-go-get/

モックの使い方

gomock packageの公式リファレンス
https://pkg.go.dev/github.com/golang/mock/gomock

具体的なコードについては記事内参照

じん@酒酔いエンジニアじん@酒酔いエンジニア

8. 【記事要約】【考察】テストコードのきれいな書き方

【考察】テストコードのきれいな書き方
https://qiita.com/tkts_knr/items/5978377dbcbc925248e8

考察

AAAパターン

AAAパターンの詳細については、9. 【記事要約】速習 AAA : Arrange-Act-Assert による読みやすいテスト を参照。

  • AAAパターンが良い理由:テストをするために必要な情報 を整理すると、その3つに分類される

ポイント

  • 前提条件 が分かりやすい(テスト実施内容 の差分も含む)
  • 想定結果 が分かりやすい(テスト実施結果 の差分も含む)
  • モック や スタブ を利用している場合、想定したものが渡されているのか を検証したい(verify)
  • テスト工数(コスト) を減らしたい

方法としては、

  • テストのパターン(「前提条件」と「想定結果」)を書き出す
  • 「想定結果」が同じものをグループ化する
  • グループ化した中で「検証内容」が異なるものは別のグループにする
  • グループ化した中で「前提条件」が明確に異なるものは別のグループにする
  • テストコードを作っていく

→ 同じものは極力まとめる

まとめ

  • テストをする目的を明確にする
  • テストコードは、できる限りまとめる
    • そのためには、下記の順序でテスト内容を決める
      1. テストのパターン(「前提条件」と「想定結果」)を書き出す
      2. 「想定結果」が同じもの or 「検証内容」が同じもの をグループ化する
      3. グループ化した中で「検証内容」が異なるものは別のグループにする
      4. グループ化した中で「前提条件」が明確に異なるものは別のグループにする
    • グループ化/テストコード 作業にあたり下記を注意する
      1. テストの目的が満たせていること
        • 検証したい内容がテストコード内で実施されている
        • 条件によって発生し得るパターンが一度はテストされている
      2. 何をテストしたいのか(前提条件/想定結果)が分かりやすいこと
      3. 業務処理 の変数(定数) を検証結果として利用しないこと
      4. テストコードの中で 分岐処理が発生しないようにグループ化すること
じん@酒酔いエンジニアじん@酒酔いエンジニア

9. 【記事要約】速習 AAA : Arrange-Act-Assert による読みやすいテスト

速習 AAA : Arrange-Act-Assert による読みやすいテスト
https://qiita.com/inasync/items/e0b54e62784710c4b42d

AAAパターン

AAA パターンについては Microsoft Docs でも 単体テストの基本 や 単体テストのベストプラクティス で紹介されています:

単体テスト メソッドの Arrange セクションでは、オブジェクトを初期化し、テスト対象のメソッドに渡されるデータの値を設定します。
Act セクションでは、設定されたパラメーターでテスト対象のメソッドを呼び出します。
Assert セクションでは、テスト対象のメソッドの操作が予測どおりに動作することを検証します。
https://docs.microsoft.com/ja-jp/visualstudio/test/unit-test-basics?view=vs-2019#write-your-tests

【追記】
AAAとは

  • arrange :『準備』
    • テスト用のデータの作成
  • act :『実行』
    • テストしたい関数やメソッドを実行すること
  • assert :『検証』
    • 実行の結果得られた値や変化が期待したものであるかどうかを確認すること

→ 各関数に対して、テスト関数を用意し、そこに適したテストデータを作成・挿入した上で、アサーションでの検証を行うことが重要となる。

じん@酒酔いエンジニアじん@酒酔いエンジニア

12. 【記事要約】OpenStack Docs: Code testing policy

OpenStack Docs: Code testing policy
https://docs.openstack.org/fuel-docs/newton/devdocs/develop/nailgun/development/code_testing.html

memo(日本語は英語の後にdeeplで。)

  • The test for specific code change must fail if that change to code is reverted, i.e. the test must really cover the code change and not the general case. Bug fixes should have tests for failing case.
  • The tests MUST be in the same patchset with the code changes.
  • The extreme cases are "hot-fix / bug-fix with Critical status" and "patching during Feature Freeze (FF) or Hard Code Freeze (HCF)"
  • Test coverage should not be decreased.
  • Consider usage of the unit testing if it is performed within one of the layers or implementing mock objects is not complicated.
  • The tests have to be isolated. The order and count of executions must not influence test results.
  • Tests must be repetitive and must always pass regardless of how many times they are run.
  • Parametrize tests to avoid testing many times the same behaviour but with different data.
  • Follow DRY principle in tests code. If common code parts are present, please extract them to a separate method/class.
  • Consider implementing performance tests for the cases:
    • new handler is added which depends on number of resources in the database.
    • new logic is added which parses/operates on elements like nodes.

メモ

  • 特定のコード変更に対するテストは、そのコード変更が元に戻された場合に失敗しなければならない。バグ修正には、失敗した場合のテストがあるべきです。
  • テストはコード変更と同じパッチセットに含まれなければならない。
  • 極端な例としては、「ホットフィックス / バグフィックスで Critical ステータスのもの」や「フィーチャーフリーズ (FF) やハードコードフリーズ (HCF) 中にパッチを当てたもの」などがあります。
  • テストカバレッジを下げるべきではない。
  • 単体テストがレイヤーの1つで実行される場合や、モックオブジェクトの実装が複雑でない場合は、単体テストの利用を検討する。
  • テストは分離しなければならない。実行の順番や回数がテスト結果に影響を及ぼしてはならない。
  • テストは繰り返し実行され、何度実行しても必ずパスしなければならない。
  • 同じ振る舞いを異なるデータで何度もテストしないように、テストをパラメータ化する。
  • テストコードは DRY の原則に従ってください。共通するコード部分がある場合は、それを別のメソッド/クラスに抽出すること。
  • ケースに対するパフォーマンステストの実装を検討してください:
    • データベースのリソース数に依存する新しいハンドラを追加する。
    • ノードのような要素を解析/操作する新しいロジックが追加された。
じん@酒酔いエンジニアじん@酒酔いエンジニア

14. 【記事要約】【書き起こし】Scenario-Based Integration Testing Platform for Microservices – 森 健太【Merpay Tech Fest 2021】

【書き起こし】Scenario-Based Integration Testing Platform for Microservices – 森 健太【Merpay Tech Fest 2021】
https://engineering.mercari.com/blog/entry/20210928-mtf2021-day5-3/

Introduction

なぜ、社内向けのインテグレーションテストプラットフォームを作っているのか?
前提:

  • 開発当初からマイクロサービスアーキテクチャを採用
  • 複数のチームがそれぞれ別のサービスを並行して開発・運用
  • すべてのサービスはGoで実装
    マイクロサービスでは、グローバルなテストの難しさがある。

マイクロサービスのテスト戦略について

  • Unit tests
    • テスト可能な最小単位のコードに対するテスト
    • いわゆる単体テスト
    • Go test(関数単位のユニットテスト)
  • Component tests
    • テスト範囲を1つのサービスに限定したテスト
    • 外部のデータストアやほかのマイクロサービスへの通信は実際には行わず、テスト範囲を1つのマイクロサービスに限定してテスト
    • Go test(MockやStubを使った1サービスに閉じたテスト)
  • Contract tests
    • Consumer が期待する仕様を見たいしているか確認するテスト
    • あるサービスに依存しているサービスであるコンシューマが期待しているAPIの仕様を、依存先であるプロバイダが満たしているかどうかというものをテスト
    • Pact(schema-first development)←これなに??
  • Integration tests
    • サービス間の通信経路や相互作用の検証も含めたテスト
    • 実際に外部との通信を行うことで、通信経路や相互作用の検証も行うテスト
    • Postman
  • End-to-end tests
    • システム全体に対するテスト
    • 端から端までシステム全体を使ったテスト
    • Appium, XCUITest, Crypress, etc.

Test Runner

Continuous Integration

Testing in the Real World

Conclusion

じん@酒酔いエンジニアじん@酒酔いエンジニア

15. 【記事要約】Go言語でのテストの並列化 〜t.Parallel()メソッドを理解する〜

Go言語でのテストの並列化 〜t.Parallel()メソッドを理解する〜
https://engineering.mercari.com/blog/entry/how_to_use_t_parallel/

まとめ

  • -p フラグによる指定は、複数のパッケージのテストを並列に異なるプロセスとして実行することを指定する。-p=1 では、パッケージが一つずつ実行されることになる。
  • t.Parallel() メソッドの呼び出しで、パッケージ内のトップレベルのテスト関数やサブテスト関数が並列に実行されることになる。
  • t.Parallel() メソッドを呼び出している(トップレベルを含む)テスト関数は、その親のテスト関数の呼び出しが戻るまで、 t.Parallel() メソッド呼び出しによる一時停止の状態から処理を再開しない。
  • t.Parallel() メソッドによる並列レベルは、デフォルトで GOMAXPROCS の値である。明示的に変更するには、 -parallel フラグで指定するか、環境変数 GOMAXPROCS で設定する。
  • テスト関数内での後処理は、t.Cleanup メソッドもしくは defer 文を使うかは、含まれるサブテスト関数がt.Parallel()メソッドを呼び出しているかいないかで使い分ける必要がある。
  • t.Parallel()メソッドを使っていても、複数のパッケージのテストが同時に一つのテストプロセス内で実行されることはない
じん@酒酔いエンジニアじん@酒酔いエンジニア

16. 【論文要約】Code Readability Testing, an Empirical Study

Code Readability Testing, an Empirical Study
https://www.researchgate.net/publication/299412540_Code_Readability_Testing_an_Empirical_Study

abstruct

Abstract—Context: One of the factors that leads to improved code maintainability is its readability. When code is difficult to read, it is difficult for subsequent developers to understand its flow and its side effects. They are likely to introduce new bugs while trying to fix old bugs or adding new features. But how do software developers know they have written readable code? Objective: This paper presents a new technique, Code Readability Testing, to determine whether code is readable and evaluates whether the technique increases programmers’ ability to write readable code. Method: The researcher conducted a field study using 21 software engineering master students and followed the Code Readability Testing with each student in four separate sessions evaluating different “production ready” software. After the observations, a questionnaire evaluated the programmer’s perspective. Results: By following Code Readability Testing, half of the programmers writing “unreadable” code started writing “readable” code after four sessions. Programmers writing “readable” code also improved their ability to write readable code. The study reveals that the most frequent suggestions for increasing code readability are improving variable names, improving method names, creating new methods in order to reduce code duplication, simplifying if conditions and structures, and simplifying loop conditions. The programmers report that readability testing is worth their time. They observe increases in their ability to write readable code. When programmers experience a reader struggling to understand their code, they become motivated to write readable code. Conclusion: This paper defines code readability, demonstrates that Code Readability Testing improves programmers’ ability to write readable code, and identifies frequent fixes needed to improve code readability.

(日本語)
概要-コンテキスト コードの保守性を向上させる要因のひとつに、コードの読みやすさがある。コードが読みにくいと、後続の開発者がその流れや副作用を理解するのが難しくなる。古いバグを修正したり、新しい機能を追加しようとしているうちに、新たなバグが発生する可能性が高い。しかし、ソフトウェア開発者は、自分たちが読みやすいコードを書いたとどうやって知るのだろうか?目的 本稿では、コードが読みやすいかどうかを判定するための新しい技法「コード可読性テスト」を紹介し、この技法によってプログラマーが読みやすいコードを書く能力が向上するかどうかを評価する。方法 研究者は、21人のソフトウェア工学の修士課程の学生を使ってフィールド調査を行い、各学生が4つのセッションで異なる「生産準備」ソフトウェアを評価し、コード可読性テストに従った。観察後、プログラマーの視点をアンケートで評価した。結果 コード・リーダビリティ・テストに従うことで、「読めない」コードを書いていたプログラマの半数が、4回のセッション後に「読める」コードを書くようになった。また、「読める」コードを書いていたプログラマは、「読める」コードを書く能力を向上させた。調査の結果、コードの可読性を高めるために最も頻繁に提案されているのは、変数名の改善、メソッド名の改善、コードの重複を減らすための新しいメソッドの作成、if条件と構造の単純化、ループ条件の単純化であることが明らかになった。プログラマーは、可読性テストは時間をかける価値があると報告している。彼らは、読みやすいコードを書く能力が向上したことを確認している。プログラマーは、読者が自分のコードを理解するのに苦労しているのを経験すると、読みやすいコードを書こうという意欲が湧いてくる。結論 この論文では、コードの可読性を定義し、コードの可読性テストがプログラマーの読みやすいコードを書く能力を向上させることを実証し、コードの可読性を向上させるために必要な頻繁な修正を特定する。

結論

VIII. CONCLUSIONS Code readability testing addresses the question, “Is my code readable?” by exposing the thought process of a peer reading the code. In this study, 21 programmers followed Code Readability Testing in four sessions. Most programmers writing “difficult to read” code became programmers writing “easy to read” code after three sessions. Programmers writing “easy to read” code improved their skill. This study identifies several common fixes to unreadable code including improvements to variable names, improvements to method names, the creation of new methods to reduce code duplication, simplifying if conditions and structures, and simplifying loop conditions. The programmers reported that the technique is worth their time and articulated how readability testing alters their programming habits.

  • コードの可読性に関する実証実験
  • 21人のプログラマが4回のセッション
  • 読みにくいコードを書いていたほとんどのプログラマーは、3回のセッションで「読みやすい」コードを書くようになった
  • 読みやすいコードを書くプログラマーは、そのスキルを向上させた
  • 変数名の改善、メソッド名の改善、コードの重複を減らすための新しいメソッドの作成、if条件や構造の簡略化、ループ条件の簡略化など、読みにくいコードに共通する修正に時間を費やす価値がある
じん@酒酔いエンジニアじん@酒酔いエンジニア

18.【文献まとめ】React / JavaScript でのテストコード

なぜテストを書くのか?

  • 品質の保証: テストは、コードが期待通りに動作するかを確認するためのツールです。
  • リファクタリング: 既存のコードの品質を向上させるための変更が容易になります。
  • ドキュメンテーション: テストはコードの動作のドキュメンテーションとしても機能します。


どのようなテストを行うべきか?

React等のUIフレームワークを使ったWebアプリケーション開発では**「ユーザーのユースケースを担保するための結合テストを優先的に行ない、要件に応じて他のテストを組み合わせていく」** ことが単体テストまたは結合テストの両者の一部ですべきテストです。


何を用いるのか

Raectのようなフレームワークで基本的に、JestReact Testing Packageを使用して、(テストコードを用いた)自動テストを行うことが一般的です。


Jestとは?

Jest はシンプルさを重視した、快適な JavaScript テスティングフレームワークです。Babel、TypeScript、Node、React、Angular、Vue など、様々なフレームワークを利用したプロジェクトで動作する。Reactと相性が良く、設定がシンプルで速度も速いため、多くのReactプロジェクトで採用されています。

Jestの特徴

  • モック機能: 簡単にモックを作成し、関数の呼び出しを検証できます。
  • スナップショットテスト: コンポーネントの出力をキャプチャし、変更を検知できます。
  • 並列実行: テストを高速に実行します。

React Testing Libraryとは?

React Testing Libraryは、Reactコンポーネントのテストを助けるユーティリティセットです。UIコンポーネントをテストするものとして、捉えていて問題なさそうです。

React Testing Libraryの特徴

  • DOMに焦点: ユーザーが見るものと触れるものに焦点を当てます。
  • 非侵襲的: コンポーネントの内部実装に依存しないテストが推奨されています。


JestとReact Testing Libraryを使用したReactテストコードの規約

Reactアプリケーションの品質を保つためには、適切なテストが不可欠です。ここでは、JestとReact Testing Libraryを使用したReactテストの規約について説明します。

  1. コンポーネントのレンダリングテスト: 最低限、コンポーネントがエラーなくレンダリングされるかを確認します。
  2. ユーザーインタラクションのテスト: ボタンのクリックやフォームの送信などのユーザーインタラクションをシミュレートし、適切な応答があるかを確認します。
  3. 状態の変更テスト: コンポーネントの状態が適切に更新されるかをテストします。
  4. イベントハンドラのテスト: 例えば、onClickonChangeのようなイベントハンドラが正しく呼び出されるかを確認します。
  5. 非同期動作のテスト: API呼び出しやタイマーなどの非同期動作が正しく行われるかをテストします。


参考記事

じん@酒酔いエンジニアじん@酒酔いエンジニア

19. 【記事要約】Goでモックを作成してテストをする

Goでモックを作成してテストをする

https://qiita.com/S-Masakatsu/items/2bc751df9583657181e9

golang/mock とは

Go 公式が提供しているライブラリで、インターフェースの定義からモックを生成することができます。

  • mockgen … モックを自動生成するツール
  • gomock … 生成したモックを扱うパッケージ

https://github.com/golang/mock

インストール

README に記載されている通りにインストールしてください。
mockgen というCLIツールと gomock というパッケージの両方をインストールする必要があります。

  • mockgen (初回のみ)
    $ go install github.com/golang/mock/mockgen
    
  • gomock
    $ go get github.com/golang/mock/gomock
    

モックの作成

下記のようなコマンドを実行することでモックが生成されます。

$ mockgen -source=player_api.go -destination=./mock/player_api_mock.go -package=repository

<オプションの意味>

  • -source … モックを生成する対象のファイルを指定
    • -source=player_api.go: playerapi.goのモックを作成する
  • -destination … 生成したモックを保存する場所を指定
    • -destination=./mock/player_api_mock.go: 現在のディレクトリにmockフォルダを作成し、その中でplayer_api_mock.goを作成する
  • -package … 生成するモックのパッケージ名を指定
    • -package=repositoryg: コード内で package repositoryとされる

【追記】この記事では、_mock.goで作成しているが公式ドキュメントでそういった記載はなく、別記事を見ると全く別の命名でもいいことから、命名規則は特にない。

コマンド実行後に自動でモックが生成されます。

├── repository
│   ├── mock
│   │   └── player_api_mock.go
│   └── player_api.go

概要手順

1. モックの自動生成(必要であれば追記)
2. モックを使用してテストの作成(_test.goをメインコードと同じディレクトリに作成)
3. モックコードから呼び出しながら実装

おまけ(これ、結構使える

その他にも以下のようなメソッド等を使うことでモックの呼び出しを指定することができます。

  • *gomock.Call.Times メソッドで、メソッドの呼び出し回数が指定
    pr.EXPECT().GetPlayerList(ctx).Return(resp, nil).Times(2)
    
  • *gomock.Call.AnyTimes メソッドで、メソッドの呼び出しが複数回呼べることを指定
    pr.EXPECT().GetPlayerList(ctx).Return(resp, nil).AnyTimes()
    
  • gomock.Any() で、常に一致する引数を指定
    pr.EXPECT().Save(ctx, gomock.Any())
    
  • *gomock.Call.DoAndReturn メソッドで、呼び出し後のアクションと戻り値を指定
    pr.EXPECT().Save(ctx, gomock.Any()).
        DoAndReturn(func(ctx context.Context, player *model.Player) error {
            require.Equal(t, "12345", player.ID)
            require.Equal(t, "test-player", player.Name)
            require.Equal(t, 1, player.Ranking)
            return nil
        })
    
じん@酒酔いエンジニアじん@酒酔いエンジニア

20. 【記事要約】React Testing Libraryの使い方(途中)

React Testing Libraryの使い方

https://qiita.com/ossan-engineer/items/4757d7457fafd44d2d2f

Jest 対 React Testing Library

  • React Testing LibraryはJestの代わりにはなりません。相互に依存し、それぞれが明確な担当領域を持つため。
  • Jestはテストランナー(テスト用スクリプトをpackage.jsonに設定したらnpm testで実行可能)である。
  • JestとRTLの違いと関係について説明しています。JestはRTLの代わりではなく、相互に補完関係にあります。JestはJavaScriptアプリケーションのための人気のテストフレームワークであり、モダンなReact開発には欠かせません​​。


How to test React with Jest

https://www.robinwieruch.de/react-testing-jest/

How to test React with Jest (日本語訳)

https://zenn.dev/jin237/articles/589a7187a98429


コンポーネントのレンダリング

create-react-appを使用している場合、React Testing Libraryは標準で含まれています。自前でReactセットアップ(例. WebpackでReact)もしくは他のReactフレームワークを使用している場合は、React Testing Libraryも自分でインストールする必要があります。このセクションでは、React Testing LibraryでReactコンポーネントをレンダリングする方法を学びます。下記のAppという名の関数コンポーネントをsrc/App.jsファイルから引用して利用します。

ここではファイル名は、「App.js」に対して、「App.test.js」にして、同じ階層においている。

src/App.js
import React from 'react';

const title = 'Hello React';

function App() {
  return <div>{title}</div>;
}

export default App;

テストコード↓

src/App.test.js
import React from 'react';
import { render, screen } from '@testing-library/react';

import App from './App';

describe('App', () => {
  test('renders App component', () => {
    render(<App />);

    screen.debug();
  });
});

実行結果↓(AppコンポーネントのHTMLを確認)

実行結果
<body>
  <div>
    <div>
      Hello React
    </div>
  </div>
</body>


要素の選択

Reactコンポーネントをレンダリングした後、RTLの様々な検索関数を使って要素を取得する方法を学びます。これらの要素はアサーションやユーザーインタラクションに使用されます​​。

検索タイプ

RTLで要素を選択する上で、TextやRoleなどの検索タイプについて学びます。Textは一般的に利用される検索タイプであり、Roleも役立つことが多いです​​。

検索バリエーション: 検索タイプの他に、RTLには検索バリエーションもあります。例えば、getByTextやgetByRoleで使用されるgetByなどがあります​​。

イベントの発火

コールバックハンドラ

非同期 / async