Webアプリケーションやモバイルアプリケーションには、新機能の実装が不可欠であり、往々にして重要な意味合いを持っています。しかし、新しいコードを追加すれば、バグのないアプリケーションを構築するという目標の達成が阻害されるおそれもあります。慎重に進めなければ、既に実装されている機能を壊してしまうかもしれません。信頼性に優れた高品質のアプリケーションを構築するには、ユニットテストと機能テストによってコードを検証するのが 1 つの方法です。

テストを行うと、新しいリリースが品質目標とパフォーマンス目標を満たしていることを確認できるため、ベストプラクティスとして推奨されています。アプリケーションの開発言語や開発フレームワークに関係なく、テスト駆動型の開発アプローチには多くの共通点があります。

今回のチュートリアルでは、顧客情報を管理する新しい Symfony アプリケーションのプロジェクトを作成します。このアプリケーションには完全な CRUD 機能は実装しませんが、いくつかのメソッドとコントローラーを作成してユニットテストと機能テストを実施し、それぞれの関数から想定どおりの応答が返されることを確認します。最後に、CircleCI を活用してテストを自動化します。

前提条件

この記事は、以下の準備を済ませたうえでお読みいただくと、よりいっそう理解が深まります。

作業を開始する

まず Composer で新しい Symfony アプリケーションを作成します。以下のコマンドを実行します。

composer create-project symfony/website-skeleton new-symfony-app

上記のコマンドにより、コマンドを実行したルート フォルダーに new-symfony-app という名前のフォルダーが作成され、必要な依存関係がすべてインストールされます。

次に、新たに作成されたプロジェクトに移動して、アプリケーションを実行する Web サーバーをインストールします。

// move into project
cd new-symfony-app

// installl web server
composer require symfony/web-server-bundle --dev ^4.4.2

これで新しいプロジェクトがセットアップされました。次は Git によってプロジェクトをローカルで初期化し、GitHub と連携させる必要があります。ただその前に、GitHub に移動してプロジェクトのリポジトリを作成しておきます。

GitHubでリポジトリの作成

GitHub のプロジェクトには同じ名前を使ってかまいません。では、ローカルのプロジェクト フォルダーで Git を初期化します。

git init

これでプロジェクトが Git リポジトリとしてセットアップされます。次に、変更をローカルでコミットします。

git add .

git commit -m "Initial commit"

git remote add コマンドを使用して、プロジェクト フォルダーのターミナルでリモートリポジトリを登録します。このコマンドには以下の 2 つの引数を設定します。

  • リモート名 - 例: origin
  • リモート URL - 例: https://github.com/<your_username>/<your_repo>.git

今回は以下のようにします。

// add remote origin
git remote add origin https://github.com/yemiwebby/new-symfony-app.git

次に、GitHub アカウントで作成した master ブランチにローカルプロジェクトをプッシュします。以下のコマンドを実行します。

// push to the repo
git push -u origin master

これで新しい Symfony プロジェクトがセットアップされ、リモートリポジトリにプッシュされました。次のセクションでは、Customer のコントローラーを作成し、エンティティ クラスを作成します。

コントローラーを作成する

コントローラーの役割は、HTTP リクエストを処理して適切な応答を返すことです。このアプリケーション用のコントローラーを自動的に生成するには、Symfony と共にインストールされる MakerBundle を使用します。以下のコマンドを実行します。

php bin/console make:controller CustomerController

これにより、以下 2 つのファイルが新たに作成されます。

  • Aコントローラー – 場所: src/Controller/CustomerController.php
  • ビュー ページ – 場所: templates/customer/index.html.twig

CustomerController.php ファイルを開き、内容を以下で置き換えます。

<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;

class CustomerController extends AbstractController
{
    /**
     * @Route("/", name="customer", methods={"GET"})
     */
    public function index()
    {
        $customerName  = "John Doe";
        return $this->render('customer/index.html.twig', [
            'name' => $customerName,
        ]);
    }
}

アプリケーションのルートを定義する場合、Symfony が最も推奨するオプションはルート アノテーションです。ここでは、customer という名前のルート / を定義しています。これは、ホームページに対するすべてのリクエストを、ここで定義するメソッドに転送するという Symfony への命令になります。つまり、ユーザーのリクエストがこのルートをヒットすると、上記のファイル内にハードコードされた $customerName を含むビューが、index() によってレンダリングされます。

次に、templates/customer/index.html.twig のビュー ファイルを開き、内容を以下で置き換えます。

{% extends 'base.html.twig' %}
{% block title %} Customer Page {% endblock %}
{% block body %}
<div class="example-wrapper">
  <h1>Hello, Welcome to the customer page</h1>

  <h2>Customer name is : {{ name }}</h2>
</div>
{% endblock %}

この更新により、既定の顧客名を示す name 変数をビューが動的にレンダリングするようになります。以下のコマンドを使用することで、このアプリケーションをローカルで実行できます。

php bin/console server:run

次に http://localhost:8000 に移動してホームページを表示します。

アプリケーションのホームページ

