Go言語の社内情報共有に関する試み、講演動画のすゝめとテストに関する翻訳を添えて

弊社が運営する、恋愛・婚活マッチングサービス「pairs」は、今年3月にPHPからのフルスクラッチによるGo言語への移行を完了しました。その後、サーバサイドの開発はGo言語で日々行われています。今回の記事では、まずはじめにエウレカ社内におけるGo言語の情報収集の試みを交えながら、@MasashiSalvadorが最近実践しているGo言語情報収集法をご紹介し、最後にGo言語でのテストに関する記事を翻訳したいと思います。

エウレカ社内におけるGo言語の情報収集の試み

社内でのエンジニアの情報収集及び情報共有の試みは、下記3種類に分かれます。

  1. 読書会
  2. slackチャンネルへの情報の垂れ流し
  3. Qiita:Teamへの翻訳記事の投稿

1. 読書会

弊社では社外の方も参加いただける読書会を開催しています。弊社のconnpassのページに記載があるように、

この辺りの書籍を輪読しています。

2. Slackチャンネルへの情報流入

エウレカのSlackには#eureka-golangというチャンネルがあります。ここでは、情報ソースとしてGo言語のGithub Trendredditから、そしてRight Relevanceというサービスを利用することで、Go言語のトレンドとなっている英語記事が随時流れてくるようになっています。

eureka-golang.png

「redditは流れすぎるからやめようか?」という話もあったのですが、流れていないと日常生活ではあまり接点がなくなってしまうため、残しています。

3. Qiita:Teamへの翻訳記事の投稿

社内Slackには前述のチャンネルに加えて、#talk-go-readingというチャンネルがあります。Go言語の情報は(もちろんGo言語に限らずですが)英語の方が進んでいることもあり、英語記事をキャッチアップしていくために有志で翻訳に取り組んでいます。
go_reading.png

翻訳する記事を#talk-go-readingチャンネルでつぶやき、翻訳した記事をQiita:Teamに投稿します。
情報ソースはGo NewsLetterMediumが多いようです。面白そうな記事は翻訳希望者が競合してしまうので、早い者勝ちに近いルールで翻訳者が決まります。

備考

先日開催された golang.tokyo #1 でも、Go言語を導入している各企業での情報共有の方法や教育の仕組みなどが話されたようですね。godgardenさんがまとめてくださったHello Gophers, Hello Golang.tokyo #1の記事にイベントの詳細が記載されています。トレンドや本質的な議論に乗り遅れないようにするために、どこから情報を収集するかについては各社方法論があると思うので、積極的に意見交換していきたいと@MasashiSalvador自身は思っています。

私的Go言語の英語の情報源まとめ

@MasashiSalvadorが、Go言語に入門してから今に至るまでに漁った英語の情報源や、普段追っている情報源を軽くまとめます。

文字情報

「### 2. Slackチャンネルへの情報流入」で紹介したGithub Trendsやredditも追っています。
朝イチでGo Forumを開いて、何か話題になってないかとチェックすることもあります。

文字情報を追いすぎると夢中になって作業が滞ることもあるので、用法用量を守って正しく使おうと心がけています。

音声や講演ビデオ

音声情報ならば、ある程度聞き流しながら作業できるため、英語の勉強にも良い!ということで、音声や講演ビデオに個人的に最近ハマっています。

  • dotGoの講演ビデオ
  • GopherConの講演ビデオ
  • podcast GO TIME

特にGo言語の設計者の一人であるRob Pike先生の講演ビデオを見ると、何を意図してGo言語が設計されたのかが高い熱量と共に伝わってきます。
特に、下記のビデオがおすすめです。

講演ビデオの紹介が生業の人みたいになってきましたのでこの辺で…。
YouTubeで作業用BGMを聞かれている方は講演ビデオを見る時はアカウントを分けるのをおすすめします。でないと履歴から適当に再生→アイドルソングの合間に突然の英語講演!!ということになりかねません。辛いです……。


最後に、気になった記事を1本翻訳して終わりたいと思います。

【翻訳】Goにおけるテストの構造化 – テストの構造化法 –  

元記事: Structuring Tests in Go – How I organize my tests in Go – written by @benbjohnson

TDDを使うべきかBDDを使うべきか、テスト本当に役に立つのか?など、テストのスタイルに関して人々は論じ合っている。Go言語において、私がテストを構造化する方法を話す前に、私のテストに関する考え方を説明するべきだろう。テストは下記2つを満たすべきである:

  1. 自己充足的である
  2. 簡単に複製可能である

