チームにある程度テストがかさばってくると、テストの実行時間が問題になる。せっかくテストを頑張って作っていても、実行時間が10分とか20分になってくるとどうしてもテストを動かすのが億劫になる。 せっかっく開発がノッてきたのに、テストのフィードバックがすぐ得られないと集中が途切れてしまう。手元なら実行範囲を狭めればいいだけだが、 CI 環境ではそうもいかないし、 そこでこけている原因を探るのに一回のトライエラーが10分間隔とかになると辛い。
まぁ、そういう建前は色々あるけど、テストのフィードバックが早く得られると困る人はいないと思う。
そういうわけでテストを並列実行しようという話が出てきて、php 界隈で並列実行を試みようとすると例えば以下のライブラリに行き当たる。
paratest は一番メジャーっぽいんだけど、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 でスクリプト書くのはよい気がしてる。