View on GitHub

Codeception.docs.ja

Codeception docs japanese translation

Download this project as a .zip file Download this project as a tar.gz file

並列実行

テストの実行時間がコーヒーブレイクよりも長くなったとしたら、それはテストの実行速度向上を考える良い機会です。もしすでにテストをSSD上で実行している、またはSeleniumの替わりにPhantomJSの使用を試しているのに、まだ実行速度にイライラしているようでしたら、テストを並列実行してみるのが良いかもしれません。

どこからはじめよう?

Codeceptionはrun-parallelのようなコマンドを提供していません。全員にとって満足に動作する共通の解決策はありません。あなたは次の疑問を解決する必要があります。

  • どのようにして並列プロセスは実行されるのか?
  • どのようにして並列プロセスがお互いに影響を与えないようにするか?
  • プロセスごとに異なるデータベースを使用するか?
  • プロセスごとに異なるホストを使用するか?
  • どのように並列プロセス間でテストを分割すべきか?

並列化を実現するためのアプローチは2つあります。Dockerを使って、それぞれのプロセスを独立したコンテナ内で実行するか、それらコンテナを同時に実行するか、することができます。

Dockerは独立したテスト環境のため、本当に良く機能します。 この章を記述している時点では、Dockerのようなすばらしいツールはありませんでした。この章では、手動による並列実行の管理方法について説明します。見てわかるように、Dockerが簡単にやってのけるのに対して、この章ではテストを分離するためにとても多くの労力を割いています。現在では、テストの並列実行には**Dockerを使うことをおすすめします**。

Docker

:construction: このセクションは準備中です(本家が完成したら和訳します)

Requirements

Using Codeception Docker image

Run Docker image

docker run codeception/codeception

Running tests from a project, by mounting the current path as a host-volume into the container. The default working directory in the container is /project.

docker run -v ${PWD}:/project codeception/codeception run

For local testing of the Codeception repository with Docker and docker-copmose, please refer to the testing documentation.

Robo

なにをすればいい?

並列テスト実行は以下の3ステップから構成されます。

  • テストを分割する
  • テストを並列に実行する
  • テスト結果をマージする

これらのステップの実施にタスクランナーを利用することを提案します。このガイドではRoboというタスクランナーを使用します。これは非常に簡単に利用できるモダンなPHPのタスクランナーです。バックグラウンドおよび並列プロセスの起動にSymfony Processを使っています。ステップ2に必要なのはこれだけです!ではステップ1とステップ2はどうでしょう?私たちはテストをグループに分割するのと、テスト結果をJUnit XMLレポートにマージするためにrobo tasksを作成しました。

まとめると、私たちに必要なものは以下となります。

  • Robo - タスクランナー
  • robo-paracept - 並列実行のためのCodeceptionタスク

Roboを準備する

Roboはグローバルインストールすることを推奨します。Composerを使用してグローバルインストールするか、robo.pharをダウンロードしてPATHを通すか、どちらでも構いません。

プロジェクトのルートでroboを実行します。

$ robo
  RoboFile.php not found in this dir
  Should I create RoboFile here? (y/n)

RoboFile.phpが作成されることを確認します。

<?php
class RoboFile extends \Robo\Tasks
{
    // define public methods as commands
}

Composer経由でcodeception/robo-paraceptをインストールし、RoboFileにインクルードします。

robofileの各publicメソッドはコンソールコマンドとして実行することができます。先ほどの3つのステップのコマンドを定義しましょう。

<?php
require_once 'vendor/autoload.php';

class Robofile extends \Robo\Tasks
{
    use \Codeception\Task\MergeReports;
    use \Codeception\Task\SplitTestsByGroups;

    public function parallelSplitTests()
    {

    }

    public function parallelRun()
    {

    }

    public function parallelMergeResults()
    {

    }
}

roboを実行すると、それぞれのコマンドが表示されます。

$ robo
Robo version 0.4.4
---
Available commands:
  help                     Displays help
  list                     Lists commands
