🍮

初チーム開発とテストとgolang

2021/10/31に公開約8,100字

とある会社の制度で開発物を提出することになりこれ好機と思いコミュニケーション能力改善のためチーム開発を始めた。しかし締め切りが早まるなどてんやわんや。

golangこと始め

golangについては月刊誌ウェブDBプレス[1]の記事にて多少読んだことがある程度の知識であった。社内で利用されているgolangのWebアプリケーションフレームワークはgin[2]であったことからWebアプリケーションフレームワークにはチームによる議論でginが選ばれ、またJavaScriptフレームワークにはVue.jsが選ばれた。(SPA+API server)

言語仕様に沿ったプログラミングのためgolangの作者の気持ちを知ることから始めた。その結果クラスが存在しないこと[3]、interfaceの気持ちを知った[4]。interfaceの気持ちというのは「インターフェイス定義はコンシューマ側から」というものでルーチンは注入されるオブジェクトに対し何を求めるのかをインターフェイスに定義するというものだ。このとき私はgolangの肝を掴んだと言っても過言ではない。structはコンシューマが求めるメソッドを定義する。しかしながら、コーディングしているうちにこのことを忘れ、盛大なミスをすることとなる。

goを用いたシステム開発サンプルを探る

開発を始めるにあたりシステム開発のサンプルを検索した結果フロントからバックエンドにかけて網羅的に記事してくれている記事が3件ほど見つかった[5,6,7]。この結果システムの大まかな構成を想定することが可能となった。しかしながら当時はimport数が多くなるそれらの構成を美しくないかつコピペで実用できるコードに達していないと判断しgithubを中心とした調査に切り替えてClean Architectureに則って実装しているGo REST API starter kit[8]なるものを見つけ特に複雑なシステムではなかったものの学ぶ良い機会だと思いこれを中心に開発を進めることにした。チームメンバはClean Architectureにあまり興味がない様子であったため共有するに留めた。 routerがozzo-routerであることが難点であったけれどもgolang初心者の私にはginへの書き換えは丁度良い課題だった。

またディレクトリ構成についてはgolang-standards[9]のプロジェクトテンプレートを参考にしこれをメンバに提案した結果快く受け入れてくれた。

contextとはなんぞ

これをルーチンに渡すことであるgoroutineから派生していった実行系についてキャンセル、タイムアウトがまとめて管理できる[10]。

調子に乗って0Authのようなトークン式認証(ステートレス認証)を自前で実装することになるo

当初BASIC認証(RFC 2617)でいいだろうと思っていたもののその週はなぜか"JWT"の文字列を頻繁にサジェストされる記事タイトルで見ていた。そんな折、フロント担当からJWTで認証しませんか?との連絡が来た。Go REST API starter kit[8]でもJWTの文字列は見ていたためコピペすれば実装は容易だろうと二つ返事で了承した。

TDDで開発しようとしたものの...

なぜTDDか、今回フロントとバックエンドで同時進行する形を取ったため一発で動作させることを目標としていたためだ。しかし時間的な焦りからしばらく一通り書いて時間に余裕があることを確認してからテストを書くという行為が慣例化した。流れは例のリポジトリ[8]をコピペしてリファクタリングしてapiの体裁を整える。テストを書くというものだ。テストは以前より勉強していて[11]コーディングコストの観点から限界値テストと条件網羅テストをバグ予測に基づいて実施すると決めていた。なお、初めて書いたapiのテストは不必要、不完全なテストとして破棄している。

テストの重要性

限られたリソースと実用的な観点からテスト結果の確認コストの観点から全てのルーチンについてテストがあればいいというものではなくバグ予測とそのルーチンのシステム及び、サービスとしての重要性の観点からテスト対象を絞るべきだと考えている。しかしながらテストは重要性関係なく往々にして自らの書いたコードのバグを浮き彫りにしてくれることも事実だ。

今回は入力内容のバリデーションについてのテストと認証機能のテストにコストを割いた。入力内容の検証は入力を制限するためシステムの予期しない動作の可能性を減らしてくれる効果が期待できる上にそのテストではどのようなデータが実際にシステムのIOを行き交うのか明確にしてくれるため他のルーチンを記述する上でのバグも抑制してくる効果があり費用対効果の高いものと考えられる(本当?)。

