kakakakakku blog

Weekly Tech Blog: Keep on Learning!

testing/fstest: Go でファイルシステムに依存したテストを書く

Go で testing/fstest を使うと,ファイルシステムに依存したテストコードを書くときに実際のファイルシステムへの依存度を減らせる.テストコードを書こうとすると,テスト項目(バリエーション)ごとにファイルを用意する必要があるため,ファイルの管理が面倒になることもある.最近使う機会があって簡単にまとめておく📝

pkg.go.dev

io/fs interface と testing/fstest.MapFS{}

testing/fstestfstest.MapFS{} を使うと任意のファイルで構成されるファイルシステム(仕組みとしてはインメモリとドキュメントに書いてある)を作れる.そして,ファイルシステムを抽象化した io/fs.FS interface を満たすオブジェクトになっているため,ビジネスロジック側で fs.FS を受け取るように実装しておけば,テストコードでは fstest のオブジェクトを渡せる.

pkg.go.dev

fs := fstest.MapFS{
    "helloworld.md":  {Data: []byte("helloworld")},
    "helloworld2.md": {Data: []byte("helloworld2")},
    "helloworld3.md": {Data: []byte("helloworld3")},
    "helloworld4.md": {Data: []byte("helloworld4")},
    "helloworld5.md": {Data: []byte("helloworld5")},
}

サンプルコード

以下のように main.gomain_test.go を書いてみた.

main.go

実装に特に意味はなくサンプルではあるけど,"files ディレクトリのファイル数を返す" というロジックを考える.ポイントはファイル数を返す countFiles() 関数の引数として fs.FS interface を受け取るようにしているところ❗️

package main

import (
    "fmt"
    "io/fs"
    "os"
)

func countFiles(fsys fs.FS) (int, error) {
    files, err := fs.ReadDir(fsys, ".")
    if err != nil {
        return 0, err
    }
    return len(files), nil
}

func main() {
    count, err := countFiles(os.DirFS("files"))
    if err != nil {
        fmt.Println(err)
    }
    fmt.Println(count)
}

main_test.go

テストコード側では testing/fstest.MapFS{} を使って,テスト項目ごとにファイルシステムを作っている(ファイルなし・1ファイル・2ファイル).もっともっとテスト項目が増えることを想像すると fstest を使うメリットが感じられると思う✌

package main

import (
    "testing"
    "testing/fstest"
)

func TestCount(t *testing.T) {
    t.Run("File does not exist.", func(t *testing.T) {
        fs := fstest.MapFS{}
        want := 0
        got, _ := countFiles(fs)
        assertCount(t, got, want)
    })

    t.Run("One file exists.", func(t *testing.T) {
        fs := fstest.MapFS{
            "helloworld.md": {Data: []byte("helloworld")},
        }
        want := 1
        got, _ := countFiles(fs)
        assertCount(t, got, want)
    })

    t.Run("Two files exist.", func(t *testing.T) {
        fs := fstest.MapFS{
            "helloworld.md":  {Data: []byte("helloworld")},
            "helloworld2.md": {Data: []byte("helloworld2")},
        }
        want := 2
        got, _ := countFiles(fs)
        assertCount(t, got, want)
    })
}

func assertCount(t *testing.T, got, want int) {
    t.Helper()
    if got != want {
        t.Errorf("got %d, want %d", want, got)
    }
}

まとめ

testing/fstest は便利だから覚えておこう〜❗️

go test -short で長時間実行されるテストをスキップする

go test でテストを実行するときに -short オプションを付けると実行対象のテストを減らせる.

例えば,長時間実行されるテストがあった場合に毎回全体の実行を待つのではなく,一部のテストをスキップすることで時間短縮できる.開発中に素早くフィードバックを得たいような場合に使えそう❗️最近使う機会があって簡単にまとめておく📝

go help testflag でヘルプを確認すると以下のように表示された.

$ go help testflag
The 'go test' command takes both flags that apply to 'go test' itself
and flags that apply to the resulting test binary.

