Help us understand the problem. What is going on with this article?

Golangでテストしやすいコードをかく

More than 3 years have passed since last update.

この記事は株式会社ネクスト(Lifull) Advent Calendar 2016の2日目の記事です。

みなさんはじめまして、ネクストのchissoです。
はじめてQiitaに投稿するのでドキドキしています。

そんなことはさておき、この記事では、Golangで自動テストしやすいコードを書く方法について、私なりに気づいたことをまとめます。

想定読者

  • 普段よくRSpecを書いている
  • Golang初心者
  • Interfaceってなに?

目指すところ

「メソッド単位でテストコードを実行できる状態にする」
これだけです。

私は普段Rubyを書くことが多く、RSpecを使ってメソッド単位でユニットテストを作成しています。
ですが、Rubyと同じ感覚でコードを書いて、RSpecと同じノリでGolangのテストを書こうとしてハマりました。
(静的型付けの言語・Interfaceに疎く、そもそもコードがダメすぎたことが原因でした。)

GolangではRSpecのようにmock(stub)が作れない

言語仕様として当たり前なのですが、私がなにもわかっていなかった時代に書いていたGolangのコードを、Rubyのコードと比較しつつ進めていきます。

sample1.go
package main

import(
  "fmt"
)

type Hoge struct {
  hoge string
}

func main() {
  h := Hoge{"hogehoge"}
  echo(h)
}

func echo(h Hoge) string {
  str := h.getHoge()
  fmt.Println(str)
  return str
}

func (h Hoge) getHoge() string {
  return h.hoge
}
sample1.rb
class Main
  def initialize(hoge)
    @hoge_instance = hoge
  end

  def echo
    p @hoge_instance.get_hoge
  end
end

class Hoge
  def initialize(str)
    @hoge = str
  end

  def get_hoge
    @hoge 
  end
end

package/classの設計とか、Rubyでget_hogeってなんだよ、みたいなツッコミは一旦置いておいて、、

この両者において、echoのテストを書くことを考えてみます。
先にRSpecを使う前提でRubyから。

sample1_spec.rb
require './sample.rb'

describe Main do
  # とりあえずechoのテストだけ
  describe '#echo' do
    let(:hoge) { double('hoge') }
    let(:str) { 'hogehoge' }
    let(:target) { described_class.new(hoge) }

    subject { target.echo }

    before do
      # Hogeのメソッドはモックする
      allow(hoge).to receive(:get_hoge).and_return(str)
    end

    it 'get_hogeの値をpすること' do
      expect(target).to receive(:p).with(str)
      subject
    end
  end
end

ではGolangはというと・・・さきほどのsample1.goをベースにすると、echoのユニットテストは書けません

Hogeというstruct、そしてgetHogeというメソッドを上書きすることができないため、mockすることができないのです。

今回は簡単のため同一ファイル(package)内でstructを作成していますが、私は当初packageをまたいでこのようなコードを書いてしまっていました。
ユニットテストが書きたいのに、他のpackageの実装に依存したテストコードになってしまうのです。

日々悩み、ネットの海を徘徊し、辿り着いた答えが下記となります。

解決策: interfaceを使う

Golangを使い始めた最初は、なんでも突っ込める魔法のビンのように思っていたinterface{}。
interfaceの意味を理解して使いこなすことで、ユニットテスト可能なpackage(≒ class)を作れるようになれます。

Golangのinterfaceについては、下記の記事が詳しいです。

interfaceを使ってsample1.goを書き直してみます。

sample2.go
package main

import(
  "fmt"
)

// Hoge structをラップするかたちでinterfaceを宣言
type HogeInterface interface {
  getHoge() string
}

type Hoge struct {
  hoge string
}

func main () {
  h := Hoge{"hogehoge"}
  echo(h)
}

// 引数をstructではなくinterfaceにする
func echo(h HogeInterface) string {
  str := h.getHoge()
  fmt.Println(str)
  return str
}

func (h Hoge) getHoge() string {
  return h.hoge
}

たったこれだけの変化で、echoがユニットテスト可能になります。

sample2_test.go
package main

import (
  "testing"
)

// Hogeのモックを宣言
// specの let(:hoge) { double('hogehoge') }に相当
type HogeMock struct {}

