GoでExcelファイルをGolden Testする
これは CastingONE Advent Calendar 2023 1日目の記事です。
株式会社CastingONEでソフトウェアエンジニアをしている @takashabe です。普段はHR領域のSaaSをGoで書いています。
会社の テックブログ が立ち上がって1年ほど経過し、今では30以上のエントリが集まっています。過去にもテックブログをやろうぜ!と立ち上げたことがありますが、やはり継続するのが難しく、ここまでしっかり継続出来ていることは感慨深いものがあります。テックブログ編集委員のこともいずれ紹介出来ればなあと思っています。
そして次のチャレンジとして今年はアドベントカレンダーに会社で参戦することとしました。実は テックブログ第一弾 はGoアドベントカレンダーに乗っかって出したもので、こちらもエモさがあります。ゆるく書きたいものが書ければ良いかなと思っているので生暖かい目で見守っていただければ幸いです。
ではさっそく僕も書きたいものを書いていきたいと思います。まずはみんな大好きExeclファイル(xlsx)をGolden Testする話です。
本記事のサンプルコードが含まれるリポジトリはこちらです。
Golden Test とは
何らかの処理の出力結果を一度ファイルに保存し、つぎにテストを実行するときにはそのファイルと比較し、同じ結果が出力出来ているかをみるテストです。この比較に用いられるファイルは通常Golden Filesと呼ばれています。
Golden Test自体の概念については@mitchellh氏のトークが詳しいです。
sebdah/goldie でGolden Testする
GoでGolden Filesを扱うときにメジャーなのは sebdah/goldie: Golden file testing for Go (github.com) ではないでしょうか。シンプルな比較メソッド群が用意されており、Golden Filesの更新も簡単に行うことが出来ます。
以下はReadmeから引用したサンプルテストコードです。
g.Assert
でGolden Filesとの比較をバイト単位で行ってくれます。Golden Filesの更新は go test -update
のようにupdateフラグを付けるだけです。
func TestExample(t *testing.T) {
recorder := httptest.NewRecorder()
req, err := http.NewRequest("GET", "/example", nil)
assert.Nil(t, err)
handler := http.HandlerFunc(ExampleHandler)
handler.ServeHTTP()
g := goldie.New(t)
g.Assert(t, "example", recorder.Body.Bytes())
}
Excelファイル(xlsx)でGolden Testする
さて、本題のxlsxファイル形式でのGolden Testについてです。xlsxファイルの生成には qax-os/excelize: Go language library for reading and writing Microsoft Excel™ (XLAM / XLSM / XLSX / XLTM / XLTX) spreadsheets (github.com) を使用します。
一見ファイルが何であろうがバイト単位で比較してくれるので問題ないように思えます。しかしxlsxファイル形式は、その実zip圧縮されたxmlファイル群であり、zip圧縮するときのメタデータが存在します。そのため、メタデータを特にいじったりせずxlsxファイルを生成すると、Golden Filesとの比較で常にdiffが発生しまいます。
WithEqualFn オプションを使って比較する
goldieにはファイルの独自関数を使って等価性を判断できるようにするための WithEqualFn
オプションが用意されています。これを使ってテストコードを書くと以下のようになります。
package main
import (
"bytes"
"io"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/sebdah/goldie/v2"
"github.com/stretchr/testify/assert"
"github.com/xuri/excelize/v2"
)
func TestGolden(t *testing.T) {
got, err := genXlsx()
assert.NoError(t, err)
b, err := io.ReadAll(got)
assert.NoError(t, err)
equalFn := func(actual, expected []byte) bool {
act, err := excelize.OpenReader(bytes.NewBuffer(actual))
if err != nil {
t.Error(err)
return false
}
actCols, err := act.GetCols("Sheet1")
if err != nil {
t.Error(err)
return false
}
exp, err := excelize.OpenReader(bytes.NewBuffer(expected))
if err != nil {
t.Error(err)
return false
}
expCols, err := exp.GetCols("Sheet1")
if err != nil {
t.Error(err)
return false
}
if len(actCols) != len(expCols) {
t.Error("len(actCols) != len(expCols)")
return false
}
for i, col := range expCols {
for j, cell := range col {
diff := cmp.Diff(cell, actCols[i][j])
if diff != "" {
t.Error(diff)
return false
}
}
}
return true
}
g := goldie.New(t,
goldie.WithDiffFn(func(a, b string) string {
return "Diff is not shown. See golden files"
}),
goldie.WithEqualFn(equalFn),
)
g.Assert(t, t.Name(), b)
}
またテスト対象のコードは以下のようにxlsxファイルをio.Readerとして返す処理を想定しています。
package main
import (
"bytes"
"io"
"github.com/xuri/excelize/v2"
)
func main() {
reader, err := genXlsx()
if err != nil {
panic(err)
}
// do something...
_ = reader
}
func genXlsx() (io.Reader, error) {
f := excelize.NewFile()
if err := f.SetCellValue("Sheet1", "A1", "Hello world!"); err != nil {
return nil, err
}
var buf bytes.Buffer
if err := f.Write(&buf); err != nil {
return nil, err
}
return &buf, nil
}
テストコード中の equalFn
関数を見てもらえれば分かる通り、xlsxファイルをそのまま比較するのではなく、一度excelizeで読み込んでから各セルの値が同一かどうかをチェックしています。
もちろんここではプロジェクトごとに最低限担保すべき内容だけ書けば良いので、非常に柔軟にGolden Testを行えることが分かるかと思います。
モチベーションあるいはGolden Testの良さ
そもそもxlsxファイルをGolden Testするモチベーションは、生成されたファイルが複雑になりがちというのがあります。
SaaSをやっていると、よくCSVやxlsxで出力するといった機能がありますが、CSVはプレーンテキストなのに対してxlsxファイル形式はそもそもエディタで内容を確認することが困難です。そこでテストを行うことでGolden Filesを出力することにより、手元で出力結果が分かりやすくなる、さらにRegression Testにもなるのでオススメしたい手法です。
しかしgoldieには元々 WithEqualFn
オプションのような機能が無かったため、機能を追加してもらい使い倒しています。
Add customizable equality compare option by takashabe · Pull Request #41 · sebdah/goldie (github.com)
まとめ
Excelファイル(xlsx)のGolden TestをGoで行う方法と、どういう場面で有効かを紹介しました。xlsxファイルと向き合っているみなさまの生活が少しでも便利になれば幸いです。
いつもの
株式会社CastingONE では、OSSを使うだけでなくコントリビュートしてコミュニティに還元していくソフトウェアエンジニアを募集しております。Twitter(X)でもBlueskyでも良いのでお気軽にご連絡ください!
Goエンジニア│導入実績1,100社以上の自社プロダクト開発担当 - 株式会社CastingONEのWebエンジニアの採用 - Wantedly
バックエンドエンジニア│モダンな開発環境で経験を積みたい方歓迎! - 株式会社CastingONEのWebエンジニアの採用 - Wantedly
Discussion