テストスイートのどこか一部の変更が他の部分に過度な影響を与えないようにするために、テストは自己充足的であるべきである。テストは簡単に複製可能であるべきである。そうすれば、誰かが私のテストスイートと同じように自分のテストスイートを実行する際に多くのステップを踏む必要がなくなる。

以上の説明を前提として、Go言語における私のテストに関するルールは以下である。

1. フレームワークを使わない

Go言語の利用者は各々、自身のGo言語用のテスティングフレームワークを持っているようだ。setup/teardownを実施するものもあれば、BDD-styleのメソッドチェーンを持つものもあり、ウェブのインターフェースを持つものもある。
それらを使わないようにしよう。
Goはビルトインで全くもって良いテスティングフレームワークを持っている。フレームワークは他の開発者があなたのコードへコントリビュートする際の1つの障壁だ。
私はGoのテスティングフレームワークがテストのアサーションを冗長にしていると感じたので、シンプルなヘルパー関数を3つ追加したコードはここに

func assert(tb testing.TB, condition bool, msg string)
func ok(tb testing.TB, err error)
func equals(tb testing.TB, exp, act interface{})

これらの関数を用いることで、テスト中で頻出する型にはまった呼び出しやエラーチェック、返り値のチェックをより理解しやすい形にまとめることができる。つまり、

func TestSomething(t *testing.T) {
    value, err := DoSomething()
    if err != nil {
        t.Fatalf("DoSomething() failed: %s", err)
    }
    if value != 100 {
        t.Fatalf("expected 100, got: %d", value)
    }
}

を用いる代わりにシンプルに

func TestSomething(t *testing.T) {
    value, err := DoSomething()
    ok(t, err)
    equals(t, 100, value)
}

と書くことになる。

この方法を用いることで、私にとってはより一層テストの見通しが良くなった。これらの関数は私が作成したパッケージの中に存在するので、go getによって外部への依存をすることがない。

2. 「アンダースコアテスト」パッケージを用いる

Go言語では分離したテストパッケージを用いる場合以外は、1つのフォルダに複数のパッケージを含めることができない。例として、下記のように1つのフォルダに2つのファイルを含めることができる。

user.go
package myapp
type User struct {
    id int
    Name string
}
func (u *User) Save() error {
    if u.id == 0 {
        return u.create()
    }
    return u.update()

}
func (u *User) create() error { ... }
func (u *User) update() error { ... }
user_test.go
package myapp_test
import (
    "testing"
    . "github.com/benbjohnson/myapp"
)
func TestUser_Save(t *testing.T) {
    u := &User{Name: "Susy Queue"}
    ok(t, u.Save())
}

この例では、私のUser型はsaveを実行することができる。しかし、ユーザーのレコードをデータベースに作成するのか更新するのかは、パッケージの内側の事象なので、私のテストはどちらであるかに関心を向けるべきでない。
本体から分けられた「myapp_test」パッケージを用いることは、公開されていないフィールドや関数にアクセスできないことを意味する。
これによって、パッケージの使い手としてテストを実行することが可能になり、自分が公開したAPIが利用可能であるか、完成しているかを確かめることができる。

更新: Dave CheneyがTwitterで「ドット」インポートの使用に関する良い指摘をしてくれた。
上に書いたテストは公開されたAPIをテストしている。それ故、パッケージの内部でいようとすることには意味がない。指摘されたアプローチは良さそうに見えるので、将来的には利用してみようと思う。

3. テスト固有の型を使う

以前、sql.DBやsql.Txのようなジェネリックな型を自分のアプリケーション固有の関数を提供する固有の型でラップする方法について話したことがある。私のテストでは、それに似たようなことを行っているので、テストに固有の関数を使うことができる。

私のアプリケーションのほとんどはDB型を中心としているので、それを一例に挙げよう。
例えば、Boltデータベースをラップするアプリケーション固有の型があるとする。

type DB struct {
    *bolt.DB
}
func Open(path string, mode os.FileMode) (*DB, error) {
    db, err := bolt.Open(path, mode)
    if err != nil {
        return nil, err
    }
    return &DB{db}, nil
}

私が書くテストでは、一時ファイルを用いて自動的にデータベースをopenし、それからクリーンアップに利用可能なclose関数を提供するTestDB型を追加する。

