概要
- DDDアーキテクチャで構築したプロジェクトにて、usecase層のテストを書こうとした。
- しかし、usecase層からは抽象化されたrepositoryを経由してDBアクセスしているため、repositoryの実装クラスからのDBアクセスをテストでどのように再現するかに行き詰まった。
- そこで、抽象化されたrepositoryを継承するmockを用意し、実装ではダミーのDBデータを返却するようにした。
- その際のデータ管理にgoldenファイルを使用し、go構造体とマッピングすることによりダミーのDBデータ返却を実現した。
- 本記事の内容ではmockやinterfaceといった作法は無視して書きます。(ソース用意するのが大変なので。。。)
goldenファイルとは
- JSON構造のファイルのこと。
- goの構造体のフィールド名、あるいは `json:"title,omitempty"``といった記述にマッピングすることができる。
前提
- go v1.17.2
- github.com/google/go-cmp v0.5.5
- 構造体比較のためにgo-cmpを使用しています。その他は標準ライブラリで実現しました。
1. goldenファイル読み出し処理の作成
-
goldenPath()
は引数のファイル名を受け取ってファイルパスを読み出す。 - 今回は
dummydata
というパッケージにgoldenファイルを格納する想定。 -
loadGoldenFile()
はファイルパスを受け取って、goldenファイルを読み出す。 - その後、goldenファイルのJSON構造を、引数で受け取ったobjにマッピングする。
func goldenPath(file string) string {
return filepath.Join("dummydata", file)
}
func loadGoldenFile(t *testing.T, path string, obj interface{}) error {
golden, err := ioutil.ReadFile(path)
if err != nil {
t.Fatalf("unable to read file: %s", err)
}
if err := json.Unmarshal(golden, &obj); err != nil {
t.Fatalf("unable to read file: %s", err)
}
return nil
}
2. 構造体の定義
- この構造体がgoldenファイルのフィールド値と対応する。
- `json:"title"``で対応させることもできますが今回は省略。
type Qiita struct {
data QiitaData
}
type QiitaData struct {
Title string
Description string
}
3. goldenファイルの作成
- いつも通りのjsonファイルですが、ファイル名は
test_qiita.golden
としています。 - Keyの方は上記で定義した構造体のフィールド名と一致させてください。
{
"Title": "qiitaTitle",
"Description": "qiitaDescription"
}
4. goldenファイルを使った構造体の初期化処理
- 上の手順で作成した
loadGoldenFile()
と構造体をここで使用しています。 - 別にこだわる理由もないのですが、ファイルパスは引数で渡せるようにしました。
func NewQiita(t *testing.T, path string) *Qiita {
var d QiitaData
if err := loadGoldenFile(t, path, &d); err != nil {
t.Fatal(err)
}
return &Qiita{
data: d,
}
}
5. テストの作成
- 上記で準備したメソッドや構造体を使用して、構造体のdiffを検証するテストを作成しました。
- 変数qiitaがgoldenファイルから読み出した構造体
- 変数wantQiitaが期待する構造体の値をハードコーディングしてます。
-
ignore := []cmp.Option{cmpopts.IgnoreUnexported(Qiita{})}
は非公開フィールドが持っていてもdiff検証をできるようにするためのオプションです。 - このテストコードを実行すると、テストがパスするはずです。
- テストがパスする = goldenファイルで定義した値が構造体にマッピングされている、と言えます。
func TestQiita(t *testing.T) {
ignore := []cmp.Option{cmpopts.IgnoreUnexported(Qiita{})}
testTable := []struct {
name string
qiita *Qiita
wantQiita *Qiita
}{
{
name: "Test_Qiita_OK",
qiita: NewQiita(t, goldenPath("test_qiita.golden")),
wantQiita: &Qiita{
data: QiitaData{
Title: "qiitaTitle",
Description: "qiitaDescription",
},
},
},
}
for _, tt := range testTable {
test := tt
qiita := test.qiita
wantQiita := test.wantQiita
if diff := cmp.Diff(qiita, wantQiita, ignore...); diff != "" {
t.Errorf("the value of the structure is different diff: %s", diff)
}
}
}
あとがき
- goldenファイルから構造体へのマッピング手法が分かったのではと思います。
- 実案件のコードでは、
NewQiita(t, goldenPath("test_qiita.golden"))
でmockを生成します。 - 一番下のfor文のところでmockのメソッドを呼び出すことで、goldenファイルで定義した値を返却値として受け取る、ような実装としています。
所感
- ほんとはテスト用DBを用意するのがよりよいのかな。。。
- repositoryの実装クラスでは当然DBアクセスをするためのクエリがあるので、そこを完全に無視ししてテストすることになってしまっているので・・