🦧

テスト可能 個人開発でクリーンアーキテクチャ...の補足

5 min read

前置き

こちらの記事の補足記事となります。

https://zenn.dev/maru44/articles/b9e07e91a0ea77

思っている以上に長くなりそうだったので別記事としました。
テスト可能について言及したいと思います。

みなさんは go でテストする際どのようなライブラリを使用していますか?
最近 ginkgo を使用し始めました。
自分の性格を考えると最終的に testing オンリーに回帰しそうです。

今回はとりあえず assert 用に testify を DB のモックとして sqlmock を使っていこうと思います。

リポジトリ層のユニットテスト

DB とのやり取りをする実処理の部分をテストします。
MVC で言うと models 層、クリーンアーキテクチャで言うとリポジトリ層になります。

MVC

models/blog.go
package models

import (
    ".../connector"
)

// blogの定義
type TBlog struct {
    ID      int    `json:":id"`
    Title   string `json:"title"`
    Content string `json:"content"`
}

// 全権取得は省略

// blog一件取得
func BlogDetail(id int) (b []TBlog, err error) {
    db := connector.AccessDB()
    defer db.Close()

    rows, err := db.Query("SELECT * FROM blogs WHERE id = ?", id)
    rows.Next()
    err := rows.Scan(
        &b.ID, &b.Title, &b.Content,
    )
    return
}

実際にテストは書きませんが、BlogDetail をほとんど模写するような形でモックを作らなければいけないのが一目でわかりますよね。
BlogDetail の引数に*sql.DB の型の変数を加えれば少しはマシになりますが、、、って感じです。

クリーンアーキテクチャ

一方でこちらは簡単です。少々面倒なのがどうしても DB とのやり取りの層なので DB のモックを作成しなければならない点です。まあ、こちらは MVC にも共通します。

まずこちらがテストの対象になるコードです。

interfaces/database/blog_repository.go
package databse

import ".../domain"

// blog repositoryはSqlHandlerを所持する
type BlogRepository struct {
    SqlHandler
}

func (repo *BlogRepository) FindById(id int) (b []TBlog, err error) {
    rows, err := repo.Query(
        "SELECT * from blogs WHERE id = ?", id,
    )
    defer rows.Close()

    rows.Next()
    err := rows.Scan(
        &b.ID, &b.Title, &b.Content,
    )
    return
}

テスト用にダミー SQL ハンドラー関数を作成した上で実装していきます。
見てわかるとおりけっこう長くて大変です。

interfaces/database/blog_repository_test.go
package database_test

import (
    ".../domain"
    ".../infrastructure"
    ".../interfaces/database"
    "database/sql"
    "regexp"
    "testing"

    "github.com/DATA-DOG/go-sqlmock"
    "github.com/stretchr/testify/assert"
)

// ダミーハンドラー生成関数
// どこか違うとこで定義して使いまわす方がお得
func newDummyHandler(db *sql.DB) database.SqlHandler {
    sqlHandler := new(infrastructure.SqlHandler)
    sqlHandler.Conn = db
    return sqlHandler
}

func TestFindBlogById(t *testing.T) {
    /*   prepare   */
    db, mock, err := sqlmock.New()
    if err != nil {
        t.Error("sqlmock not work")
    }
    defer db.Close()

    id := 30
    titile := "ブログその30"
    content := "ブログの本文\n今日も沢山プログラミングした。"

    rows := sqlmock.NewRows([]string{
        "id", "title", "content",
    }).
        AddRow(id, title, content)

    query := "SELECT * from blogs WHERE id = ?"
    mock.ExpectQuery(regexp.QuoteMeta(query)).WithArgs(id).WillReturnRows(rows)

    repo := &database.ReviewRepository{
        SqlHandler: newDummyHandler(db)
    }

    /*   prepare end   */
    /*   execute repository function   */

    blog, err := repo.FindById(id)

    /*   execute repository function end   */
    /*   evaluation   */

    assert.Equal(t, err, nil)
    assert.Equal(t, blog.Content, content)

    if err = mock.ExpectationsWereMet(); err != nil {
        t.Errorf("Test FindBlogById: %s", err)
    }
}

ユースケース層のテスト

テスト可能で真価が発揮されるのはこちらです。

テストの対象となるのはこちらのコードです。

usecase/blog_interactor.go
package usecase

import ".../domain"

type BlogInteractor struct {
	repository BlogRepository
}

func NewBlogInteractor(blog BlogRepository) domain.BlogInteractor {
	return &BlogInteractor{
		repository: blog,
	}
}

/************************
        repository
************************/

type BlogRepository interface {
	FindById(int) (domain.TBlog, error)
}

/**********************
   interactor methods
***********************/

func (interactor *BlogInteractor) DetailBlog(id int) (blog domain.TBlog, err error) {
        blog, er = interactor.repository.FindById(id)
	return
}

DetailBlog という関数をテストします。
モックは mock を使わずに手作りします。とても簡単に作れます。

usecase/blog_interactor_test.go
package usecase_test

import (
    ".../domain"
    ".../database"
    ".../usecase"
    "testing"

    "github.com/stretchr/testify/assert"
)

var (
    id      = 555
    title   = "タイトルその555"
    content = "本文です。\n今日もプログラミングした"
)

// BlogRepositoryインターフェースを満たすfakeBlogRepositoryを作る
type fakeBlogRepository struct {
    database.BlogRepository
}

// FindByIdメソッドをオーバーライド
func (repo *fakeBlogRepository) FindById(id int) (domain.TBlog, error) {
    fakeBlog := domain.TBlog{
        ID:      id,
        Title:   title,
        Content: content,
    }
    return fakeBlog, nil
}

func TestDetailBlog(t *testing.T) {
    mockBlogRepo := new(fakeBlogRepository)

    interactor := usecase.NewBlogInteractor(mockBlogRepo)
    b, err := interactor.DetailBlog(id)

    assert.NotError(t, err)
    assert.NotNil(t, b)

    assert.Equal(t, id, b.ID)
    assert.Equal(t, title, b.Title)
    assert.Equal(t, content, b.Content)
}

このテストで BlogInteractor の DetailBlog メソッドを実行した際に、BlogRepository の FindById メソッドが呼び出されていることがわかりました。

たったこれだけでテストができてしまいます。
非常に簡単ではないでしょうか?

コントローラー層も同じようにユースケース層の mock を作ってそれを注入すればできてしまいます。

まとめ

少し妥協してしまったのでちゃんとした比較になってない点はすいません。

しかし、クリーンアーキテクチャではテストを簡単に、かつ非常にクリーンに書けることが伝わってくれればうれしいです。

クリーンアーキテクチャのテスト可能

層ごとにテストの目的を明確にしたうえでテストできる
コントローラーやユースケースでは依存先のインターフェースをモックするだけで簡単にテストできる
依存関係が整理されておりテストが本番の処理とは分離されている