LoginSignup
12
10

More than 5 years have passed since last update.

独自MockによるGo言語のテスト自動化

Posted at

Mock ライブラリを使わない独自のMock でテスト自動化をする

Go 言語ではMock ライブラリとして幾つか有名なものがありますが、約1.5ヶ月それらを使ってGo 言語でMock を使ったテストをトライアンドエラーで勉強していましたが現段階で独自にMock を使ってテストする方法に落ち着きました。
ここではそれらの勉強でMock ライブラリを使わずに独自のMock を作成してテストを自動化する方法について記録として残しておこうと思います。
説明は完成前のプログラムと完成後のプログラムをまず提示し、個々に説明していく形式で実施していきます。

サンプルプログラム

テスト自動化のサンプルとして"http://httpbin.org/ip" にGET リクエストを送信し、レスポンスJSON を解析して自分のグローバルIP アドレスを取得するプログラムを作成しました。

globalip.go
package main

import (
    "net/http"
    "io/ioutil"
    "encoding/json"
    "errors"
    "fmt"
)
const url = "http://httpbin.org/ip"

func GetGlobalIP() (string, error) {
    response, err := http.Get(url)
    if err != nil {
        fmt.Println("ERROR: Requesting " + url + " has failed")
        return "", err
    }
    defer response.Body.Close()

    byteJSON, _ := ioutil.ReadAll(response.Body)

    if response.Status != "200 OK" {
        fmt.Println("ERROR: Status of response from " + url + " was " + response.Status)
        return "", errors.New("ERROR: Got response status " + response.Status + " from " + url)
    }
    ipv4, _ := fetchIPFromJSON(byteJSON)

    return ipv4, nil
}

func fetchIPFromJSON(jsonByte []byte) (string, error) {
    var unmarshaled interface{}
    err := json.Unmarshal(jsonByte, &unmarshaled)
    if err != nil {
        return "", err
    }
    jsonMap := unmarshaled.(map[string]interface{})

    return jsonMap["origin"].(string), nil
}

func main() {
    ipv4, _ := GetGlobalIP()
    fmt.Println("Global IP is " + ipv4)
}

/* 以下略... */
実行
$ go run globalip.go
Global IP is 9.9.9.9

上記のプログラムをテストする場合http.Get, ioutil.ReadAll をMock 化してテスト自動化を行いたいところですがGo 言語の仕様上、このままのコードでは難しそうです。
そのため上記のプログラムをテスタブルに書き換えてMock を使ったテスト自動化をできるようにしましょう。

テスタブルに書き換える

テスタブルに書き換えた結果、プログラム本体は次のようになります。

globalip.go
package main

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

const url = "http://httpbin.org/ip"

type GlobalIPUtil struct {
    httpGet func(string) (*http.Response, error)
    ioutilReadAll func(io.Reader) ([]uint8, error)
}

func NewGlobalIPUtil() (*GlobalIPUtil) {
    globalIPUtil := &GlobalIPUtil {
        httpGet:        http.Get,
        ioutilReadAll:  ioutil.ReadAll,
    }
    return globalIPUtil
}

func (self *GlobalIPUtil) GetGlobalIP() (string, error) {
    response, err := self.httpGet(url)

    if err != nil {
        fmt.Println("ERROR: Requesting " + url + " has failed")
        return "", err
    }
    defer response.Body.Close()

    byteJSON, _ := self.ioutilReadAll(response.Body)
    if response.Status != "200 OK" {
        fmt.Println("ERROR: Status of response from " + url + " was " + response.Status)
        return "", errors.New("ERROR: Got response status " + response.Status + " from " + url)
    }
    gip, _ := self.fetchIPFromJSON(byteJSON)

    return gip, nil
}

func (self *GlobalIPUtil) fetchIPFromJSON(jsonByte []byte) (string, error) {
    var unmarshaled interface{}
    err := json.Unmarshal(jsonByte, &unmarshaled)
    if err != nil {
        return "", err
    }
    jsonMap := unmarshaled.(map[string]interface{})

    return jsonMap["origin"].(string), nil
}

func main() {
    iputil := NewGlobalIPUtil()
    ipv4, _ := iputil.GetGlobalIP()
    fmt.Println("Global IP is " + ipv4)
}

/* 以下略... */

それに対し、テストコードとしては以下のようになります。

package main