このページは、コントローラー内でハードコードされた顧客名をレンダリングし、ビューに渡しています。MVC 構造のアプリケーションのビュー内でコンテンツを動的にレンダリングするのと似たプロセスです。次に、この動作を検証する機能テストを記述していきます。

エンティティを作成する

ここでは Customer オブジェクトを表すモデルを作成します。そのために、以下のコマンドを実行します。

php bin/console make:entity Customer

src/Entity/Customer.php ファイルへのフィールドの追加を求めるメッセージが表示されます。フィールドは手動で追加することもできますが、「firstName」と「lastName」を Customer クラスのフィールドとして入力すると、Symfony MakerBundle が残りの処理を実行してくれます。以下の図を参照してください。

Creating entity using makerbundle

Customer.php ファイルは以下のようになります。確認し、こちらの内容に手元のファイルを合わせて更新してください。

<?php

namespace App\Entity;

use App\Repository\CustomerRepository;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass=CustomerRepository::class)
 */
class Customer
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="string", length=255)
     */
    private $firstName;

    /**
     * @ORM\Column(type="string", length=255)
     */
    private $lastName;

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getFirstName(): ?string
    {
        return $this->firstName;
    }

    public function setFirstName(string $firstName): self
    {
        $this->firstName = $firstName;

        return $this;
    }

    public function getLastName(): ?string
    {
        return $this->lastName;
    }

    public function setLastName(string $lastName): self
    {
        $this->lastName = $lastName;

        return $this;
    }

    public function getCustomerFullName(): string
    {
        return $this->getFirstName() . '' . $this->getLastName();
    }
}

作成された gettersetter は、Symfony の自動生成によるものです。また、顧客のフル ネームを返す新しいメソッド getCustomerFullName() を追加しています。チュートリアルの後半では、ここで定義された個々の関数に対応するユニットテストを記述します。

PHPUnit を使ってテストを作成する

Symfony は、PHP のテスト フレームワーク PHPUnit と連携しています。どのようなテストを記述する場合も、以下のコマンドによって必ず事前に PHPUnit を実行してください。

./bin/phpunit

今回のようにこのコマンドを初めて実行する場合は、PHPUnit とアプリケーションに必要な依存関係がすべてインストールされます。

ユニットテストを記述する

一般的にユニットテストは、コードの個別コンポーネント (ユニット) のテストと定義されます。Symfony アプリケーション用のユニットテストは、標準的な PHPUnit ユニットテストの場合と同様に記述されます。ここでは、Customer Entity ファイル内に作成されたメソッドのテストを記述していきます。

まず、tests フォルダー内に Entity という新しいフォルダーを作成します。新しく作成したフォルダー内に新しいファイルを作成し、CustomerTest.php という名前を付けます。このファイルを開き、以下のような内容にします。

<?php
namespace App\Tests\Entity;

use App\Entity\Customer;
use PHPUnit\Framework\TestCase;

class CustomerTest extends TestCase
{
    public function testSettingCustomerFirstName()
    {
        $customer = new Customer();
        $firstName = "John";

        $customer->setFirstName($firstName);

        $this->assertEquals($firstName, $customer->getFirstName());
    }

    public function testSettingCustomerLastName()
    {
        $customer = new Customer();
        $lastName = "Doe";

        $customer->setLastName($lastName);

        $this->assertEquals($lastName, $customer->getLastName());
    }

    public function testReturnsCustomerFullName()
    {
        $customer = new Customer();
        $customer->setFirstName("John");
        $customer->setLastName("Deo");

        $fullName = $customer->getFirstName() . '' . $customer->getLastName();

        $this->assertSame($fullName, $customer->getCustomerFullName());
    }
}

上記のファイルでは、次の 3 種類のテストを記述しました。

  • testSettingCustomerFirstName(): ここでは Customer クラスのインスタンスを作成し、顧客のファーストネームを表すダミー値を設定しました。次に PHPUnit のアサーション メソッドを使用して名前を確認しています。
  • testSettingCustomerLastName(): 1 つ目に定義したメソッドと同様に、顧客のラストネーム用に作成された getter メソッドと setter メソッドの両方をテストしました。
  • testReturnsCustomerFullName(): このテストでは最後に、顧客の firstNamelastName が正常に取得できることを確認します。

テストをローカルで実行する

先ほどはターミナルで ./bin/phpunit コマンドを使用してテストを実行しました。これでも問題はありませんが、少し構成に手を加えたいと思います。composer.json ファイルの scripts セクションに追加することで、このコマンドを自動化します。composer.json を開き、テスト コマンドを scripts セクションに追加します。

{
    ...
    "scripts": {
        ...,
        "test": [
            "./bin/phpunit"
        ]
    }
}

これ以降はこのテスト コマンドを composer test として利用できます。

テストユニット

プロジェクトを CircleCI に追加する前に、非常にシンプルな機能テストを記述してみましょう。

機能テストを記述する