(中略)

    -short
        Tell long-running tests to shorten their run time.
        It is off by default but set during all.bash so that installing
        the Go tree can run a sanity check but not spend time running
        exhaustive tests.

testing.Short() と組み合わせる

go test -short と実行すると testing.Short() の値が true になる.よって,長時間実行されるテストが自動的に判別されるのではなく,テストコードに以下のような実装を追加することでスキップできるようになる.スキップする場合は SkipNow() 以外に Skip()Skipf() もある.

  • func (c *T) Skip(args ...any)
  • func (c *T) SkipNow()
  • func (c *T) Skipf(format string, args ...any)
if testing.Short() {
    t.SkipNow()
}

pkg.go.dev

サンプルコード

以下のように main_test.go を書いてみた.

  • TestCase1 は毎回実行される
  • TestCase2 はスキップできる
package main

import (
    "testing"
)

func TestCase1(t *testing.T) {
    got := 1
    want := 1
    if got != want {
        t.Errorf("got %d, want %d", want, got)
    }
}

func TestCase2(t *testing.T) {
    if testing.Short() {
        t.SkipNow()
        // t.Skip("it skipped")
        // t.Skipf("it skipped because `testing.Short()` is %t", testing.Short())
    }
    got := 1
    want := 1
    if got != want {
        t.Errorf("got %d, want %d", want, got)
    }
}

実行例 1

TestCase1TestCase2 も実行された.

$ go test -v
=== RUN   TestCase1
--- PASS: TestCase1 (0.00s)
=== RUN   TestCase2
--- PASS: TestCase2 (0.00s)
PASS
ok

実行例 2: t.SkipNow()

TestCase2 はスキップされた.

$ go test -v -short
=== RUN   TestCase1
--- PASS: TestCase1 (0.00s)
=== RUN   TestCase2
--- SKIP: TestCase2 (0.00s)
PASS
ok

実行例 3: t.Skip()

TestCase2 はスキップされた.t.Skip() で指定したログも出力された.

$ go test -v -short
=== RUN   TestCase1
--- PASS: TestCase1 (0.00s)
=== RUN   TestCase2
    main_test.go:18: it skipped
--- SKIP: TestCase2 (0.00s)
PASS
ok

実行例 4: t.Skipf()

TestCase2 はスキップされた.t.Skipf() で指定したログも出力された.

$ go test -v -short
=== RUN   TestCase1
--- PASS: TestCase1 (0.00s)
=== RUN   TestCase2
    main_test.go:19: it skipped because `testing.Short()` is true
--- SKIP: TestCase2 (0.00s)
PASS
ok

Learn Go with Tests: テスト駆動開発を体験しながら Go を学ぼう

TDD(テスト駆動開発)を体験しながら Go を学べる学習コンテンツ「Learn Go with Tests」を紹介する❗️全てのコンテンツを実施してみて,非常に良かったのでまとめることにした💡

  • Go に入門できる
  • TDD のサイクル (Red / Green / Refactor) を体験できる
  • コンテンツは "35種類" もある
  • 無料で学べる
  • GitBook (GitHub) に公開されている
  • 日本語対応

英語版 📚

quii.gitbook.io

日本語版 📚

andmorefine.gitbook.io

コンテンツ一覧

なんと「35種類」もコンテンツがある❗️

Go fundamentals 🚢 21種類

  • Install Go(Go をインストールする)
  • Hello, world(Hello, World)
  • Integers(整数)
  • Iteration(反復、繰り返し)
  • Arrays and slices(配列とスライス)
  • Structs, methods & interfaces(構造体、メソッド、インターフェース)
  • Pointers & errors(ポインタとエラー)
  • Maps(マップ)
  • Dependency Injection(依存性注入)
  • Mocking(スタブ・モック)
  • Concurrency(並行性)
  • Select(選択)
  • Reflection(リフレクション)
  • Sync(同期)
  • Context(コンテキスト)
  • Intro to property based tests(プロパティベースのテスト概要)
  • Maths(数学)
  • Reading files(ファイルの読み取り)
  • Templating(テンプレート)
  • Generics(ジェネリクス)
  • Revisiting arrays and slices with generics(ジェネリクスを使用した配列とスライスの再検討)

