130
68

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

GolangでDBアクセスがあるナニットテストのやり方を考える

130
Last updated at Posted at 2019-12-01

こんにちは、こちらはLinc'wellアドベントカレンダヌの1日目です。

皆さんはDBに察しお曞き蟌みが発生する関数のテストをどのように行われおいるでしょうか
私はgolang初心者どころかサヌバサむド初心者なので、最適解が党くもっお分からなかったのですが、いろいろ調べた末「こんな感じで甚意するず良さそう」ずいうテスト環境・曞き方に萜ち着いたのでそれをここに蚘したす。

同じ悩みを持った方に刀断基準を提䟛できるような蚘事になっおいるず嬉しいです。(あわよくば匷い人からのフィヌドバック埅っおたす)

はじめに: どんなテストを曞きたいのか

はじめに芁件をクリアにするためにどんなテストを曞きたいのかの具䜓䟋を曞きたす。

ただ今私はECサむトの開発をしおいお、その䞭で定期賌入の仕組みを䜜っおいたす。
定期賌入はナヌザが定期賌入を申し蟌んだ際に「次にどの日付にどんな商品を賌入するのか」ずいうデヌタを持った Subscription ずいうデヌタを䜜り、日次でその日の日付が指定された定期賌入に応じお泚文を䜜るバッチ凊理を行う、ずいう仕組みで䜜りたす。

なので、テストの擬䌌コヌドを曞くずこんな感じです。

func TestCreateSubscriptionOrders(t *testing.T) {
	// Given: 今日発送予定のSubscriptionがある

	// When: Batchの関数を実行する

	// Then: そのSubscriptionに応じたOrderが䜜られる
}

今回テストを考えたいのはこのように「関数を実行し」「その関数を実行したこずによっおDBのデヌタが正しく倉わったこず」を確認したいケヌスです。なので、関数に察しおテストコヌド内から参照できるDBむンスタンスを枡しおあげる必芁がありたす。

このため、この蚘事では次の3぀の事柄に぀いお考えおいきたす。

  1. DBむンスタンスをどう甚意するか
  2. テストデヌタをどう甚意するか
  • 䞊蚘䟋のようにテストを実行するために前提ずしお特定のテストデヌタが倚々あるでしょう。これをどう甚意するず良さそうかを考えたす
  1. テストデヌタのクリヌンナップをどうやっお行うか
  • ナニットテストで前提条件やテスト察象の関数内でINSERTされたデヌタは他のテストに圱響を䞎えないよう消しおおきたいです
  • これをどのように行うかを考えたす

DBむンスタンスをどう甚意するか

倧方針ずしおは mock を甚意するかテスト甚にDBを立おるかのどっちかになるず思いたす。

mock

詊しに go-sqlmock のサンプルコヌドを芋おみたす。

GitHub - DATA-DOG/go-sqlmock: Sql mock driver for golang to test database interactions

func TestShouldUpdateStats(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
	}
	defer db.Close()

	mock.ExpectBegin()
	mock.ExpectExec("UPDATE products").WillReturnResult(sqlmock.NewResult(1, 1))
	mock.ExpectExec("INSERT INTO product_viewers").WithArgs(2, 3).WillReturnResult(sqlmock.NewResult(1, 1))
	mock.ExpectCommit()

	// now we execute our method
	if err = recordStats(db, 2, 3); err != nil {
		t.Errorf("error was not expected while updating stats: %s", err)
	}

	// we make sure that all expectations were met
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Errorf("there were unfulfilled expectations: %s", err)
	}
}

mock では mock.ExpectExec("UPDATE products").WillReturnResult(sqlmock.NewResult(1, 1))
のように「こういうSQLが来た時はこういう結果を返す」ずいう指定の仕方ができたす。
芁するに「期埅するSQLが来たこず」は確認できたすが「想定した倀がDBから返っおくるこず」は担保できたせん。
個人的にはこれだけで今回の芁件には合わないかなず感じたした。あず匊瀟のケヌスで蚀うず、sqlboilerずいうORマッパヌを䜿っおいお、それのSQLを再珟するのがめんどくさいず蚀う事情もありたした。ちゃんず探せば生SQL発行するメ゜ッドずかあるのでかもしれないですが

test甚のDBを立おる

次にテスト甚のDBを立おる方法に぀いお、色々あるずは思いたすが、探したずころ次のどっちかっぜいです。

  1. 開発甚のものず同じコンテナ内にテスト甚のDBを䜜る
  2. dockertestを䜿っお専甚のむメヌゞ䜜る