parallel
  parallel:merge-results   
  parallel:run             
  parallel:split-tests     

サンプルプロジェクト

とても時間のかかる受け入れテストを5プロセスに分割して実行することを考えてみましょう。それぞれのテストが衝突しないように異なるホストとデータベースを使用すべきです。そのため、先に進む前に5つの異なるホストにApache/Nginxを設定する必要があります(もしくは、単に異なるポートを使用してPHPのビルトインサーバーでアプリケーションを実行します)。ホスト情報に基づいて対応するデータベースを使用するようにしてください。

代替としてDockerLXCを使用して分離された環境を準備し、それぞれのテストプロセスをコンテナー上で実行することもできます。新しいコンテナを起動してより多くのプロセスを実行することは、手動で追加のデータベースとホストを作成するよりもはるかに簡単です。これはより安定したテスト環境を作成しているということです(データベース、ファイル、プロセスの衝突はありません)。ただ、新しく仮想マシンを作成する度にコンテナをプロビジョニングする必要があるでしょう。

SSHを使用してリモートホスト上でテストを実行するということも考えられます。RoboはSSHコマンドを実行するためのビルトインタスクも備えています。

このサンプルプロジェクトでは、アプリケーションのために5つのデータべースと5つの独立したホストを準備していることを想定しています。

ステップ1 テストを分割する

Codeceptionはテストをグループに整理することができます。バージョン2.0からはグループの情報をファイルから読み込むことができます。テキストファイルにファイルの一覧を記述すると、動的にグループとして設定されます。サンプルのグループファイルを見てみましょう。

tests/functional/LoginCept.php
tests/functional/AdminCest.php:createUser
tests/functional/AdminCest.php:deleteUser

\Codeception\Task\SplitTestsByGroupsタスクは交差しない(non-intersecting)グループファイルを生成します。テストはファイル単位、テスト単位のどちらでも分割することができます。

<?php
    function parallelSplitTests()
    {
        $this->taskSplitTestFilesByGroups(5)
            ->projectRoot('.')
            ->testsFrom('tests/functional')
            ->groupsTo('tests/_log/p')
            ->run();

        // alternatively
        $this->taskSplitTestsByGroups(5)
            ->projectRoot('.')
            ->testsFrom('tests/functional')
            ->groupsTo('tests/_log/p')
            ->run();
    }

後者の場合、Codeception\TestLoaderが使用され、テストクラスはメモリ上に読み込まれます。

グループファイルを準備しましょう。

$ robo parallel:split-tests

 [Codeception\Task\SplitTestFilesByGroupsTask] Processing 33 files
 [Codeception\Task\SplitTestFilesByGroupsTask] Writing tests/_log/p1
 [Codeception\Task\SplitTestFilesByGroupsTask] Writing tests/_log/p2
 [Codeception\Task\SplitTestFilesByGroupsTask] Writing tests/_log/p3
 [Codeception\Task\SplitTestFilesByGroupsTask] Writing tests/_log/p4
 [Codeception\Task\SplitTestFilesByGroupsTask] Writing tests/_log/p5

これでグループファイルができました。生成されたグループファイルを読み込むよう、codeception.ymlを更新してください。今回の場合、p1p2p3p4p5 のグループがあります。

groups:
    p*: tests/_log/p*

2つ目のグループからテストを実行してみましょう。

$ php codecept run functional -g p2

ステップ2 テストを実行する

すでに述べたように、Roboにはバックグラウンドプロセスを起動するためのParallelExecを備えています。しかし、これが唯一のオプションとは考えないでください。たとえば、SSHを介してリモートでテストを実行することもできますし、GearmanやRabbitMQなどを利用してプロセスを起動することもできます。ただ、この例では5つのバックグラウンドプロセスを利用します。