機能テストでは、Web サイトの閲覧、リンクのクリック、フォームへの入力、ページに表示された要素の確認などを、ブラウザに命令することができます。これを行うには、tests フォルダーに Controller というフォルダーを作成します。CustomerController のテスト スクリプトは、このフォルダーに格納されます。新しく作成したフォルダー内に CustomerControllerTest.php というファイルを作成します。このファイルを開き、以下のような内容にします。


<?php

namespace App\Tests\Controller;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class CustomerControllerTest extends WebTestCase
{
    public function testShowCustomer()
    {
        $client = static::createClient();

        $client->request('GET', '/');

        $this->assertResponseStatusCodeSame(200, $client->getResponse()->getStatusCode());

        $this->assertStringContainsString('Customer name is : John Doe', $client->getResponse()->getContent());
    }
}

上記のコード スニペットでは、ステータスコード 200 を返し、HTTP 応答が成功したことを確認しています。次に、先ほどチュートリアルで表示されていた内容が、想定どおりページに含まれていることを確認しています。

このテストは、以下のコマンドで再実行できます。

composer test

Run test functional

CircleCI でテストを自動化する

すべてのテストがローカルで成功することがわかったら、コードの更新が GitHub リポジトリにプッシュされたときにテストが自動実行されるようにする必要があります。

CircleCI が使用する構成ファイルを作成するには、プロジェクトのルートに移動して .circleci という名前のフォルダーを作成し、その中に config.yml という名前のファイルを作成して、以下のような内容にします。

version: 2
jobs:
  build:
    docker:
      # Specify the version you desire here
      - image: circleci/php:7.4-node-browsers

    steps:
      - checkout

      - run: sudo apt update # PHP CircleCI 2.0 Configuration File# PHP CircleCI 2.0 Configuration File sudo apt install zlib1g-dev libsqlite3-dev
      - run: sudo docker-php-ext-install zip

      # Download and cache dependencies
      - restore_cache:
          keys:
            # "composer.lock" can be used if it is committed to the repo
            - v1-dependencies-{{ checksum "composer.json" }}
            # fallback to using the latest cache if no exact match is found
            - v1-dependencies-

      - run:
          name: "Install Dependencies"
          command: composer install -n --prefer-dist

      - save_cache:
          key: v1-dependencies-{{ checksum "composer.json" }}
          paths:
            - ./vendor

      # run tests with phpunit
      - run:
          name: "Run tests"
          command: composer test

ここではまず、Docker イメージと共にインストールする PHP のバージョンを指定しています。この Docker イメージは CircleCI イメージ レジストリからプルされます。次に、すべての依存関係をプロジェクトにインストールするよう指定し、最後にテストの実行コマンドを含めています。

この段階で、これまでの変更をすべてコミットし、すべての更新を GitHub のプロジェクト リポジトリにプッシュすることができます。

CircleCI にプロジェクトをセットアップする

ここでは Symfony アプリケーションを CircleCI に接続します。CircleCI の Web サイトに移動して、お使いのアカウントでログインします。CircleCI コンソールで [Add Projects (プロジェクトの追加)] ページに移動して [Set Up Project (プロジェクトのセットアップ)] をクリックします。

CircleCI プロジェクトのダッシュボード

これにより、以下のようなページが表示されます。

CircleCI project configuration page

[Start Building (ビルドの開始)] ボタンをクリックすると、提供されている CircleCI 構成を新しいブランチのプロジェクトに追加するか、手動で構成をセットアップするかを確認するメッセージが表示されます。[Add Manually (手動で追加)] を選択します。

Start building prompt

構成ファイルは GitHub にプッシュする前に追加しているため、[Start Building (ビルドの開始)] をクリックします。これにより、構成ファイルに基づいて継続的インテグレーション (CI) パイプラインが実行され、パイプラインのステータスが表示されます。

以下は実行中の状態です。

Build running

以下は実行が成功した後の状態です。

ビルドの成功画面

ビルドの結果

これですべての手順が完了しました。作成したすべてのテストは正常に実行されています。

まとめ

Symfony は、洗練された構造と再利用可能なコンポーネントで知られ、最も長い歴史を誇る、最も安定した PHP フレームワークとして高く評価されています。このチュートリアルでは、新しい Symfony アプリケーションを作成し、定義したメソッドおよびコントローラー用のユニットテストと機能テストを PHPUnit で記述する方法を説明しました。さらに CircleCI を使って自動化された CI パイプラインをセットアップする方法を解説しました。

残念ながら、テストのエキスパートになるためには、この記事の知識だけでは到底足りません。ここで紹介したのはごく一部の情報であり、さらに多くの知識は各自の努力によって習得する必要があります。しかし、一般的な Symfony アプリケーションにおけるテスト構造の基礎知識や、テストを自動化する基本的な方法については理解していただけたのではないかと思います。今回のソース コードは GitHub で公開されているので、中身をじっくりご覧になると共に、CircleCI の公式ドキュメントを参照して、さらに学習を進めていただければ幸いです。


Olususi Oluyemi 氏は、テクニカルの世界に大きな情熱を傾け、プログラミングに没頭し、Web 開発なしには生きていけないほど、最新テクノロジーを取り入れることが大好きです。

さんの他の投稿を読む Olususi Oluyemi