Build an application 🚢 6種類

  • HTTP server(HTTP サーバー)
  • JSON, routing and embedding(JSON、ルーティング、埋め込み)
  • IO and sorting(IO、並び替え)
  • Command line & project structure(コマンドライン、パッケージ構造)
  • Time(時間)
  • WebSockets(ウェブソケット)

Testing fundamentals 🚢 2種類

  • Introduction to acceptance tests(受け入れテストの紹介)
  • Scaling acceptance tests(受け入れテストを拡張する)

Questions and answers 🚢 4種類

  • OS exec(OS 実行)
  • Error types(エラーの種類)
  • Context-aware Reader(コンテキスト認識リーダー)
  • Revisiting HTTP Handlers (HTTP ハンドラーの再検討)

Meta / Discussion 🚢 2種類

  • Why unit tests and how to make them work for you(ユニットテスト機能を作成する方法)
  • Anti-patterns (アンチパターン)

quii/learn-go-with-tests より引用

おすすめ理由 1

\( 'ω')/ Red! Green! Refactor!

何よりも TDD(テスト駆動開発)のサイクルを体験し,良さを実感できることが Learn Go with Tests の1番の素晴らしさだと思う.最初に期待するテストコードを書くけど,関数名がなく undefined エラーになって,コンパイルを通しつつ,期待する実装に近付けていく.そして,リファクタリングをしながら一歩一歩改善する.

実際に体験してみると「こんなに歩幅を小さく進められるんだ〜」「過剰なリファクタリングや共通化は不要なんだ〜」ということに気付ける❗️

  • テストコードを書く(落ちる : Red)
  • コンパイルを通しつつ最低限の実装をする(落ちる : Red)
  • 実装をする(通る : Green)
  • リファクタリングをする(通る : Green)

おすすめ理由 2

\( 'ω')/ Go に詳しくなれる!

TDD(テスト駆動開発)だけではなく,Go をちゃんと学べるのも良かった.特に Go fundamentals(21種類)は目次を見るとわかる通り,Hello World・スライス・マップのような基本的なトピックから始まって,依存性注入・goroutine・リフレクション・ジェネリクスのような実践的なトピックまで学べる.

他にも Build an application(6種類)では,net/http を使った API 構築や CLI 構築も体験できる.Go で API を実装するときのテスト戦略に悩むこともあると思うし,そういった参考にもなる❗️

とは言え,完全に Go 入門者だと途中から難しくなってしまうと思うので(個人差はありそうだけど),A Tour of Go ぐらいはやっておくと良さそう👍

go-tour-jp.appspot.com

おすすめ理由 3

\( 'ω')/ テスト手法に詳しくなれる!

Learn Go with Tests はテスト手法に対する理解が深まるのも良かった.Testify などは使わずにアサートを実装するし,テーブル駆動テスト (Table Driven Test) ・プロパティ駆動テスト (Property Based Test)・承認テスト (Approval Test)・モックなど,実践的に使えるテスト手法が出てくる.

今まで「テストが書きにくいからここは諦めよう...」と悩んだことがあったらきっと参考になると思う❗️

kakakakakku.hatenablog.com

日本語に関して 🇯🇵

Learn Go with Tests には日本語版 テスト駆動開発でGO言語を学びましょう がある.とても読みやすく翻訳されているし,Google 検索で候補に出てくることもあって,検索流入で本コンテンツを知る人も多いと思う.翻訳に感謝👏

しかし現時点だと,あまりメンテナンス(プルリクエストの取り込み)はされてなく,英語版 (quii/learn-go-with-tests) に対する更新の反映も止まっている.

ちなみに以下のコンテンツには日本語版がなく,今回は英語版で実施したため,ついでに翻訳も行った.Go fundamentals の4種類は既に完了してプルリクエストは出してある✅ 引き続き残りも進めていく予定だけど,もしリポジトリ運用が回ってなかったらメンテナとしてお手伝いしたく,コラボレーター権限をもらえると嬉しいなーと❗️❗️❗️