<?php
    function parallelRun()
    {
        $parallel = $this->taskParallelExec();
        for ($i = 1; $i <= 5; $i++) {            
            $parallel->process(
                $this->taskCodecept() // use built-in Codecept task
                ->suite('acceptance') // run acceptance tests
                ->group("p$i")        // for all p* groups
                ->xml("tests/_log/result_$i.xml") // save XML results
            );
        }
        return $parallel->run();
    }

私たちは何か重要なことを見落としています。異なるプロセスに異なるデータベースを定義することを忘れていますね。これは環境を利用して行うことができます。acceptance.suite.ymlに新しく5つの環境を定義しましょう。

class_name: AcceptanceTester
modules:
    enabled: [WebDriver, Db]
    config:
        Db:
            dsn: 'mysql:dbname=testdb;host=127.0.0.1'
            user: 'root'
            dump: 'tests/_data/dump.sql'
            populate: true
            cleanup: true
        WebDriver:
            url: 'http://localhost/'
env:
    p1:
        modules:
            config:
                Db:
                    dsn: 'mysql:dbname=testdb_1;host=127.0.0.1'
                WebDriver:
                    url: 'http://test1.localhost/'
    p2:
        modules:
            config:
                Db:
                    dsn: 'mysql:dbname=testdb_2;host=127.0.0.1'
                WebDriver:
                    url: 'http://test2.localhost/'
    p3:
        modules:
            config:
                Db:
                    dsn: 'mysql:dbname=testdb_3;host=127.0.0.1'
                WebDriver:
                    url: 'http://test3.localhost/'
    p4:
        modules:
            config:
                Db:
                    dsn: 'mysql:dbname=testdb_4;host=127.0.0.1'
                WebDriver:
                    url: 'http://test4.localhost/'
    p5:
        modules:
            config:
                Db:
                    dsn: 'mysql:dbname=testdb_5;host=127.0.0.1'
                WebDriver:
                    url: 'http://test5.localhost/'

そうしたら、対応する環境を利用するようにparallelRunメソッドを更新してください。

<?php
    function parallelRun()
    {
        $parallel = $this->taskParallelExec();
        for ($i = 1; $i <= 5; $i++) {            
            $parallel->process(
                $this->taskCodecept() // use built-in Codecept task
                ->suite('acceptance') // run acceptance tests
                ->group("p$i")        // for all p* groups
                ->env("p$i")          // in its own environment
                ->xml("tests/_log/result_$i.xml") // save XML results
              );
        }
        return $parallel->run();
    }

これで次のようにテストを実行することができます。

$ robo parallel:run

ステップ3 テスト結果をマージする

テスト実行中はコンソールに出力される内容を信用すべきではありません。parallelExecタスクの場合、いくつかの文字列は失われるでしょう。テスト結果は、マージできて継続的インテグレーションに挿入できるJUnit XML形式で保存することをおすすめします。

<?php
    function parallelMergeResults()
    {
        $merge = $this->taskMergeXmlReports();
        for ($i=1; $i<=5; $i++) {
            $merge->from("/tests/_log/result_$i.xml");
        }
        $merge->into("/tests/_log/result.xml")
            ->run();
    }

result.xmlファイルが生成されます。これを処理して、分析することができます。

すべてを統合する

これまでのステップを統括して1つのコマンドとするために、新しくpublicなparallelAllメソッドを定義し、すべてのコマンドを実行するようにします。parallelRunの結果を保存して、最終的な終了コードとして使います。

<?php
    function parallelAll()
    {
        $this->parallelSplitTests();
        $result = $this->parallelRun();
        $this->parallelMergeResults();
        return $result;
    }

まとめ

Codeceptionはテストを並列実行するためのツールを提供していません。これは複雑なタスクであり、解決策はプロジェクトに依存しています。私たちはすべての必要なステップを行うために外部ツールとしてのRoboタスクランナーを使いました。テストを並列実行するための準備としてCodeceptionの動的グループと環境の仕組みを利用しました。さらに、テストプロセスに応じて動的な設定を行うための拡張機能とグループクラスを作成することができます。