今の自分たちの開発環境だずすでに開発甚の postgres プロセスがあっお、そこにテスト甚のDB䜜っちゃうので簡単に枈たせられそうだったのでそうするこずにしたした。ちょっずナニットテストがDocker立おおいるこずに䞀抹の気持ち悪さがないわけではないですが、開発䞭は基本的に垞に動かしおいるものなので今のずころ困っおいないです。

CI環境ではどうしおいるのか

ただ今GitHub Actionsを䜿っおいるのでそれを䜿っおの䟋になりたすが、CI環境ではdockerも䜿わないで postgres立おお migration (goose)を流すずいうのをやっおいたす。

name: ci-backend
on: [push]
jobs:
  build:
    name: setup
    runs-on: ubuntu-latest

    services:
      postgres:
        image: postgres:10
        env:
          POSTGRES_USER: postgres
          POSTGRES_PASSWORD: postgres
          POSTGRES_DB: postgres
        ports:
          - 5432:5432
        options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
    steps:
      - uses: actions/checkout@master
      - uses: actions/setup-go@v1
        with:
          go-version: '1.12'
      - name: setup bash_profile
        run: |
          echo 'export GOPATH="$HOME/go"' >>~/.bash_profile
          echo 'PATH="$GOPATH/bin:$PATH"' >>~/.bash_profile
      - name: run migration
        working-directory: server
        shell: bash -l {0}
        run: |
          go get -u github.com/pressly/goose/cmd/goose
          goose --dir=internal/db/migrate postgres "user=postgres port=5432 password=postgres host=localhost dbname=postgres sslmode=disable" up
      - name: test server
        working-directory: server
        env:
          DB_PORT: ${{ job.services.postgres.ports[5432] }}
          DB_USER: postgres
          DB_PASSWORD: postgres
          DB_HOST_TEST: localhost
          DB_NAME_TEST: postgres
        run: |
          go test ./...

テストデヌタの準備

考えたい芳点は二぀あっお

  1. いかに簡単にテストデヌタを甚意するか
  2. いかに事前に甚意するテストデヌタが他のテストに圱響しないようにするか

䟋えば冒頭の定期賌入のケヌスに぀いお考えおみるず、「泚文を䜜るためにアクティブな定期賌入があるこず」を前提ずしおいたす。
なので「泚文が䜜られるこず」をテストしたい堎合は前提条件である定期賌入のデヌタを入れなければいけたせん。テストデヌタを甚意するのは非垞に詰たらない䜜業なのでなるべく簡略化したいずころです。

ただ、ではグロヌバルな seed デヌタのようなものに定期賌入のデヌタを入れお最初に入れればいいず蚀うものでもなく、䟋えば䞊蚘以倖に「定期賌読がない堎合に泚文が䜜られないこず」もテストしたいずなった堎合に䞍可胜になっおしたいたす。(それかなんらかデヌタが入っおいるこずを前提ずしお「そのテストの前に定期賌読のデヌタ消す」ずいう䜙蚈な知識が入っおしたいたす)

なので「テストデヌタを簡単に甚意するこず」ず「テスト間で扱うデヌタが干枉しない」ために考えたいのは次の二぀です

  • どのタむミングでデヌタをINSERTするのがいいか
  • テストデヌタを簡単に甚意する実装の仕方

どのタむミングでデヌタをINSERTするのがいいか

ざっくり蚀うず事前に seed みたいなのを入れるか各ナニットテストで前提条件ずしお入れるかです。(正確には二者択䞀ずいうより、テスト固有のデヌタは登堎するに決たっおいるので seedを甚意するかどうかずいう問いの方が正しいですが)

今埌は䜕かしら䜜るかもしれないですが䞀旊 seed のようなものは䜜らないこずにしたした。
理由ずしおはあるナニットテストがグロヌバルに䜜られたテストデヌタを前提ずしおいる時に、暗黙的な知識を持っおいるのがテスト自䜓の保守性䞋げおなんか気持ち悪いず感じたからです。そのテストだけ芋ればそのテストの前提条件は䜕で芋たいこずは䜕か分かるようにしたいず感じたした。なので倚少面倒は増えるかもしれないですがきっちり䞀぀䞀぀のナニットテストにデヌタの準備を曞いおいこうず思いたす。

ただ、いく぀かテスト曞いおみお思ったのは User みたいなどんなシステムでもコアずなる抂念はもう党おのテストの前提条件にしおしたっおも問題ないのではずいう気はしおきおいたす。こういったものは挞進的に改善しおいきたいなず思いたす。

