Photoruction工事中!

Photoructionの開発ブログです!

PHPUnit勉強会を開催しました

自己紹介

今年3月に入社した北原です。普段は、サーバーサイドをメインに開発をしつつ、スクラムマスターも兼務しています。

今回書かさせていただく内容は、社内で開催したPHPUnit勉強会の話です。

※弊社は、PHPのFWであるLaravelを採用しています。

PHPUnit勉強会を開催した理由

テストを書く文化が根付いていない

  • テストが現状0ということではなかったんですが、基本的にテストを書くという文化が根付いていなくて、コードを信頼できる状態かどうか怪しいという現状
  • テストが書かれている箇所があっても一般的な状態ではなかった。
    • ここでいう一般的というのは、Laravel wayに則って書くということを指しています。(データ作成やディレクトリ、継承元)

将来的に

  • PHPUnitは書くもんだろ!って人たちからすれば当たり前ですがテストが書かれていない状態でのPRのマージを原則NGにする
  • エンジニア全員がテストをかける状態にする
    • 少なくともサーバーサイドをメインにする人は、書ける

勉強会

以下のことをまず準備しました。

  • Laravelのサンプルプロジェクト
    • 環境毎の差異をなくすためです(色々察していただけると)
  • スライドの作成
    • 今回説明は割愛します。

どんな形式の勉強会にしたのか?

結果的には、Liveコーディングになりましたが、ハンズオン形式のつもりで進めました。

全体の流れ

  • スライドにて今回勉強する内容の説明と前提情報を共有。
  • 実装(主に時間割きました)

基礎編と応用編の2回開催

以下に実際に勉強会で使用したコードの一部を貼っておきます

  • 基礎編
    • 基本的なPHPUnitの書き方の学習
    • 内容としては、AdminがOrganizationに関連付くUserの作成を行うエンドポイントのテストとなっています。
      • 実際には、異常系のテストも書いております。

テスト対象のControllerの記述

<?php

namespace App\Http\Controllers\Api\Admin;

use App\Admin;
use App\Organization;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Auth;
use App\Http\Requests\UserStoreRequest;
use Illuminate\Auth\Access\AuthorizationException;

class UserController extends Controller
{
    private $organization;

    public function __construct(Organization $organization)
    {
        $this->organization = $organization;
    }

    /**
     * Store a newly created resource in storage.
     *
     * @param  UserStoreRequest  $request
     * @param int $organizationId
     * @return \\Illuminate\\Http\\Response
     */
    public function store(UserStoreRequest $request, int $organizationId)
    {
        $admin = Auth::user();
        if ($admin->role_id !== Admin::MASTER) {
            throw new AuthorizationException('権限がありません');
        }
        $validated = $request->validated();
        $organization = $this->organization->findOrFail($organizationId);

        $user = $organization->users()->create($validated);

        return $user;
    }
}

<?php

namespace Tests\Feature;

use App\Admin;
use Tests\TestCase;
use App\Organization;

class ExampleTest extends TestCase
{
    private Organization $organization;
    protected function setUp(): void
    {
        parent::setUp();

        $admin = factory(Admin::class)->create(['role_id' => Admin::MASTER]);
        $this->actingAs($admin, 'admin');
        $this->organization = factory(Organization::class)->create();
    }

    public function test_create_user_success()
    {
        $params = $this->params();

        $response = $this->postJson(route('admin.user-store', ['organization_id' => $this->organization->id]), $params);
        $response->assertStatus(201);

        $data = $response->json();
        $this->assertEquals($params['name'], $data['name']);
        $this->assertEquals($params['email'], $data['email']);
        $this->assertEquals($this->organization->id, $data['organization_id']);
    }

    private function params(): array
    {
        return [
            'name' => 'test',
            'email' => 'test@gmail.com',
            'password' => 'hogehoge12'
        ];
    }
}

  • 応用編
    • Reflectionを使用してのprivateメソッドに対してのテストの実装
    • dataProviderを使用してのテストの実装
    • 下記が実際に勉強会で使用したコードになります。
      • 目的がdataProviderの使い方とprivateメソッドに対してのテストの書き方なのでコードは複雑なものにはしておりません。