日本語版未対応のコンテンツ一覧

英語版 GitHub リポジトリ 👾

github.com

日本語版 GitHub リポジトリ 👾

github.com

まとめ

Learn Go with Tests でテスト駆動開発を体験しながら Go を学ぼう❗️

素晴らしいコンテンツでした❗️

参考 : 所要時間

GitHub のコミット履歴を確認したら「35種類」全てを実施するまでの所要時間は1ヶ月ほど(2/3 ~ 3/7まで)だった.毎日コツコツと1時間~1時間半ほど取り組んでいたため,所要時間の参考としては「30時間ぐらい」だと思う.追加で調査をしたり,誤りを直してプルリクエストを送っていた時間も含めている.コミット数は 237 だった👌

関連記事

kakakakakku.hatenablog.com

読み手を意識したテクニカルドキュメントを書くために /「エンジニアのためのドキュメントライティング」を読んだ

2023年3月に出版された「エンジニアのためのドキュメントライティング」を読んだ.原著は Docs for Developers で,翻訳を担当された @iwashi86 さんに本書を送っていただいた.ありがとうございます❗️そして出版おめでとうございます🎉

本書は今まで書いてきたテクニカルドキュメントに対して "できてなかったな〜" という気付きを与えてくれるイイ本でした📖 読みながら感じたことを大きく分類すると以下2点!印象に残ったところを中心にまとめようと思う.

  • テクニカルドキュメントの重要さを改めて認識できた👌
  • テクニカルライティングという仕事(ポジション)に興味を持った👀

目次

  • PART I「ドキュメント作成の準備」
    • CHAPTER 1 : 読み手の理解
    • CHAPTER 2 : ドキュメントの計画
  • PART Ⅱ「ドキュメントの作成」
    • CHAPTER 3 : ドキュメントのドラフト
    • CHAPTER 4 : ドキュメントの編集
    • CHAPTER 5 : サンプルコードの組み込み
    • CHAPTER 6 : ビジュアルコンテンツの追加
  • PART Ⅲ「ドキュメントの公開と運用」
    • CHAPTER 7 : ドキュメントの公開
    • CHAPTER 8 : フィードバックの収集と組み込み
    • CHAPTER 9 : ドキュメントの品質測定
    • CHAPTER 10 : ドキュメントの構成
    • CHAPTER 11 : ドキュメントの保守と非推奨化

テクニカルドキュメントをプロダクト開発のように考える

本書を読んで印象に残ったのは,ドキュメントも「プロダクト開発のように考えている」というところ.ドキュメントの読み手(ペルソナ・目的)を整理して,ドキュメントをうまく構造化してまとめて,評価やフィードバックを基に改善を繰り返す.まさにプロダクト開発のフローのように感じられたし,同時にそれほど注力するべきとも言える❗️ドキュメントライティングをある意味 "おまけのように" 考えてしまうと,価値がなく誰にも読まれないドキュメントになってしまう可能性もある.

CHAPTER 1 で解説されている「読み手の理解」は良かった👏 ドキュメントの読み手を意識してドキュメントを書くべきということは頭では理解していても案外できていないこともありそう.結果的に "独りよがりな" ドキュメントを書いてしまうことにも繋がる.そして,ドキュメントを読んでもうまく使えなかったときの状況をまとめる「フリクションログ」というテクニックも参考になった.

知識の呪い

人間は他人が自分と同じ知識を持っていると思い込んでいる という認知バイアスを「知識の呪い」として紹介されていたのも良かった.僕自身は技術講師(テクニカルトレーナー)として働いているため,技術を教える立場としてこの「知識の呪い」には絶対に該当しないように常に気を付けているけど,ドキュメントライティングでも意識するべきだと気付けた.

テクニカルドキュメントの種類