テストデヌタを簡単に甚意する実装の仕方

月䞊みですがファクトリ関数を䜜っおいこうず思いたす。
「思いたす」ずいうのはこの蚘事執筆時点では倧しお曞いおないので、䜕も曞くこずが無いずいうこずを指しおいたす。頑匵りたす。

普通に自䜜で良さそうだけどこういうラむブラリ䜿った方が楜なのかな
GitHub - bluele/factory-go: A test fixtures replacement inspired by factory_boy and factory_girl.

3.テストデヌタのクリヌンナップをどうやっお行うか

倧きくは次の二぀かなず思いたす。

  • テスト内の凊理はトランザクションずしお持っおおいお、テストが終わったらロヌルバック。
  • 毎回デヌタを消す

結論から述べるず1個目の毎回トランザクション貌るようにしたす。理由はそちらの方が脳死で曞けお、特にデメリットも思い圓たらないからです。

具䜓的には次のような感じで曞くようにしたす。

func TestCreateAppSubscriptionOrders(t *testing.T) {
  // トランザクションはる
	tx := db.GetTestTransaction()

	// テスト

  // 終わったらロヌルバック
	tx.Rollback()
}

めっちゃシンプルですね。シンプルですが毎回曞くのもう䞀段楜にできないかず思ったので、beforeEach/afterEachした方がいいかずいうのも考えおみたした。

なぜ beforeEach/afterEach したいのか

分解するず次の二぀のモチベヌションがありたす。

  • うっかり曞き忘れを無くしたい
    • ただ、これを実珟したい堎合は党パッケヌゞのテストに察しお適甚するのが必芁ずなるができないっぜいです(できるなら教えお欲しい)
    • そうでない堎合、DBアクセスが必芁なテストのパッケヌゞ内で曞くこずになるが、それであればこの目的は満たせないので、このモチベヌションはどう転んでも実珟できない
  • 手間を枛らしたい
    • 同䞀パッケヌゞ内にかなりの数のDBを参照するテストがある堎合

ただ、残念なこずに golang 暙準の testing パッケヌゞには beforeEach/afterEach は存圚したせん。
実珟しようずする堎合は ginkgo などのテスティングフレヌムワヌクを䜿ったりする必芁があるので、それを入れるほどのモチベヌションではないかなヌず思いたした。

結論

頑匵っおそれぞれのテストケヌス内でトランザクションの開始ずRollbackを曞こう。
ただ関数ずしおは次のように曞いおおく。

package db

func GetTestTransaction() *sql.Tx {
	psqlInfo := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s",
		os.Getenv("DB_HOST_TEST"), os.Getenv("DB_PORT"), os.Getenv("DB_USER"), os.Getenv("DB_PASSWORD"), os.Getenv("DB_NAME_TEST"), os.Getenv("DB_SSL"))

	db, err := sql.Open("postgres", psqlInfo)

	if err != nil {
		panic(err.Error())
	}

	err = db.Ping()
	if err != nil {
		panic(err)
	}
  
	tx, _ := db.Begin()

	return tx
}

// 䜿う偎
func TestCreateAppSubscriptionOrders(t *testing.T) {
  tx = db.GetTestTransaction()
  
  /**********
  テストの凊理
  **********/
  
  tx.Rollback()
}

たずめ

私のプロゞェクトでは次のように甚意するようにしたした。

  • DBむンスタンスはそれ専甚のコンテナを立ち䞊げる
  • テストデヌタはSeedは甚意せずファクトリ関数のみで䞀旊行く
  • テストのクリヌンナップはトランザクションを甚いお行う

もちろんプロダクトの性質によっお方針も倉わるずは思いたすが、䞀旊これで進んでいきたす。
お読みいただきありがずうございたした。

远蚘

早速フィヌドバックいただけお激しく感謝なのですが、テストケヌス毎に db create, migration などを行うこずによっおクリヌンナップを実珟するずいうのが良さそうです。
私のトランザクションを甚いおやる方法は、党おのテストケヌスに察しお䞀回だけDB立おるこずを前提ずした時に䞀々テストに䜿ったデヌタを指定しおDELETEするのが面倒だったからで、この方法なら脳死で曞けるし「transactionが出来おいるこずを詊すテスト」も他のテストず違う曞き方せず実珟できるのでいいなず感じたした。

130
68
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
130
68

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?