目次
go言語を触っておきたかったのでプロジェクトの雛形を作る事にした。
自分は、新しい言語を触るときは予めテストの書き方やデバッガの設定、モックを注入する方法などを調べてプロジェクトの雛形を作る様にしている。
そんな訳でやっていく🤘
結論だけ見たい方は こちら ⇛ t-kuni/go-cli-app-skeleton
go言語をインストールする
インストール手順に従って進めていく。
特に詰まる点は無かったのでほぼ割愛。
以下の様なHello worldを書いてコンパイルして動くところまで確認した。
package main import "fmt" func main() { fmt.Printf("hello, world\n") }
Golandでプロジェクトを作成する
GolandとはJetBrains製のGO用IDEです。
クイックスタートを参考に進めていく。
Golandの実行ボタンを押すだけでビルドから実行までやってくれるっぽい。
ユニットテストのセットアップ
ユニットテストは何かとお世話になるので書き方を調べておく。
このドキュメントを参考にする。
ユニットテストの書き方は以下の通り。
- ファイル名の末尾に
_test.go
を付ける。 - 関数名は
TestXxx
の様にTest
から始める。
プロジェクトを右クリックして「Run」「go test ...」を押下するとテストが実行される。
ここでtesting: warning: no tests to run
というエラーが発生した。
調べた所、ユニットテストの関数名をtestXxx
の様にキャメルケース(小文字から開始)で記述していたためテストケースとして扱われなかったらしい。
ユニットテストの関数名はパスカルケースで書く必要があるとの事(詳しくは次章)
Golangの命名規則
ユニットテストでハマったので命名規則について調べる。
- 変数名やメソッド名
- キャメルケース(
hogeFuga
) or パスカルケース(HogeFuga
)で書く - キャメルかパスカルかで可視性が異なる。キャメルはprivate、パスカルはpublic
- キャメルケース(
- ファイル名
- スネークケース
キャメルかパスカルかで可視性が異なるのは入門者のハマりポイントって感じがしますねー
ユニットテストをデバッグ実行する
どうせユニットテスト書くならそのなかでブレークポイント掛けたりしたいよねという事でデバッグ方法を調べていく。
とりあえず素直にデバッグ実行してみる。
Golandで以下のエラーが発生した。
「ディレクトリのような実行構成ではコンパイルを実行できません」との事でGolandの実行構成がまずい事はなんとなくわかる。
Error running 'go test go-cli-app-skeleton': Cannot run compiling on directory-kind run configurations
GOPATH is empty
という警告が出ていることに気付いた。
GOPATH
とは?
ワークスペースの場所を表している。デフォルトでは$HOME/go
になっている。
ワークスペースとはGo コードを探す場所を表すらしく、パッケージをビルドしたりインストールしたりする際に使われるとの事。
このワークスペースにはsrc
フォルダとbin
フォルダが必要でsrc
フォルダには各種パッケージとそのソースコードが、bin
フォルダには実行可能なバイナリが配置されるとの事
という訳で~/.bashrc
にexport GOPATH=$HOME/.go
を追記した
しかし、これだけでは解決しなかった。
JetBrainsのフォーラムを眺めていると「プロジェクトをGOPATH内に配置しないとテストのデバッグ実行はできません」的な事が書かれているのを発見した。
という訳でプロジェクトを~/.go/src/
配下に移動した。
これによって、デバッグ実行でき、無事にブレークポイントが効く状態になった。
それにしても特定のフォルダ構造配下に自分のプロジェクトを置かなきゃいけないのはちょっとどうなんだという気持ちになる。
これがgo言語の仕様なのかGolandの仕様なのか入門したばかりの自分には良くわからないが・・・。
DIコンテナを導入する
ユニットテストを書くにあたって副作用を伴う処理はモック化するのだが、そのためにDIコンテナを使って依存性の注入を行う。
sarulabs/diというパッケージがあるのでこれを使ってみる。
Golandでは、import文を追記して該当の行でAlt+Enter
を押下するとパッケージをダウンロードできる。
DIコンテナの初期化はこんな感じになる。di.NewBuilder()
でDIコンテナの作成を開始し、di.Def
でサービス名とそれに対応する構造体を定義する。builder.Build()
で定義に従ってDIコンテナを作成する。
func createApp() di.Container { builder, _ := di.NewBuilder() builder.Add([]di.Def{ { Name: "quotation-generator", Build: func(ctn di.Container) (interface{}, error) { return SimpleQuotationGenerator{}, nil }, }, }...) return builder.Build() }
DIコンテナからサービスを解決するには[DIコンテナ].Get("サービス名")
を使う。
末尾の.(QuotationGenerator)
はキャストを行っている。
generator := app.Get("quotation-generator").(QuotationGenerator)
モック生成ツールを導入する
DIコンテナは導入できたので、golang/mockというモックを生成するツールを導入する
以下のコマンドで既存のコードからモックを生成できる。
mockgen -source=clock.go -package=main -destination=clock_mock.go
現在時刻を返す自作のインタフェースClockerからモックを生成すると以下の様なコードが生成された。
構造体MockClocker
を作成して、インタフェースに定義されている関数を生やしているのが分かる。
// 省略 // MockClocker構造体 type MockClocker struct { ctrl *gomock.Controller recorder *MockClockerMockRecorder } // MockClockerのコンストラクタ func NewMockClocker(ctrl *gomock.Controller) *MockClocker { mock := &MockClocker{ctrl: ctrl} mock.recorder = &MockClockerMockRecorder{mock} return mock } func (m *MockClocker) EXPECT() *MockClockerMockRecorder { return m.recorder } // 生成元のインタフェースで定義していた関数がモック仕様で実装されている func (m *MockClocker) Now() time.Time { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Now") ret0, _ := ret[0].(time.Time) return ret0 } func (mr *MockClockerMockRecorder) Now() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Now", reflect.TypeOf((*MockClocker)(nil).Now)) }
モックを使用するときはこんな感じ。defer ctrl.Finish()
のdefer
は関数を遅延実行させる仕組みらしい。テスト関数がreturnする時にctrl.Finish()
が呼び出されてモックの呼び出し結果が検証されるんだと思う。NewMockClocker
でモックを生成して、[モック].EXPECT().[関数名]().Return("戻り値")
で関数が呼び出される事の検証と呼び出された時の戻り値を設定している。
ctrl := gomock.NewController(t) defer ctrl.Finish() loc, _ := time.LoadLocation("Asia/Tokyo") clocker := NewMockClocker(ctrl) clocker.EXPECT().Now().Return(time.Date(2020, 1, 1, 0, 0, 0, 0, loc)) actual := RandomQuotationGenerator{Clocker: clocker}
.env
を使えるようにする
環境毎に処理を切り替えたりするために.env
ファイルを読める様にする。
joho/godotenvのREADMEにしたがってインストールする。
特にハマりポイントはなかったので割愛。
フォルダ構成をスタンダードに合わせる
Goにはディレクトリ構成のスタンダードがあるらしい。 という記事を参考にフォルダ構成を整えた所、以下の様になった。cmd
フォルダにはエントリポイントとなるソースコードを格納するらしい。app-name
はコマンド名と揃える必要があるとの事。internal
フォルダにはこのプロジェクトでしか使用しないコードを格納する。ほとんどのコードはここに配置されるはず。pkg
フォルダは他のプロジェクトとも共有するコードを格納する。
cmd
└ app-name
└ main.go
internal
├ clock.go
├ clock_mock.go
├ quotation_generator.go
└ quotation_generator_test.go
pkg
.env
おわりに
最終形はこうなりました。参考までに。
Webエンジニアをやっています
UX/UIデザインからプログラミング、DB設計、SEO、インフラ構築など幅広く対応してます
PHP/PHPUnit/Laravel/Vue/Nuxt/Docker/Terraform
ご連絡はTwitterのDMまで。