CHAPTER 2 でテクニカルドキュメントの種類(コンテンツタイプ)が以下のようにまとまっていて,分類として参考になった.ここまで厳密に分類されてドキュメントが公開されている事例ってどのぐらいあるんだろう.また "コンセプトドキュメントや手順書" は「教育や情報提供」を取り扱って,"リファレンスドキュメント" は「原因と結果」を取り扱うという解説があって,ここでも読み手を意識した分類になっているんだなぁーと感じた.

  • コードコメント
  • README
  • スタートガイド (Getting Started)
  • コンセプトドキュメント
  • 手順書
    • チュートリアル (Tutorial)
    • ハウツーガイド (How To Guide)
  • リファレンスドキュメント
    • API リファレンス
    • 用語集
    • トラブルシューティングドキュメント
    • 変更に関するドキュメント (Change Log)

そして,僕は気になるサービスやツールを試すときに「チュートリアル」を試してテックブログにまとめることが多く,チュートリアルの良し悪しによってそのサービスやツールへの第一印象が決まってしまう.実際にこういう体験があるからこそ,テクニカルドキュメントの重要性も改めて認識できた.また本書では Getting Started / Tutorial / How To Guide それぞれの違いがまとまっていたのも良かった.Getting Started = Tutorial というドキュメントも結構ある気がする...

非推奨化 (deprecated)

ドキュメントは1度書いて終わりではなく継続的にメンテナンスをしていく必要がある.メンテナンス計画を考えたり,ドキュメントオーナーを任命するなど,CHAPTER 11 で解説されている内容は特に良かった.またメンテナンス作業をできる限り自動化するというプラクティスも参考になった.

  • 鮮度確認(最終更新日を表示する)
  • リンクチェッカー(リンク切れを回避する)
  • ドキュメントリンター(文章チェックをする)
  • リファレンスドキュメント生成(OpenAPI / Swagger などを活用した自動化)

ドキュメントリンターとして textlint は便利だし,textlint-rule-no-dead-link を使えば textlint でリンク切れも検知できる.

kakakakakku.hatenablog.com

そして,不要になったドキュメントを突然消すのではなく,非推奨化 (deprecated) という形で事前に読み手に伝えるというプラクティスも実体験としてあると嬉しく,明確にまとまっていて良かった.実は CHAPTER 11 を読んでいて "書いてあったら引用しやすくて嬉しいなぁー" と思っていたことがあって,それは「ドキュメントを削除するときに最低限リダイレクト設定はして欲しい」という話なんだけど,最後の "まとめ"ユーザーが立ち往生しなくて済むようにリダイレクトを設定してください。 と書いてあって「最高〜」と心の中で叫んだw

テックブログに当てはめて考える

本書で取り扱う "テクニカルドキュメント" として,テックブログは当てはまらないところもあるとは思うけど,テックブロガーとして参考になるプラクティスもあった.例えば,CHAPTER 5 で解説されているサンプルコードのプラクティスや CHAPTER 6 で解説されている図解の必要性(百聞は一見にしかず)など❗️

さらに CHAPTER 3 で解説されていた「流し読みに適した書き方」というのは重要なマインドセットだと思った.テックブログを書いてる側としてはうまく推敲した文章全てを上から下まで読んでもらいたいとは思うけど,実際には "知りたいことを最速で見つけたい" というニーズも多いはずで,期待した通りに読まれないことを受け入れる必要がある.TL;DR を活用したり,文章を簡潔にまとめたり,図解を活用したり,今後工夫できることはたくさんあるな〜と気付けた💡

\( 'ω')/ 流し読み時代を受け入れよう!

付録 B(リソース)と付録 C(参考文献)

本書では関連するウェブサイトや論文など,多くの引用元が注釈されていて,さらに本書の最後には付録としてまとまっていた.付録は非常にありがたく,はじめて出会えたものもあれば,読もう読もうと思って後回しにしていたものもあって,優先順位を上げるきっかけになった❗️

Google 提供の Technical Writing Courses は前から気になっていたけどまだ受講できてなく,やるぞー!という気持ちになった.本書には2コースと書かれていたけど,現在は4コースありそう.

  • Technical Writing One
  • Technical Writing Two
  • Tech Writing for Accessibility
  • Writing Helpful Error Messages