テスト対象のServiceの記述

<?php

namespace App\Services;

use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;

class SampleService
{
    const TYPE_NUMBER = [
        1 => 'first',
        2 => 'second',
        3 => 'third',
        4 => 'four'
    ];

    /**
     * [
     *   'isXxxx' => true / false
     *   'canXxx' => true / false
     * ]
     * @param array $params
     * @return array
     */
    public function getXxxx(array $params): array
    {
        if ($params['is_xxx']) {

            if ($params['can_xxx']) {
                return [
                    'message' => 'どちらもtrueです',
                    'can_edit' => true
                ];
            }

            return [
                'message' => 'is_xxxはtrueでcan_xxxはfalseです',
                'can_edit' => true
            ];
        }

        if ($params['can_xxx']) {
            return [
                'message' => 'is_xxxはfalseでcan_xxxはtrueです',
                'can_edit' => false
            ];
        }

        return [
            'message' => 'どちらもfalseです',
            'can_edit' => false
        ];
    }

    private function firstMethod(int $typeNumber)
    {
        if (!in_array($typeNumber, array_keys(self::TYPE_NUMBER), true)) {
            throw new BadRequestHttpException('正しい選択を行なってください');
        }

        return self::TYPE_NUMBER[$typeNumber];
    }
}

<?php

namespace Tests\Unit;

use ReflectionClass;
use App\Services\SampleService;
use Tests\TestCase;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;

class ExampleTest extends TestCase
{
    private $service;
    private $reflection;
    protected function setUp(): void
    {
        parent::setUp();
        $this->service = app()->make(SampleService::class);
        $this->reflection = new ReflectionClass($this->service);
    }

    /**
     * @dataProvider params
     * @param array $params
     * @param string $message
     * @param bool $canEdit
     */
    public function test_getXxx(array $params, string $message, bool $canEdit)
    {
        $result = $this->service->getXxxx($params);

        $this->assertEquals($message, $result['message']);
        $this->assertEquals($canEdit, $result['can_edit']);
    }

    public function params()
    {
        return [
            'どちらもtrue' => [
                [
                    'is_xxx' => true,
                    'can_xxx' => true
                ],
                'どちらもtrueです',
                true
            ],
            'canXxxはfalse' => [
                [
                    'is_xxx' => true,
                    'can_xxx' => false
                ],
                'isXxxはtrueでcanXxxはfalseです',
                true
            ],
            'canXxxはtrue' => [
                [
                    'is_xxx' => false,
                    'can_xxx' => true
                ],
                'isXxxはfalseでCanXxxはtrueです',
                false
            ],
            'どちらもfalse' => [
                [
                    'is_xxx' => false,
                    'can_xxx' => false
                ],
                'どちらもfalseです',
                false
            ]
        ];
    }

    public function test_first_method_success()
    {
        $method = $this->reflection->getMethod('firstMethod');
        $method->setAccessible(true);
        $result = $method->invoke($this->service, 1);
        $this->assertEquals(
            $this->service::TYPE_NUMBER[1],
            $result
        );
    }

    public function test_first_method_fail()
    {
        $this->expectException(BadRequestHttpException::class);
        $method = $this->reflection->getMethod('firstMethod');
        $method->setAccessible(true);
        $method->invoke($this->service, 5);
    }
}

勉強会のその後

  • 少しずつですがテストの書かれたPRが増えつつあるのかな?という所感です。
  • また私自身は、テスト警察ではないですがPRに対して「テストは書けないですか?」という問いを文化が根付くまで続けていく予定です。

今後

応用編で扱いきれなかったモックに関しての勉強会が開けていないのでやらないとなーと思っています。

また少しずつでもいいからテストに慣れてもらい、今よりもコードの質(仕様通り動くよねという意味で)を上げれていけたらなと思っています。

 

株式会社フォトラクションでは一緒に働く仲間を募集しています