import (
    "testing"
    "bytes"
    "net/http"
    "io/ioutil"
    "github.com/stretchr/testify/assert"
)

func TestGetGlobalIP(t *testing.T) {
    assert := assert.New(t)
    // Create mock
    iputil := &GlobalIPUtil{
        httpGet: func(url string) (*http.Response, error) {
            result := &http.Response{
                Status: "200 OK",
                Body: ioutil.NopCloser(bytes.NewBufferString("{\"origin\":\"1.1.1.1\"}")),
            }
            return result, nil
        },
        ioutilReadAll = ioutil.ReadAll,
        // 以下のようにMock を定義することも可能
        /*
        ioutilReadAll = func(io.Reader) ([]uint8, error) {
            return []byte("{\"origin\":\"1.1.1.1\"}"), nil
        }
        */
    }

    result, err := iputil.GetGlobalIP()
    assert.Nil(err)
    assert.Equal("1.1.1.1", result)
}
// .......

ここでテスタブルにするためのポイントを個々に解説していきたいと思います。
ポイントとしては以下のようになります。

  • 外部からもアクセスできるstruct (struct 名の頭文字が大文字なやつ)を作成し、その中にMock 化したい関数へのポインタを定義する
  • struct のインスタンスを返すNew 関数(NewGlobalIPUtil)を作成する
  • 本番時はNewGlobalIPUtil 関数を使って、テスト時は自前でstruct をインスタンス化する

外部からもアクセスできるstruct を作成し、その中にMock 化したい関数へのポインタを定義する

http.Get 関数、ioutil.ReadAll 関数に対するポインタを格納するstruct を定義します。

GlobalIPUtil
......
type GlobalIPUtil struct {
    httpGet func(string) (*http.Response, error)        /* 関数ポインタ */
    ioutilReadAll func(io.Reader) ([]uint8, error)      /* 関数ポインタ */
}
......

Go 言語は静的型付けなので関数のポインタを作成するときは正確に引数の型や返り値の型を宣言して上げる必要があります。
それらを調べるにはドキュメントを調べるでもいいですが、ドキュメントが少なかったりソースコードが見当たらない場合は以下のコードを書いて実行してみるでもOK です。

http.Get関数の詳細な型を調べる
package main
import (
    "fmt"
    "net/http"
)
func main() {
    fmt.Printf("%T\n", http.Get)
}
----
$ go run test.go
func(string) (*http.Response, error)

struct のインスタンスを返すNew 関数を作成する

struct のインスタンスを返すNew 関数を作成しhttp.Get, ioutil.ReadAll へのポインタを持ったstruct を返す関数を作成します。

NewGlobalIPUtil関数の定義
func NewGlobalIPUtil() (*GlobalIPUtil) {
    globalIPUtil := &GlobalIPUtil {
        httpGet:        http.Get,           /* デフォルトでhttp.Get を入れる */
        ioutilReadAll:  ioutil.ReadAll,     /* デフォルトでioutil.ReadAll を入れる */
    }
    return globalIPUtil
}

本番時はNewGlobalIPUtil 関数を使って、テスト時は自前でstruct をインスタンス化する

ソースコードをテスタブルに書き換えたら、本番ではNewGlobalIPUtil 関数を使って、テストでは自前でstruct をインスタンス化してプログラムを実行します。

本番用のソースコードの例
    ......
    iputil := NewGlobalIPUtil()
    ipv4, _ := iputil.GetGlobalIP()
    ......
テスト用のソースコードの例
    ......
    iputil := &GlobalIPUtil{
        httpGet: func(url string) (*http.Response, error) {
            /* Mock プログラムをここに書く */
        },
        ioutilReadAll: func(io.Reader) ([]uint8, error) {
            /* Mock プログラムをここに書く */
        },
    }
    ipv4, _ := iputil.GetGlobalIP()
    ......

具体例は本記事の最初に示したgithub のURL にあるので参考にしてみてください。

最後に

上記のように作成することで本番用のソースコードにテストのためだけのボイラープレートを極力書かなくて済み、可読性の高さを維持したままMock も利用できるテスタブルなソースコードを利用することができるようになります。
Go 言語でテストの自動化をする時にMock を利用せずに実際にテストサーバを構築してテスト環境の準備にコストがかかっていたり本番用のソースコードにMock を入れた為にしっくり来なかったり…といった悩みから解消されましょう!

参考

12
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
10