developers.google.com

REST API のドキュメント開発を学べるコース💡内容的には非常に深く気になるけど,162ページもあってボリュームすげぇーw

  • I: Introduction to REST APIs
  • II: Using an API like a developer
  • III: Documenting API endpoints
  • IV: OpenAPI spec and generated reference docs
  • V: Step-by-step OpenAPI code tutorial
  • VI: Testing API docs
  • VII: Conceptual topics in API docs
  • VIII: Code tutorials
  • IX: The writing process
  • X: Publishing API docs
  • XI: Thriving in the API doc space
  • XII: Native library APIs
  • XIII: Processes and methodology
  • XIV: Metrics and measurement
  • XV: Additional resources

idratherbewriting.com

ソフトウェアアーキテクチャをうまく表現するための C4 model (context, containers, components, code) も理解しておこう.

c4model.com

その他メモ

  • エラーをドキュメントに載せるときは検索しやすいように1ページにまとめる
  • コールアウト
  • 完璧主義 から脱却する(書籍 "完璧を求める心理" とも関係しそう)
  • プラッシング (Plussing)
  • サンプルコードを動かすサンドボックス環境があると便利だけど多くの場合は過剰
  • テクニカルライターの多くはメンテナンスの面から映像には慎重
  • TTHW (Time to Hello World)

kakakakakku.hatenablog.com

あわせて読みたい・聞きたい

本書のポイントや本書とあわせて読むと良さそうな名著「理科系の作文技術」などの紹介もあって非常にうまくまとめられていた.実は本書を読みながら,対象としているテクニカルドキュメントの具体的なイメージが掴めず,数回戻って読み直したりしていた.最終的に SaaS / OSS ドキュメント・API ドキュメント・GitHub の README.md などを想像しながら読むことにしたけど,社内ドキュメントはどうなんだろう〜?という点は疑問だった.以下のスライドの P.50-51 でちゃんと 実際にはほとんどのドキュメントで応用可能 とフォローアップされていた❗️

speakerdeck.com

原著の共著者 Zachary Sarah Corleissen にインタビューする fukabori.fm のエピソードも良かった📻

fukabori.fm

まとめ

2023年3月に出版された「エンジニアのためのドキュメントライティング」を読んだ.重要なのは本書で学んだプラクティスを実践して,ドキュメントによって読み手を支援できるかどうかだと思う.できそうなところから着手していこう.また今まで長年テックブログを書き続けているけど,この経験を役立てられそうなテクニカルライティングという仕事(ポジション)にも興味を持った💡

テクニカルドキュメントの重要さを改めて認識できるイイ本でした❗️おすすめ❗️

誤植など

既に報告されているかもしれないけど 初版第一刷 を読んでいて気付いたところをまとめておくー👾

  • P.42 表してします。表しています。
  • P.54 コンセプトガイドコンセプトドキュメント(もしかしたら表記揺れ?)
  • P.59 熱心の熱心な
  • P.84 アップロートアップロード
  • P.211 KubenetesKubernetes
  • P.222 付録 2付録 B
  • P.228 https://snagit.com/https://www.techsmith.com/screen-capture.html(リダイレクトされる?)

関連記事

kakakakakku.hatenablog.com

go-approval-tests: Go で「承認テスト」を実装する

Approval Tests というツールセットを使うと簡単に「Approval Testing(承認テスト)」を実装できる.Approval Testing では,スナップショットテストのように "結果全体を意識した" テストコードを記述する.GitHub には Golden master Verification Library とも書かれていて,BDD (Behavior-Driven Development) で使われることもある.

単体テストを実装するときに一般的に使われる assertEquals のようなアサーションだと値ごとに細かく検証することになるため,ファイル全体やオブジェクト内容を検証するときの選択肢になる.今回紹介する Approval Tests は多くのプログラミング言語をサポートしている💡

approvaltests.com

