こんにちは!
NUTMEGのくぼ(kubosaka)です!
NUTMEGのアドカレ企画の6日を担当します
NUTMEGのプロダクトでは、おそらく導入されていない自動テスト導入しているとこはなかったと思います。 そこで、今回は自動テストについての紹介と運用しているプロダクト(FinanSu)でGoの実DBを用いたAPIテストを導入したので、ブログにします。
まず、自動テストについて簡単に紹介します
テスト自動化ツールやテストスクリプトを活用し、ソフトウェア評価におけるテストの実行や結果の確認といった、一連の工程を自動化することです。
以下のようなメリットとデメリットがあります!
運用しているプロダクトであれば、品質を高めるためにもテストの自動化は導入しましょう!
では本題です。
FinanSuにAPIテストの自動化を導入します。
使用している技術・フレームワークなどはこちら
今回は実DBを用いたAPIのテストを自動化を導入します。 DB側にモックを使うことも考えたのですが、サーバーとDB間の接続も確認したいと思いました。 RailsのRspecのcontorollerテストのようなものを想定して作成しました。
goのテストの際には、以下のパッケージを使いました。
開発環境のDBは、Dockerコンテナ上でMySQLを起動しています。 APIのテストを行う際DBが必要になりますが、テストで使うDBは開発環境で使うDBとは分けたいので、テストDBを作成します。
dokcerの起動には、docker composeを使用しています。
dockerのMySQL image では /docker-entrypoint-initdb.d/
というディレクトリ内に初期化用のSQLやスクリプトを置くことで、最初にコンテナを起動したときにDBの初期化を自動的に行うことができます。
この機能を使いテストDBの作成とテスト用テーブルの作成を行います。
開発環境のDBは以下のdbディレクトリのvolumeをdocker-entrypoint-initdb.dにマウントしています。
docker-compose.yml
services:
db:
image: mysql:8.0
container_name: "nutfes-finansu-db"
volumes:
- ./mysql/db:/docker-entrypoint-initdb.d # 初期データディレクトリ
- ./my.cnf:/etc/mysql/conf.d/my.cnf
environment:
MYSQL_DATABASE: ***
MYSQL_USER: ***
MYSQL_PASSWORD: ***
MYSQL_ROOT_PASSWORD: ***
TZ: "Asia/Tokyo"
ports:
- "3306:3306"
dbディレクトリ配下のsqlがコンテナ起動時に実行されます。
mysql ── db
│ ├── activities.sql
│ .
│ └── users.sql
│
└─────── docker-compose.yml
dbディレクトリ配下にテスト用DB作成に関するsqlファイルを追加すれば解決しそうですが、テスト用と開発用のディレクトリは明示的に分けたいと思い以下のようにしました。
mysql ── db
│ ├── activities.sql
│ .
│ ├── users.sql
│ └── init_create_db.sh
│
├────── testdb
│ ├── 01_create_testdb.sql
│ └── 02_test_users.sql
│
└─────── docker-compose.yml
docker-compose.ymlにテストdb用のディレクトリを新しく作成しvolumesでコンテナにマウントします。
docker-compose.yml
services:
db:
image: mysql:8.0
container_name: "nutfes-finansu-db"
volumes:
- ./mysql/db:/docker-entrypoint-initdb.d # 初期データディレクトリ
- ./mysql/testdb:/docker-entrypoint-testdb.d # テスト用初期データ ←追加
- ./my.cnf:/etc/mysql/conf.d/my.cnf
environment:
MYSQL_DATABASE: ***
MYSQL_USER: ***
MYSQL_PASSWORD: ***
MYSQL_ROOT_PASSWORD: ***
TZ: "Asia/Tokyo"
ports:
- "3306:3306"
init_create_db.shはdocker-entrypoint-testdb.d内のsqlファイルを実行するスクリプトです。
init_create_db.sh
#!/bin/bash
set -e
# docker-entrypoint-testdb.d内のSQLファイルを順番に実行
for sql_file in docker-entrypoint-testdb.d/*.sql; do
if [ -f "$sql_file" ]; then
mysql -u root -p$MYSQL_ROOT_PASSWORD < "$sql_file"
else
echo "SQLファイルが見つかりません: docker-entrypoint-testdb.d"
fi
done
echo "すべてのSQLファイルを実行"
DBを作成し、ユーザーに権限を与えます。(サーバーから接続する際のユーザーを使っています。また、テーブルも作成します。
01_create_testdb.sql
CREATE DATABASE finansu_test_db;
GRANT ALL PRIVILEGES ON `finansu_test_db`.* TO `{MYSQL_USER名}`@`%`
02_test_users.sql
use finansu_test_db;
CREATE TABLE users (
id int(10) unsigned not null auto_increment,
name varchar(255) not null,
bureau_id int(10) not null,
role_id int(10) not null,
is_deleted boolean DEFAULT false,
created_at datetime not null default current_timestamp,
updated_at datetime not null default current_timestamp on update current_timestamp,
PRIMARY KEY (id)
);
ここまででdbを起動すると、テストのdbが作られると思います。
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| finansu_db |
| finansu_test_db |
| information_schema |
| mysql |
| performance_schema |
| sys |
+--------------------+
mysql> show tables;
+---------------------------+
| Tables_in_finansu_test_db |
+---------------------------+
| users |
+---------------------------+
次のgoのテスト環境を作成します。
テストでは、テスト用のHTTPサーバーをコード内でたてて、そのサーバーに対して、リクエストを行いテストを行います。
goは自動テストの仕組みが備わっており、go test ~
で実行できます。
テストファイルを作成する際の注意点
_test.go
にする今回は、テストするインスタンスがDBと接続できないとエラーになるため、TestMainにDBの環境変数を定義しました。ここでは、テストDBの環境変数を定義してください。
sample_test.go
package test
import (
"io"
"net/http"
"net/http/httptest"
"os"
"testing"
"github.com/NUTFes/FinanSu/api/internals/di"
"github.com/stretchr/testify/assert"
)
func TestMain(m *testing.M) {
// テスト前処理
os.Setenv("NUTMEG_DB_USER", "***")
os.Setenv("NUTMEG_DB_PASSWORD", "***")
os.Setenv("NUTMEG_DB_HOST", "***")
os.Setenv("NUTMEG_DB_PORT", "3306")
os.Setenv("NUTMEG_DB_NAME", "finansu_test_db")
// テスト実行
code := m.Run()
// テスト後処理
os.Exit(code)
}
const helloTestMessage = "healthcheck: ok"
func TestSampleHelloHandler(t *testing.T) {
// インスタンスの生成(DB接続、ルーティング)
_, router := di.InitializeServer()
// サーバを立てる
testServer := httptest.NewServer(router)
t.Cleanup(func() {
testServer.Close()
})
r, err := http.Get(testServer.URL + "/")
if err != nil {
t.Errorf("Error making request: %s", err)
return
}
defer r.Body.Close()
body, err := io.ReadAll(r.Body)
if err != nil {
t.Errorf("Error reading response body: %s", err)
return
}
// テスト
assert.Equal(t, http.StatusOK, r.StatusCode)
assert.Equal(t, helloTestMessage, string(body))
}
sample_test.goでは、APIのルートへのGETリクエストをテストしました。
go test {ディレクトリのパス}
で実行します
レスポンスが、200で、“healthcheck: ok"と返ってきたので、サーバーが起動しテストをパスすることができました。
# go test ./test
ok github.com/NUTFes/FinanSu/api/test 0.018s
DBの接続はdi.InitializeServer()内で行ってます。 Testでは、godotenv.Load(“env/dev.env”)でロードに失敗するので、(パスがカレントディレクトリになる)別ディレクトリに分けるか、今回のように直接指定してあげるのがいいかと思いました。
err := godotenv.Load("env/dev.env")
if err != nil {
fmt.Println(err)
}
dbUser := os.Getenv("NUTMEG_DB_USER")
dbPassword := os.Getenv("NUTMEG_DB_PASSWORD")
dbHost := os.Getenv("NUTMEG_DB_HOST")
dbPort := os.Getenv("NUTMEG_DB_PORT")
dbName := os.Getenv("NUTMEG_DB_NAME")
// MySQLに接続する
// dbconf := "finansu:password@tcp(nutfes-finansu-db:3306)/finansu_db?charset=utf8mb4&parseTime=true"
dbconf := dbUser + ":" + dbPassword + "@tcp(" + dbHost + ":" + dbPort + ")/" + dbName + "?charset=utf8mb4&parseTime=true"
db, err := sql.Open("mysql", dbconf)
テスト環境が完了したので、テストコードを作成しましょう。
その前に、DBにテストデータを入れたいので、testfixturesを使いましょう。testfixturesはテストデータを作成するだけでなくテーブルのclean upもしてくれます。RspecのコントローラーテストのActiveRecord fixturesを参考に作られているみたいです。
https://github.com/go-testfixtures/testfixtures
使い方は<table_name>.yml
ファイルを作るだけで、テストデータを作ってくれます。
# users.yml
- id: 1
name: テスト太郎
bureau_id: 1
role_id: 1
created_at: 2020-12-31 23:59:59
updated_at: 2020-12-31 23:59:59
- id: 2
name: テスト花子
bureau_id: 2
role_id: 2
created_at: 2020-12-31 23:59:59
updated_at: 2020-12-31 23:59:59
以下テストコード sample_test.go
package test
import (
"database/sql"
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/url"
"os"
"testing"
"github.com/NUTFes/FinanSu/api/internals/di"
"github.com/go-testfixtures/testfixtures/v3"
"github.com/stretchr/testify/assert"
)
var (
db *sql.DB
fixtures *testfixtures.Loader
)
func TestMain(m *testing.M) {
var err error
os.Setenv("NUTMEG_DB_USER", "finansu")
os.Setenv("NUTMEG_DB_PASSWORD", "password")
os.Setenv("NUTMEG_DB_HOST", "nutfes-finansu-db")
os.Setenv("NUTMEG_DB_PORT", "3306")
os.Setenv("NUTMEG_DB_NAME", "finansu_test_db")
db, err = sql.Open("mysql", "{ユーザー名}:{パスワード}@tcp({ipアドレス}:{ポート番号})/{データベース名}")
if err != nil {
fmt.Println(err)
}
defer db.Close()
fixtures, err = testfixtures.New(
testfixtures.Database(db),
testfixtures.Dialect("mysql"),
testfixtures.Directory("fixtures"), // ここでymlファイルのディレクトリを指定する
)
if err != nil {
fmt.Printf("Error creating fixtures: %v\n", err)
return
}
// テスト実行
code := m.Run()
// テスト後処理
// db.Exec("DELETE FROM users")
if err != nil {
fmt.Print(err.Error())
}
os.Exit(code)
}
func prepareTestDatabase(t *testing.T) {
if err := fixtures.Load(); err != nil {
fmt.Println(err)
}
}
func TestGetUserHandler(t *testing.T) {
prepareTestDatabase(t)
_, router := di.InitializeServer()
testServer := httptest.NewServer(router)る
t.Cleanup(func() {
testServer.Close()
})
r, err := http.Get(testServer.URL + "/users")
if err != nil {
t.Errorf("Error making request: %s", err)
return
}
defer r.Body.Close()
body, err := io.ReadAll(r.Body)
if err != nil {
t.Errorf("Error reading response body: %s", err)
return
}
assert.Equal(t, http.StatusOK, r.StatusCode)
assert.Contains(t, string(body), "テスト太郎")
}
/users
はユーザー一覧を取得するAPIです。テーブルにユーザーのテストデータを用意し、レスポンスにテスト太郎が返ってくるテストです。
最後にPOSTのテストも作成します。
func TestAddUserHandler(t *testing.T) {
prepareTestDatabase(t)
_, router := di.InitializeServer()
testServer := httptest.NewServer(router)
t.Cleanup(func() {
testServer.Close()
})
u, err := url.Parse(testServer.URL + "/users")
if err != nil {
return
}
// クエリパラメータ追加
q := u.Query()
q.Set("name", "技大太郎")
q.Set("bureau_id", "1")
q.Set("role_id", "1")
u.RawQuery = q.Encode()
fmt.Println(u.String())
r, err := http.Post(u.String(), "application/json", nil)
if err != nil {
t.Errorf("Error making request: %s", err)
return
}
defer r.Body.Close()
body, err := io.ReadAll(r.Body)
if err != nil {
t.Errorf("Error reading response body: %s", err)
return
}
assert.Equal(t, http.StatusCreated, r.StatusCode)
assert.Contains(t, string(body), "技大太郎")
}
テスト実行後もテストDBのテーブルは空なので、clean upもしてくれてますね。
mysql> select * from users;
Empty set (0.00 sec)
長くなりましたが、Goの実DBを使ったAPIテストの導入について紹介させていただきました! フロントのテストも導入したいですね。導入した際には、ブログを書くかもです。 まだまだNUTMEGのアドカレは毎日更新です。色々な内容があって面白いと思うのでぜひご覧ください!
テスト自動化とは? ツール導入のメリットや流れを徹底解説 Go言語でテストコードを書いてみよう GoのWebアプリをテストするノウハウ Go言語のHTTPサーバのテスト事始め Go Test Fixtures テスト(go test/testing)