いざ行かんJWT

JWTとはRFC7519にて定義されJSONを電子署名したurl-safe(URLで使用できない文字が含まれる)なclaimのことを指し[12]これによる認証方法を探るため複数のWebページを訪れた[13-17]。おおよそのフローは理解できたもののなぜリフレッシュトークンとアクセストークンの二つがあるのか釈然としていなかった。これを解決してくれたのが「漏洩リスクが最も高いのはネットワークを流れている時だから」[18]という回答で今となっては私の常識となってしまった。逆に、現在何故釈然としていなかったのか当時のことは不明だ。

今回のアプリでは実装方針としてトークンはただのデータ登録の権利となっている。真正性はリフレッシュトークンを秘密鍵としてクライアント側で送信データに署名すればトークン認証における上限まで引き上げることが可能と思われるが今回は見送った(最初アクセストークンで署名すればいいのでは!?と考えたのは内緒)。

ユーザIDでひと悶着

ユーザIDをUUIDにしようと考えていたがとあるメンバはintを前提に既に開発を始めてしまっていてUUIDをintとして扱おうにもgolangは標準でint128をサポートしていない[19]。このためDBテーブルのidをそのままユーザIDとして扱うことになった。このため私の一部の処理はORMの登録した際にidが返ってくる機能を利用した逐次処理による実装を行い任意のユーザについての各テーブルにおけるIDの整合性を保った。UUIDをルーチン内で生成するのであれば、特に実装するつもりもなかったものの複数のテーブルへの登録が並行処理でできた。

そして後々とあるメンバのコードにてginのBindJSONルーチンが構造体の構成によってはintを正常にパースできない問題が発生したためそのメンバはIDをstringで扱っていた。脳死していて私はこのとき「intでもう少し頑張って下さい」と言っていたもののUUIDに変更する良い機会であったとほぼ全て終わり一人反省会をしている時に気づいた。あほ。

フロントエンドとバックエンドの時刻仕様

golangのtime.Dateの時刻フォーマットはRFC 3339であったもののチャットアプリやbacklogで私含めメンバそれぞれが多少なりとも時刻の扱いについて問題を抱えている雰囲気を醸し出していた?ため戦々恐々としていた。ビデオ通話の際にフロントエンド(javascript)のデフォルトフォーマットもRFC 3339であると確認できたため一安心。そりゃそっか。とはいえ「それはそう」という考えは時にはあてが外れると知っているので確認しておいて損はない。

repositoryのテスト

ORMの採用とgormの採用が決まっていたのでgo-sqlmockを用いたテストを書き始め私はこのとき初めてgormがテーブル名を勝手に複数形にしおるのだと知った。また、actualとされたSQLをexpectedにコピペしてテストを通すという作業が慣例化し目的と手段を逆転させた。ピエロよろしく大道芸人だ(逆立ちオーギュスト)。また、api、service、repositoryを一通り書き終わりgo-sqlmockを利用した統合テストを成功させた後にデータベースとの通信がつつがなく成功するのか確認するという作業が依然として残る結果になった。

結論としてrepositoryのテストはモックを用いずに実際に運用するデータベースサーバを用いるべきだろう。なお、愚かさがにじみ出るテストコードはその後抹消した。意味のないコードならないほうがまし。

認証モジュールの実装で事故る

当初ついでに"EZ0Auth-go"というOSSなんてついでにリリースしてみるかという軽いノリでauthenticationモジュール(仮称)とauth22モジュールに分けて実装していた。authenticationモジュールはserviceにgin.contextを渡すのはナンセンスという考えからapiがファットになりがちな?認証モジュールのapi部分を担い、また同じくボイラーテンプレートとなるserviceのメソッドはauth22モジュールにて埋め込みで実装させる設計であったがなにを間違えたのか、従来のパラダイムの感覚でライブラリを構築してテストを実行するとDispatch backされない罠(言語仕様)に引っかかった[20]。

認証モジュールの実装で事故る その2