最近 Go で go-approval-tests (ApprovalTests.go) を使う機会があって,学んだことを簡単にまとめておくことにした❗️ドキュメントを確認すると多くの Verify 関数が実装されている.以下に代表的なものを載せておく📝他にもまだまだある〜

  • VerifyString()
  • VerifyMap()
  • VerifyJSONBytes()
  • VerifyJSONStruct()
  • VerifyAllCombinationsFor1()
  • VerifyAllCombinationsFor2()

github.com

pkg.go.dev

approvals/ApprovalTests.Go.StarterProject リポジトリ

今回は動作確認に使える Starter Project(サンプルプロジェクト)を使って go-approval-tests を試す.まず sample.gosample_test.go を載せておく❗️

github.com

sample.go

  • Person 構造体
package starterpackage

type Person struct {
    Name string
    Age  int
}

sample_test.go

  • TestNormalTest() テスト(一般的な値比較)
  • TestBasicApproval() テスト(go-approval-tests で文字列比較)
  • TestJsonApproval() テスト(go-approval-tests で JSON 比較)
  • TestCombinationsApproval() テスト(go-approval-tests で組み合わせ比較)
package starterpackage

import (
    "fmt"
    approvals "github.com/approvals/go-approval-tests"
    "testing"
)

func TestNormalTest(t *testing.T) {
    if 5 != 5 {
        t.Fatal("5 != 5")
    }
}

func TestBasicApproval(t *testing.T) {
    approvals.VerifyString(t, "Hello Approvals")
}

func TestJsonApproval(t *testing.T) {
    person := Person{
        "John Galt", 100,
    }
    approvals.VerifyJSONStruct(t, person)
}

func TestCombinationsApproval(t *testing.T) {
    arr := []int{10, 20, 30, 40}
    names := []string{"john", "paul", "mary"}
    approvals.VerifyAllCombinationsFor2(t, "File Names", func(a interface{}, b interface{}) string { return fmt.Sprintf("%v_%v.txt", a, b) }, names, arr)
}

VerifyString() 関数

まず VerifyString() 関数を確認する.以下のコードでは Hello Approvals という文字列が返ってくる場合(正確にはビジネスロジックから得られた値)をテストしている.go-approval-tests では「期待値」はファイルに記載する.

func TestBasicApproval(t *testing.T) {
    approvals.VerifyString(t, "Hello Approvals")
}

このままテストを実行すると,自動的に以下の2ファイルが作られる.received.txt には戻り値(いわゆる got)が設定されていて,approved.txt には期待値(いわゆる expected)をこれから設定する.

  • sample_test.TestBasicApproval.received.txt
  • sample_test.TestBasicApproval.approved.txt

そこで approved.txt に期待値として Hello Approvals を設定して再実行するとテストが通って,received.txt は自動的に削除される.なお,macOS で go-approval-tests を実行すると,デフォルトでは Xcode の FileMerge を使って GUI で差分を確認できる.今回の例だと文字列が短いけど,もっと長かったりすると差分を明確に確認できて便利❗️

次に戻り値を Hello Approvals ではなく Bye Approvals のように意図的に間違えて再実行すると,以下のように「どのような差分があるのか」を確認できる.

VerifyJSONStruct() 関数

go-approval-tests の GitHub に以下のように書かれている通り,単純な文字列よりも複雑なオブジェクトなどをテストするときにメリットが出てくると思う.

ApprovalTests allows for easy testing of larger objects, strings and anything else that can be saved to a file (images, sounds, csv, etc...)

VerifyJSONStruct() 関数や VerifyJSONBytes() 関数を使うと JSON オブジェクトをテストできて,サンプルコードでは VerifyJSONStruct() 関数を使って Go の struct(構造体)をテストしている.

func TestJsonApproval(t *testing.T) {
    person := Person{
        "John Galt", 100,
    }
    approvals.VerifyJSONStruct(t, person)
}

実行すると,自動的に以下の2ファイルが作られる.

  • sample_test.TestJsonApproval.received.json
  • sample_test.TestJsonApproval.approved.json

