読者です 読者をやめる 読者になる 読者になる

goでテスト並列実行させたら楽な気がする

チームにある程度テストがかさばってくると、テストの実行時間が問題になる。せっかくテストを頑張って作っていても、実行時間が10分とか20分になってくるとどうしてもテストを動かすのが億劫になる。 せっかっく開発がノッてきたのに、テストのフィードバックがすぐ得られないと集中が途切れてしまう。手元なら実行範囲を狭めればいいだけだが、 CI 環境ではそうもいかないし、 そこでこけている原因を探るのに一回のトライエラーが10分間隔とかになると辛い。

まぁ、そういう建前は色々あるけど、テストのフィードバックが早く得られると困る人はいないと思う。

そういうわけでテストを並列実行しようという話が出てきて、php 界隈で並列実行を試みようとすると例えば以下のライブラリに行き当たる。

paratest は一番メジャーっぽいんだけど、php で書かれていて心配。php でマルチプロセスを行うのはかなりの黒魔術が必要なはずで、正直地雷踏みそうで近寄りたくない。 あとで見てみるとそんなことなさそう。昔読んだ php でマルチプロセスしてるコードが並列関係なく単純に出来が悪くて悪い印象持っていただけっぽい。

一方で parallel-phpunit は shell でなんか安心感がある。ただやっぱりそこは shell なのでテストの実行単位を分けていったりするのにもう少し自由度があったほうがいいのではという気がする。また少し離れたところでいうと、RRRSpec とかもある。これは確かにすごいのだが、こういう大艦巨砲が必要なわけではない。とにかく楽に学習コストとか下げてやりたい。

そんな感じの文脈で、ある程度費用対効果が良さそうな線を考えると、go で phpunit の並列実行するスクリプト書くのがいいんじゃないかなあという結論に至ってえいやで書いてみた。

package main

import (
    "fmt"
    "os/exec"
    "path/filepath"
    "runtime"
    "sync"
)

var (
    runner  = "./vendor/bin/phpunit"
    target  = "./tests/*"
    exclude = []string{"tests/fixture"}
    outputs = []byte{}
)

func main() {
    files, err := filepath.Glob(target)
    if err != nil {
        panic(err)
    }

    var wg sync.WaitGroup
    runtime.GOMAXPROCS(runtime.NumCPU())
    for _, file := range files {
        wg.Add(1)
        if contains(file, exclude) {
            wg.Done()
            continue
        }
        go func(file string) {
            out, err := exec.Command(runner, file).Output()
            if err != nil {
                wg.Done()
                fmt.Println("failed to execute: " + runner + " " + file)
                panic(err)
            }
            outputs = append(out)
            wg.Done()
        }(file)
    }
    wg.Wait()

    for _, output := range outputs {
        fmt.Printf("%c", output)
    }
}

func contains(value string, slice []string) bool {
    for _, s := range slice {
        if value == s {
            return true
        }
    }

    return false
}

このスクリプトでは、tests 以下のディレクトリごとに phpunit を並列実行している。今回はテストランナーを phpunit にしているけれど、var 以下を書き換えれば rspec でも karma でも testem でも何でも動かせる。

本当にとりあえずで書いたのでテストの実行単位が雑すぎるというのはあるけど、まぁそれに関しては拡張は難しくないのであまり気にしていない。 それよりも一番弱点になるのは CI 環境で go が必要になることだろうけど、別に致命的ではないと思う。go の書き方がなってない、とかそういう話は素直にすいませんといいたい。

とにかく楽に、費用対効果が高い形でテストを並列実行して高速化したいという話であれば go でスクリプト書くのはよい気がしてる。