20211022のGoに関する記事は3件です。

【Golang】gRPC単位のテストで、テスト雛形を自動生成したらだいぶ楽になれた話

gRPC単位の結合テストを書く際、雛形を自動生成してテストを書けるようにした話です。 gRPCのテストでなくても、他の様々なテストに似たような活かし方ができそうと思ったので書いています。 注意書き テンプレートのライブラリとして text/template を使うともっとスマートにできることを知ったので、修正予定です。 GoでgRPC単位のテストを書くときの難点 Goで単体テストを書く場合、InteliJやVSCodeのプラグインには、コードの雛形を自動生成してくれる機能があります。 しかしgRPCやAPIなど、リクエスト単位のテストを書く際は、テストファイルを作って、テストを書いて...というステップをすべて自分で行う必要があります。 そのため、自動化できるところは自動化してしまおうと思ったのが今回の試みのきっかけです。 また、Go製のコード自動生成ツールはいくつかあるのですが、今回自動生成するものは割と小さめなコードでパターンがほとんど決まっていたので、お手製で自動生成するコマンドを作成しました。 テストファイルの単位 gRPC単位テストでやりたかったことは、「gRPCを叩いてそのレスポンスをアサートする」というのを、基本的にすべてのAPIに対して1テストケースは用意しておこうというものでした。 テスト対象が関数ではなくgRPCのリクエストになるので、1テストファイル=1種類のgRPCとしました。 例えば、 createBook というgRPCがあった場合、 create_book_test.go というファイルを作ります。 その中に、 createBook gRPCに対する様々なテストケースを書いていく感じです。 gRPCのテストケースごとに共通していた処理 新しいテストファイルを作る際、他のテストファイルでやっている同じような処理を毎回コピペしたり書いたりするのが大変だったのが、自動生成のモチベでした。 gRPC単位のテストを書き始める際、共通していたのが主に下記の処理です。 テストファイルを作成する テストを実行する関数を作成する テストの一番最初に、特定の環境に繋ぎこむgRPC Clientを生成する テスト実行後に、gRPCのコネクションをCloseする 上記を丸っと自動生成するようにしました。 実装 CreateBookというgRPCをテストする場合の例です。 テストの例 create_book_test.go package scenarios import ( "context" "testing" "github.com/kmurata/book-sample/grpcclient" "github.com/kmurata/book-sample/bookservice" ) type createBookTest struct { t *testing.T cli *grpcclient.Client connClose func() } func Test_CreateBook(t *testing.T) { test := createBookTest{t: t} test.setUp() t.Run("CreateBookが成功する", func(t *testing.T) { req := &bookservice.CreateBook{ Title: "読唇術のススメ" } // gRPCを叩く err := test.cli.CreateBook(context.Background(), req) if err != nil { test.t.Fatalf("failed to create book: %s", err.Error()) } }) test.tearDown() } func (c *createBookTest) setUp() { var err error c.cli, c.connClose, err = Initialize() if err != nil { c.t.Fatalf("failed to Initialize: %s", err.Error()) } } func (c *createBookTest) tearDown() { c.connClose() } テストファイルごとに共通になる部分を雛形にする 例えば上記のCreateBook以外に、GetBookというgRPCのテストを書く際にも、同じようなコードになりそうな部分を雛形にします。 主に createBook, CreateBook, c とある部分をテンプレートタグとして扱っていきます。 cmd/gen_api_test/scenario_test.go.tpl package scenarios import ( "testing" "github.com/kmurata/book-sample/grpcclient" ) type _apiName_Test struct { t *testing.T cli *grpcclient.Client action *actions.Action connClose func() } func Test__ApiName_(t *testing.T) { test := _apiName_Test{t: t} test.setUp() // -- Implement here -- t.Run("テストケース", func(t *testing.T) { }) // ---- test.tearDown() } func (_recv_ *_apiName_Test) setUp() { var err error _recv_.cli, _recv_.connClose, err = Initialize() if err != nil { _recv_.t.Fatalf("failed to Initialize: %s", err.Error()) } } func (_recv_ *_apiName_Test) tearDown() { _recv_.connClose() } テンプレートタグとして、下記の文字列を用意しました。 _apiName_ : gRPC名のローワーキャメルケースが入る部分(createBook) _ApiName_ : gRPC名のアッパーキャメルケースが入る部分(CreateBook) _recv_ : レシーバーの変数名が入る部分(c) これらのテンプレートタグを、新しくgRPCのテストファイルを作った際に良い感じに置換してくれるようにします。 自動生成コマンド cmd/gen_api_test/main.go package main import ( "bufio" "fmt" "io/ioutil" "log" "os" "strings" "github.com/iancoleman/strcase" ) const ( tplPath = "./cmd/gen_api_test/api_test.go.tpl" outputDir = "./scenarios" tplCamelTag = "_ApiName_" tplLowerCamelTag = "_apiName_" tplRecvTag = "_recv_" ) // templateを読み込み、テストケースのファイルを生成する func main() { log.Println("=== APIテストファイル生成開始 ===") tplContent, err := readTpl(tplPath) if err != nil { log.Fatal(err) } snakeApiName := scanSnakeApiName() newContent := replaceTplContent(tplContent, snakeApiName) outFilePath := fmt.Sprintf("%s/%s_test.go", outputDir, snakeApiName) if err := writeFile(outFilePath, newContent); err != nil { log.Fatal(err) } log.Printf("API名: %s\n", snakeApiName) log.Printf("出力先: %s\n", outFilePath) log.Println("=== APIテストファイル生成成功 ===") } func scanSnakeApiName() string { fmt.Printf("テスト対象API名を指定してください。(例: create_book)\n>> ") scanner := bufio.NewScanner(os.Stdin) var answer string for scanner.Scan() { answer = scanner.Text() if answer == "" { fmt.Println("空文字は使用できません。再度入力してください。") continue } break } return answer } func readTpl(filePath string) (string, error) { bytes, err := ioutil.ReadFile(filePath) if err != nil { return "", fmt.Errorf("ファイル読み込みに失敗: %w", err) } return string(bytes), nil } func replaceTplContent(tplContent, snakeApiName string) string { r1 := strings.ReplaceAll(tplContent, tplCamelTag, strcase.ToCamel(snakeApiName)) r2 := strings.ReplaceAll(r1, tplLowerCamelTag, strcase.ToLowerCamel(snakeApiName)) r3 := strings.ReplaceAll(r2, tplRecvTag, snakeApiName[0:1]) return r3 } func writeFile(outFilePath, content string) error { if err := ioutil.WriteFile(outFilePath, []byte(content), 0777); err != nil { return fmt.Errorf("ファイル書き込みに失敗: %w", err) } return nil } やっていることはシンプルで、 テスト対象のAPI名の入力を促す 入力されたAPI名から、テストファイルをテンプレートをもとに自動生成する といった感じです。 実行 $ go run cmd/gen-api-test/main.go 2021/08/30 15:25:08 === APIテストファイル生成開始 === テスト対象API名を指定してください。(例: create_book) >> create_book 2021/08/30 15:25:16 API名: create_book 2021/08/30 15:25:16 出力先: ./scenarios/create_book_test.go 2021/08/30 15:25:16 === APIテストファイル生成成功 === すると、下記のようなテストケースが作成されました! create_book_test.go package scenarios import ( "context" "testing" "github.com/kmurata/book-sample/grpcclient" "github.com/kmurata/book-sample/bookservice" ) type createBookTest struct { t *testing.T cli *grpcclient.Client connClose func() } func Test_CreateBook(t *testing.T) { test := createBookTest{t: t} test.setUp() // -- Implement here -- t.Run("テストケース", func(t *testing.T) { }) // ---- test.tearDown() } func (c *createBookTest) setUp() { var err error c.cli, c.connClose, err = Initialize() if err != nil { c.t.Fatalf("failed to Initialize: %s", err.Error()) } } func (c *createBookTest) tearDown() { c.connClose() } あとはテストケースの中身だけ書いていけばOKですね。 最後に 今回はgRPC単位の結合テストにおいて、テストファイルを共通処理含めて自動生成したお話を書きました。 様々なシーンにおいて似たような活かし方ができるかと思うので、ぜひ参考にしてみてください。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Goのポインタに関するお話

今回のお話 今回のお話はGoのポインタです。 Goは基本的に守備範囲外なのですが、面白かったので自分なりにまとめます。 目次 ポインタとは ポインタのメリット ポインタの取得方法 ポインタ型変数 ポインタから変数を取得する方法 実際に使ってみよう ポインタとは ポインタとは、メモリ上のどこに変数の値が保存されているのかを表したものになります。 いわば、変数の住所のようなものですね。 ちなみに、ポインタというのはGoの世界での呼び名であり、他の言語ではアドレスと呼ぶこともあるようです。 ポインタは基本的に16進数で表されます。 ポインタの例 0xc123456789 ポインタのメリット ポインタの一番のメリットは、「スコープ外の変数を利用できる」という点です。 スコープとは、変数が利用できる範囲のことですね。 一般に、変数はその関数の外側に持ち出すことができません。 以下がその例です。 スコープによる制限の例 func main(){ a := 10 addOne(a) // 出力:11 println(a) // 出力:10 } func addOne(a int){ a++ println(a) } func main()以下を見ると、addOneで呼び出した結果は11ですが、その後に変数aを出力すると10に戻っていますね。 func mainで定義した変数aとaddOneに渡したaは同一ではなく、あくまでコピーを渡しただけなのでこのようなことが起こります。 もし、addOneを用いてaの値を更新したければ、以下のようにする必要があります。 スコープによる制限の例 func main(){ a := 10 a = addOne(a) println(a) // 出力:11 } func addOne(a int)int{ a++ return a } ですが、今回のお題であるポインタを用いると、addOneメソッドの中で直接aの更新ができるようになります。 ポインタの取得方法 では、肝心のポインタはどのように取得すればよいのでしょうか? 答えは簡単で、変数名の先頭に&をつければポインタを取得できます。 name := "kenn" println(&name) //結果:0xc123456789 ポインタ型変数 ところで、Goには変数や引数を定義する際には型を明示するというルールがありました。 ポインタが代入された変数をポインタ型変数と呼ぶのですが、ポインタ型変数の型はどのように記述するのでしょうか。 実は、ポインタ型変数に関しては専用の型は用意されておらず、参照している変数の型の先頭に*をつけたものがポインタ型変数の型になります。 ポインタの例 num := 10 name := "taro" var numPtr = *int var namePtr = *string numPtr = &num namePtr = &name これで、ポインタを変数に入れることができるようになりました。 では、今度はポインタから変数にアクセスしてみましょう。 ポインタから変数を取得する方法 変数名の先頭に&をつけるとポインタが取得できました。 逆に、ポインタ型変数の先頭に*をつけると変数の値が取得できます。 ポインタから変数を取得 num := 10 var numPtr int* numPtr = &num println(*numPtr) //結果:10 では、これを用いて実際にスコープ外の変数にアクセスしてみましょう。 実際に使ってみよう 最初にお見せしたmain, addOneを以下のように書き換えます。 ポインタを用いた書き換え package main func main(){ a := 10 addOne(&a) println(a)// 結果:11 } func addOne(aPtr *int){ *aPtr++ } これまでのようにaddOneの結果をわざわざaに再代入することなくaの値が更新されています。 終わりに 以上がポインタのお話でした。 これまでに学習した言語にはない概念だったので最初は戸惑いましたが、結果的に変数のスコープに関する理解が深まったように思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

あったらいいなと思っていたスキルマップアプリをVueとGoで作ってみた

作ってみたもの 作ったWebアプリは Graphyee と名付けました。 技術と技術は関連しているものなので、「スキルマップをグラフ構造で表現できたら分かりやすくて面白いかな」と思いついたのが経緯です。 まだβ版としていますが、一旦使える感じになったのでノリと勢いで公開してみました。 こんな感じで サンプル は誰でも使えます。 サンプルは実績を入力しても保存できないのでご注意下さい。 ユーザ認証すると、より細かい スキルマップ が使えます。 ユーザ認証していただければ、入力した実績が保存されます。 上記リンクからだとAuth0の認証画面へのリダイレクトにやや時間がかかりますが、そのうち認証画面が出てきます。 技術構成 せっかく作ったので、どうやって作ったかを簡単にご紹介します。 フロントエンド Vue 2.6.11 Vuetify 2.4.0 Vuex 3.6.2 axios 0.21.1 Cytoscape.js 3.18.1 Auth0 VueでSPAを作り、UI周りのデザインはVuetifyを活用しました。バックエンドのAPI呼び出しはaxiosです。 Cytoscape.jsを使ってグラフ構造を実現しています。jsonでnodeとedgeを定義するとグラフが描画されます。 sample_node [{"id":"1","name":"node1"},{"id":"2","name":"node2"},{"id":"3","name":"node3"}] sample_edge [{"source":"1","target":"2"},{"source":"2","target":"3"}] 認証機能は、アプリから切り離したかったのでAuth0で実現しています。 Silent AuthenticationやRefresh Token Rotationはこのサイトが理解しやすかったです。 バックエンド Go 1.16 Gin 1.7.1 GORM 1.21.11 フロントエンドから呼び出されるAPIは、GoとGinで実装し、GORMでDBと接続しています。 Goはパッケージ管理の考え方がまだ模索中なのかな?って印象ですが、そこさえちゃんと理解できれば早く立ち上がるので気に入ってます。Ginも簡単なWebアプリ作るなら一瞬だったので、いい感じです。世の中にサンプルも多そうなでキャッチアップしやすい印象です。 GORMはいわゆるORMのクセみたいなのはありましたけど、普通に使う分には問題なく使えました。 最終的には、生のSQLを書きたくなるんですけどね。 インフラ インフラはAWSで構築しました。コンテナ化や触ったことないサービスを使ってみようかなとも思いましたが、個人的に一番立ち上がりが早いサービスを選択しています。 ALB+EC2+RDSというとてもシンプルな構成です。 ここらへんを見ながら何使うか一瞬悩みましたが、また今度挑戦しようかなと思います。 なぜ作ろうと思ったのか IT業界で働いていて、「結局、具体的に何をどこまで経験してきて、何が不足しているのか?」を常々考えていたのが根底にあります。さらに、ある技術を網羅的に学んで仕事に活かすことが、自分にとって「塗り絵」をしていくイメージだったのも強く影響しています。 ただ、いちいち思い出して次は何しようかを考えたり、実績を誰かに伝えたりすることは中々面倒くさいのです。そこで、スキルマップを分かりやすく管理できればいいのでは?と思い立ちました。 使えそうな場面 例えばこんなシーンで使えるのかなと想像しています。是非、継続的に使ってみてください。 特にこれから勉強しようとしている人、勉強中の人向けに使えるのかなと想像しています。 学習や経験が網羅的にできているか知りたい。 学習や経験を計画したり、振り返ったりしたい。 今後どんなことを学習・経験すべきか知りたい。 自分や誰かの足りない部分を発見したい。 使ってみた方へお願い 求められるものが何なのか、今後作り続ける価値があるのか知りたいので、よろしければアドバイスいただけると嬉しいです! 今後 やりたいことや検証してみたいことはたくさんあります。 もし良いフィードバックをいただけたら、下記以外にも色々試してみたいと思います。 コンテンツがJavaだけで寂しいので追加したい。 認知度を向上して色々フィードバックをもらいたい。 誰かのスキルマップとの比較機能を付けたらありがたがられるのか。 API Gateway+コンテナ(Lambda/Fargate)にしたら嬉しいのか。 Github ActionsでCI/CDしたら楽になるのか。 ランニングコストぐらい回収できる感じにしたい。 課金して使える機能と課金の仕組みを入れてみたい。 最後に 作っていて記事になりそうなネタがあればまた投稿するので、その時にまたお会いしましょう。最後までお読みいただき、ありがとうございます!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む