approved.txt に期待値として以下の JSON(Person 構造体に値を設定して JSON 化した)を設定してテストを再実行するとテストが通る❗️

{
  "Name": "John Galt",
  "Age": 100
}

次に戻り値の Age 「意図的に 1000 にして再実行すると,以下のように差分を確認できる.

VerifyAllCombinationsFor2() 関数

VerifyAllCombinations ではオブジェクト(コレクション)の組み合わせを検証できる.引数に与えるコレクション数によって VerifyAllCombinationsFor1() から VerifyAllCombinationsFor9() まである.サンプルコードでは VerifyAllCombinationsFor2() 関数が使われていて,id と name の配列を組み合わせて "ファイル名" を生成するロジックの検証に go-approval-tests を使っている.

func TestCombinationsApproval(t *testing.T) {
    arr := []int{10, 20, 30, 40}
    names := []string{"john", "paul", "mary"}
    approvals.VerifyAllCombinationsFor2(t, "File Names", func(a interface{}, b interface{}) string { return fmt.Sprintf("%v_%v.txt", a, b) }, names, arr)
}

流れは同じで,実行すると,自動的に以下の2ファイルが作られる.

  • sample_test.TestCombinationsApproval.received.txt
  • sample_test.TestCombinationsApproval.approved.txt

approved.txt に以下のような期待値(記述は received.txt をコピーすれば OK)を設定してテストを再実行するとテストが通る❗️

File Names


[john,10] => john_10.txt
[john,20] => john_20.txt
[john,30] => john_30.txt
[john,40] => john_40.txt
[paul,10] => paul_10.txt
[paul,20] => paul_20.txt
[paul,30] => paul_30.txt
[paul,40] => paul_40.txt
[mary,10] => mary_10.txt
[mary,20] => mary_20.txt
[mary,30] => mary_30.txt
[mary,40] => mary_40.txt

⛏️ UseFolder() 関数

デフォルトだと received.txtapproved.txt はテストコードと同じディレクトリ階層に並ぶ.テストコードが増えるほどファイルが増えて気になるようになると思う.そのときは UseFolder() 関数を使って,特定のディレクトリにファイルをまとめられる.設定する場合は Go testing を実行するときのエントリーポイントになる TestMain に以下のように実装する.

func TestMain(m *testing.M) {
    approvals.UseFolder("testdata")
    os.Exit(m.Run())
}

⛏️ UseReporter() 関数

macOS のデフォルトだと,差分確認には Xcode の FileMerge が使われる.UseReporter() 関数を使うと任意のツールに変更できる.サポートされているツールは以下など.

  • VS Code (Visual Studio Code)
  • IntelliJ
  • GoLand
  • diff コマンド

例えば VS Code を使う場合は以下のように実装する.UseFolder() 関数と組み合わせて TestMain に実装できる.

func TestMain(m *testing.M) {
    r := approvals.UseReporter(reporters.NewVSCodeReporter())
    defer r.Close()
    approvals.UseFolder("testdata")
    os.Exit(m.Run())
}

実行すると以下のように VS Code で差分を確認できた.

もし GitHub Actions など CI/CD パイプラインの実行中に差分が出てしまうこともあると思う.その場合は単純に diff コマンドを使った差分として NewRealDiffReporter() 関数も使える.env 環境変数などによって挙動を切り替えておくと良さそう.

func TestMain(m *testing.M) {
    r := approvals.UseReporter(reporters.NewRealDiffReporter())
    defer r.Close()
    approvals.UseFolder("testdata")
    os.Exit(m.Run())
}

実行すると,以下のようにテキストで差分を確認できた.real_diff_reporter.go で実装を確認したところ exec.Command("diff", "-u", approved, received) のようになっていた.

--- testdata/sample_test.TestJsonApproval.approved.json  2023-03-03 15:50:00
+++ testdata/sample_test.TestJsonApproval.received.json   2023-03-03 16:00:00
@@ -1,4 +1,4 @@
 {
   "Name": "John Galt",
-  "Age": 100
+  "Age": 1000
 }