こんにちは、この記事はgo advent calendar 2018 9日目の記事です
Go言語初心者といっても過言ではなく他のGoアドベントカレンダーの方々の記事を見てると。とても恐れ多いですが 初心者なりにアウトプットしたいと思っています!!
ちなみに、↓のネタもあったんですが、全く受けなかったのでやめておきましたw
Goアドベントカレンダーのネタとして「人間の業(ごう)がわかる業(ごう)APIをGo言語で作った」というのを思いついたのだが、いかがだろうか
— Takayama Kazuyuki (@pco2699) 2018年12月4日
モチベーション
- Go言語でちょっとしたCLIツール(tailコマンドコピーみたいなやつ)は作ったことあるけど、Web系のAPI作ったこと無い
- せっかくだから、なんか欲しいものつくりたい
- 最近、手ぶらで出かけるのにハマってるんだけど、雨によく打たれて困るから、朝に傘もっていくか知りたい -> それ作ろう
というわけで今回のネタに決まりましたー!!!
APIの方針
- GoのAPIフレームワーク(Goaとか)はいろいろあるらしいけど、超簡単なAPIなのでとりあえず標準パッケージのみでつくる
- API自体の設計は超シンプルにする
- せっかくなので、モックかましてAPIのテストも書いてみる
せっかくなので、GraphQLとかgRPC とかでやってみる説もあったんですが
今後LINE BOTとかに組み込みやすくしたいので
ノウハウが溜まっているJSON/REST APIにしておきます。(ほんとは時間なかっただけ
APIをつくっていくよ編
API自体は非常にシンプルでして、ほぼほぼOpenWeatherMap APIのラップに近いです。 /v1を投げると以下のようなシンプルなjsonが返ってきます。
{ "is_umbrella_required": false, // 傘いるかフラグ "rain_volume": 0 // 向こう24時間での予測降水量 }
OpenWeatherMap APIにリクエストを投げる
天気を取得するAPIとしてOpenWeatherMapAPIを使います。
このAPIは天気関連のプロダクトを作る際には非常によく使われているAPIなので
基本的な登録の仕方やAPIの内容詳細については、説明を割愛します。
今回はこちらの「5 day / 3 hour forecast」を使います。
構造体でjsonのマッピングを設定
まずはAPIの仕様書に基づいて Goで構造体を定義します。
type WeatherData struct { Cod string `json:"cod"` Message float64 `json:"message"` Count int `json:"count"` List []WeatherList `json:"list"` } // 以下、WeatherListの内容など...(長いので省略)
これって全部マッピングする必要無いよな...!?とか思ったんですが
よくわからなかったんで、とりあえず全部jsonを構造体にマッピングしました。
たぶん、必要なデータだけjson:"hogehoge"
的な感じでやればマッピングされると思います。
ApiClient interfaceの定義
今回はMockテストをしたいので、interfaceでAPIコールを行う機能を定義します。
以下の通りです。
package WeatherApiClient import "umbrellaApi/WeatherData" type ApiClient interface { Fetch() WeatherData.WeatherData }
ちなみに、自分は普段JavaでSpringを書いているのですが
このコードを書いたことでデフォでinterfaceに依存させているのか
よく理解できました。
ApiClientの実装を行う
上記のinterfaceの実装を行っていきます。 まず構造体で接続情報を定義していきます。
type WeatherApiClient struct { RequestUrl string //リクエスト先のURL Place string //OpenWeatherMapのリクエストパラメータ① Format string //リクエストパラメータ② Count string //③ Lang string //④ }
コンストラクタを定義します。 ここで具体的な接続情報を入れていきます。
// WeatherApiClient.go func NewWeatherApiClient() ApiClient { return &WeatherApiClient{ "https://api.openweathermap.org/data/2.5/forecast", "Tokyo,jp", "json", "8", // 3時間毎×8 -> 24時間後までの予測を取得する "ja", } }
Fetchの処理の中身を書いていきます。 ここで、OpenWeatherMapAPIをコールしてjsonをFetchした上で、WeatherDataの構造体につめて返してます。
// WeatherApiClient.go func (w *WeatherApiClient) Fetch() WeatherData.WeatherData { values := url.Values{} apiKey := os.Getenv("API_KEY") values.Add("APPID", apiKey) values.Add("q", w.Place) values.Add("mode", w.Format) values.Add("cnt", w.Count) values.Add("lang", w.Lang) res, err := http.Get(w.RequestUrl + "?" + values.Encode()) if err != nil { log.Fatal("Failed to get request url") } defer res.Body.Close() body, err := ioutil.ReadAll(res.Body) var wd WeatherData.WeatherData err = json.Unmarshal(body, &wd) if err != nil { log.Fatal("Failed to fetched unmarshal json") } return wd }
APIサーバーを立てる
OpenWeatherMap APIにコールする部分は完成したので
今度は、APIサーバー部を書きます。
Serverの定義
テストしやすいようにAPIサーバーの処理は
構造体で定義しておき、その中にinterfaceであるAPIClientを入れておきます。
APIサーバーのルーティングはシンプルなのでhttp.handleFuncで定義します。
handleApiでは、OpenWeatherMap APIで取得した情報をもとに
今後、24時間の予測降水量を積算して、少しでも今後雨が降るようだったら
傘を持っていくフラグをONにしています。
それをmapに詰めてjson形式にして返しています。
// Server.go func (s *Server) Start() { http.HandleFunc("/v1", s.handleApi) log.Fatal(http.ListenAndServe(":3000", nil)) } func (s *Server) handleApi(w http.ResponseWriter, r *http.Request) { wd := s.ApiClient.Fetch() rainVolume := 0.0 for _, values := range wd.List { rainVolume += values.Rain.Volume } isUmbrellaRequired := false if rainVolume > 0 { isUmbrellaRequired = true } if err := json.NewEncoder(w).Encode(map[string]interface{}{"isUmbrellaRequired": isUmbrellaRequired, "rainVolume": rainVolume}); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } }
mainを書く
あとは、mainで書いたServerを呼び出せば OKです。
.envファイルを読み込めるようにjoho/godotenv/autoload
を読み込んでます。
これでルートディレクトリにある.envを勝手に読み込んでくれます。(便利)
// main.go import ( "umbrellaApi/Server" "umbrellaApi/WeatherApiClient" _ "github.com/joho/godotenv/autoload" ) func main() { wac := WeatherApiClient.NewWeatherApiClient() server := &Server.Server{ wac, } server.Start() }
Mockかましてテストするよ編
さて、すごい簡単なAPIにもかかわらずinterfaceなどが登場して、いろいろめんどくさい感じになってますが あれもこれも、テストをしやすくするための工夫です。
それでは、以下の重要な部分について、それぞれテストを書いていきましょう。
今回のコードですと、それぞれ依存することなくテストを書くことができます。
OpenWeatherMap APIにリクエスト投げる部分
WeatherMapApiClientに対して、テストを書いていきます。
今回は、利用する部分のチェックしか行ってません。
// WeatherMapApiClient_test.go package WeatherApiClient import ( "testing" ) func TestWeatherApiClient_Fetch(t *testing.T) { wac := NewWeatherApiClient("{API_KEYの情報を入れる}") wd := wac.Fetch() if wd.Cod.String() != "200" { t.Errorf("got: %v\nwant: %v", wd.Cod, "200") } for _, values := range wd.List { if &values.Rain.Volume == nil { t.Errorf("got: %v\nwant: %v", values.Rain.Volume, "Number") } } }
本APIの傘を持っていく判定を行う部分
まずは、mockのApiClientを差し込むことで、毎回OpenWeatherMap APIにアクセスせずとも
テストを行えるようにします。
type testApiClient struct {} func (t testApiClient) Fetch() WeatherData.WeatherData { wd := WeatherData.WeatherData{ // テストデータを詰める } return wd }
こいつをServerに押し込んでやることで、Serverでテストが行えるようになります。
Serverのテストはhttptest.NewServer
で行います。
func TestServer(t *testing.T) { server := &Server{ testApiClient{}, } ts := httptest.NewServer(http.HandlerFunc(server.handleApi)) defer ts.Close() res, err := http.Get(ts.URL) if err != nil { t.Fatalf("%v", err) } data, err := ioutil.ReadAll(res.Body) if err != nil { t.Fatalf("%v", err) } var apiData ApiData err = json.Unmarshal(data, &apiData) if err != nil { t.Fatalf("%v", err) } if apiData.IsUmbrellaRequired != true { t.Errorf("got: %v\nwant: %v", apiData.IsUmbrellaRequired, true) } if apiData.RainVolume != 0 { t.Errorf("got: %v\nwant: %v", apiData.RainVolume, true) } }
今回のコード
今回はコードは以下からご覧いただけます。
指摘やコメントなど大歓迎です m( )m