// HogeMockにgetHogeを実装
// specの allow(hoge).to receive(:get_hoge).and_return("hogehoge")に相当
func (h HogeMock) getHoge() string {
  return "hogehoge"
}

func TestEcho(t *testing.T) {
  mock := HogeMock{}
  hoge := echo(mock)

  if hoge != mock.getHoge() {
    t.Error("error!")
  }  
}

このようにstructをinterfaceでラップすることで、他のメソッドやpackageとの依存関係はその”振る舞い”(= interface)だけに留めることができ、各メソッドでユニットテストを作成することができるようになります。

これをやった上で、凝ったテストをやりたい場合にはテスト用のライブラリを使うとはかどります。
私はやはりRSpec風に書きたかったので、ginkgoというライブラリを使いました。

ただし、これだけでは対応できないパターンもあったので、テストしやすいコードを書く方法をもう一つご紹介しておきます。

関数を変数化する

私は下記の記事でこの方法を学びました。
Golangで関数をグローバル変数に代入してテスト時にスタブする

実際のコードに近い形で例を示します。

API接続packageとmodel packageを切り離す

APIからデータを取得して整形する、modelに相当するようなpackageがあるとして、このmodelをテストする時は実際にAPIを叩きたくありません。
そこで、APIのclientを担うpackageを作成し、そのclientを使ってAPIを呼び出すメソッドを変数化しておきます。

client.go
package client

import(
  "encoding/json"
  "io/ioutil"
  "net/http"
)

type clientInterface interface {
  buildUri(string, string) string
  buildQueryString(map[string]string) string
  getHttpClient() http.Client
}

type client struct {
  host    string
  port    int
  timeout int
}

var c clientInterface

func init() {
  c = &client{
    host:    "www.example.com",
    port:    80,
    timeout: 5,
  }
}

// APIからjsonでデータを受け取り、任意のstructにparseする
var Get = func (path string, params map[string]string, target interface{}) error {
  p := c.buildQueryString(params)
  uri := c.buildUri(path, p)
  httpClient := c.getHttpClient()

  response, err := httpClient.Get(uri)
  if err != nil {
    return err
  }
  defer response.Body.Close()

  body, err := ioutil.ReadAll(reponse.Body)
  if err != nil {
    return err
  }

  err = json.Unmarshall(body, &target)
  if err != nil {
    return err
  }

  return nil
}

// 以下、clientInterfaceはいい感じに実装します。

API接続packageをこのようにしておくことで、取得パスに応じたmodelの作成を行うことができます。(さらにPost/Delete/Putをapi.go内で作成する場合も同様)
そして、Getを下記のようにモックしてやれば、それぞれのmodelでテストが行えます。

model_test.go
// modelのpackageで、上記client.goをclientとしてimportする想定。
// 変数として宣言したGetメソッドを上書きする
client.Get = func(path string, params map[string]string, target interface{}) error {
  j := `{"id": 1, "name": "chisso"}`
  json.Unmarshall(j, &target)
  return nil
}

// error時をテストしたいときも自在
client.Get = func(path string, params map[string]string, target interface{}) error {
  return errors.New("Not Found")
}

エラー時の再現が自在になる恩恵は大きいと思います。
clientで起こった各種errorをmodelできちんとハンドリングする場合に、モックしたGetメソッドで各種エラーを引き起こせるので、とてもテストしやすいです。

なお、client自身もinterface化しているので、Get自身もきちんとユニットテスト可能です。

また、複数のAPIを利用するような場合であれば、コンストラクタを変数として提供することでそれぞれうまくモックすることができます。

まとめ

いかがでしたでしょうか。
interfaceをきちんと理解している人にとってはとても当たり前の話だったかもしれません。

ただ、もし自分のようにphpやRubyからプログラミングに入り、テストコード書いてたけどinterfaceの概念がうまく理解できてなかった人の助けになれば幸いです。

株式会社ネクスト(Lifull) Advent Calendar 20163日目の記事は私の部署の先輩でもあるzomさんです。
ご期待下さい!

chisso
lifull
日本最大級の不動産・住宅情報サイト「LIFULL HOME'S」を始め、人々の生活に寄り添う様々な情報サービス事業を展開しています。
https://lifull.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away