汎用化は諦めて一本化して実装していたものの、再び躓いた。golangでライブラリを書く際には「状態を表す変数xを持たせてステートフルにしてインスタンス化して...」という考えは一切通用しない。引数で受け取って引数に渡して戻して戻してというステートレスな処理しか通らん。JWTってほら、mapにクレームを記述してシリアライズして署名する処理で完成するのですけれどクレームの記述についてステートフルな形で実装していたのですよ。そしたらね、mapは参照型なわけですからmakeで領域割り当てるルーチンをコール、有効期限を書き込むルーチンをコール、SEGV!!あれ?SEGV!!うわぁ...

認証モジュールの実装で事故る その3

golangでは受け身の精神が重要だ。だからといって自身のメソッドに同一の構造体のメソッドからアクセスできないなんて...つまり、golangではエンティティの切り分けをしっかりと行い適切なレベルでDIするモデルが重要なのではなかろうか。切り分けがしっかりできていれば「あれ、interfaceに自身のinterface名を記述して...無理じゃん」ということがなくなる。なんでそな思想を持つ至るかって「その2」で言及したように引数で渡してあげる必要があるから。エンティティの切り分けはむしろ強制されているといっても過言ではない。あるコンテキストにおいてこの問題を速やかに回避するためにはクロージャという手がありまして....何やら今回はびこってしまった結果については反省の余地がある。

やっとの思いで認証モジュールを完成させるもメンバの一人は作業を終えている!

やーばい。すみません。

フロントの人:「jsonにあれとこれ追加でいいですか?」私:「いいですよ!」

崩れ行くテストコード。工数見積もり下手かっ。

ちなみに実際の追加はgormのPreloadで簡単にできた。ORMの利点ってアホに組ませてもインジェクションの危険性をなくせるくらいにしか知らなかったのですけれどいいとこあるじゃん。

(N+1)問題あるあるの状態でこんな便利恐ろし機能使ってSQLがどうなっているか知らない(ガクガク)。

(((学校が始まり)))中途半端にコミット

#bug #bug #bug ...

本当に申し訳ありませんでしたぁ!!!自分の作業に責任を持つことの大切さを身をもって知った。bugで言及されるたびに「胸がっ張り裂けそうだっ」(某テレビ番組)となった。

ようやっと完成

ありがとうございました!


参考文献

[1] WEB+DB PRESS|gihyo.jp … 技術評論社
[2] gin package - github.com/gin-gonic/gin - pkg.go.dev
[3] Go言語で「embedded で継承ができる」と思わないほうがいいのはなぜか? - Qiita
[4] WEB+DB PRESS vol.103 2018 連載「Goに入りては… When in GO… 第5回」
[5] Go言語(Golang)でMVCモデルを実現する 前編
[6] Go/Gin/Vue.js/MySQLで超簡単なSPAを開発 - Qiita
[7] Vue.jsとGo言語で簡単なWebアプリを作成する(Dockerで開発環境構築) - Qiita
[8] qiangxue/go-rest-api: An idiomatic Go REST API starter kit (boilerplate) following the SOLID principles and Clean Architecture
[9] golang-standards/project-layout: Standard Go Project Layout
[10] Go1.7のcontextパッケージ | Taichi Nakashima
[11] ソフトウェア・テストの技法 第2版 | マイヤーズ,J., トーマス,M., バジェット,T., サンドラー,C., Myers,Glenford J., Thomas,Todd M., Badgett,Tom, Sandler,Corey, 真, 長尾, 正信, 松尾 |本 | 通販 | Amazon
[12] JWT(JSON Web Token)を使った認証を試みる | 69log
[13] JWT, JWS and JWE for Not So Dummies! (Part I) | by Prabath Siriwardena | FACILELOGIN
[14] JWEとJWSによる暗号化の実装 | Fintan
[15] JWT(+JWS)について調べた - takapiのブログ
[16] JWT の最新ベスト プラクティスに関するドラフトを読み解く (auth0.com)
[17] rfc7515
[18] OAuth 2.0 - なぜOAuthでは有効期限の長いアクセストークンを提供するのではなく、リフレッシュトークンを提供するのでしょうか?|teratail
[19] The Go Programming Language Specification - The Go Programming Language
[20] Go言語での構造体実装パターン · THINKING MEGANE

Discussion

ログインするとコメントできます