type TestDB struct {
    *DB
}
// NewTestDB returns a TestDB using a temporary path.
func NewTestDB() *TestDB {
    // Retrieve a temporary path.
    f, err := ioutil.TempFile("", "")
    if err != nil {
        panic("temp file: %s", err)
    }
    path := f.Name()
    f.Close()
    os.Remove(path)
    // Open the database.
    db, err := Open(path, 0600)
    if err != nil {
        panic("open: %s", err)
    }
    // Return wrapped type.
    return &TestDB{db}
}
// Close and delete Bolt database.
func (db *TestDB) Close() {
    defer os.Remove(db.Path())
    db.DB.Close()
}

これを用いることで、私たちのテストにおいては、次のように簡単な2行の記述でsetupとteardownを行うことができる

func TestDB_DoSomething(t *testing.T) {
    db := NewTestDB()
    defer db.Close()
    ...
}

他の依存関係もテスト型に追加することができる。この方法を取ることで、全てを一緒にsetup、teardownできる。

4. インラインのインターフェイスとシンプルなモックを使う

私はインターフェイスのベストプラクティスに関して堂々巡りをしている。最初、私はインターフェースを頻繁に利用していたが、私のコードはインターフェイス利用前より複雑になった。
それから、私は全くもって使うのをやめた。しかし、外部への依存をモックするのがインターフェイス利用時より難しくなった。
インターフェイスを提供する呼び出される側の代わりに、呼び出す側がインターフェイスを作成するべきであることに気づいたのが私の最大のターニングポイントだった。
呼び出す側は何を必要としているかを正確に宣言することが出きるので、この方法は理にかなっている。

例を見てみよう。例えば、私たちみんなの大好きなソーシャルネットワークであるYoのクライアントを利用中だとしよう。我々のクライアントは次のようになっているとする。

package yo
type Client struct {}
// Send sends a "yo" to someone.
func (c *Client) Send(recipient string) error
// Yos retrieves a list of my yo's.
func (c *Client) Yos() ([]*Yo, error)

アプリケーションがYoの送信だけに関心がある場合ならば、インラインでインターフェイスを宣言することができる。

package myapp
type MyApplication struct {
    YoClient interface {
        Send(string) error
    }
}
func (a *MyApplication) Yo(recipient string) error {
    return a.YoClient.Send(recipient)
}

この方法により、私たちのmain.goでは、アプリケーションの初期化とクライアントの設定を次のように行える。

package main

func main() {
    c := yo.NewClient()
    a := myapp.MyApplication{}
    a.YoClient = c
    ...
}

また、我々のテストにおいて、モックの実装を下記のように利用することができる。

package myapp_test
// TestYoClient provides mockable implementation of yo.Client.
type TestYoClient struct {
    SendFunc func(string) error
}
func (c *TestYoClient) Send(recipient string) error {
    return c.SendFunc(recipient)
}
func TestMyApplication_SendYo(t *testing.T) {
    c := &TestYoClient{}
    a := &MyApplication{YoClient: c}
    // Mock our send function to capture the argument.
    var recipient string
    c.SendFunc = func(s string) error {
        recipient = s
        return nil
    }
    // Send the yo and verify the recipient.
    err := a.Yo("susy")
    ok(t, err)
    equals(t, "susy", recipient)
}

以上は、実例のために作り上げた単純にクライアントに受け手を渡すような例だ。しかし、この手のアプローチは、より複雑なロジックの流れや外部の依存モジュールから帰ってくるエラーをテストするの役に立つ。

結論

Goのツールチェインに組み込まれるようなテストを用いることで、より簡単にテストを利用し始めることができる。
私が経験した困難はありきたりな様式美のようだった。Go言語におけるテストにはシンプルで自然なアプローチが存在すると思っている。

テスト用の型を利用し、型の利点を活かすことで、テストのロジックの流れを明確にしよう。
本体から分離したテストパッケージを使い、パッケージ内部の物事ではなく、APIをテストしよう。
そして最後に、インラインのインターフェイスを用いることで、外部への依存からアプリケーションを切り離そう。

以上が、より見通しが良く、よりメンテナブルなテストをGo言語で書く際に助けになるいくつかの方法論だ。

  • このエントリーをはてなブックマークに追加

エウレカでは、一緒に働いていただける方を絶賛募集中です。募集中の職種はこちらからご確認ください!皆様のエントリーをお待ちしております!

Recommend

エウレカ社内のhubotで生き残っている機能3選

採用広報担当がエンジニアと信頼関係を構築するための5ステップ