🙌

CircleCIでLaravelの自動テストを動かしてみる

2021/11/26に公開

今回取り上げること

  • CircleCIでのPHPStan(Larastan)の設定
  • CircleCIでのPHPUnitの設定

今回深くは触れないこと

  • CircleCIの細かい設定
  • PHPStan(Larastan)の導入の詳細
  • PHPUnitでのテストコードの深い話し

Larastan

PHPStanのLaravel用のラッパーしたパッケージです。
インストール済みであれば、

./vendor/bin/phpstan

で実行できます。

実行時の設定ファイル

phpstan.neon
includes:
    - ./vendor/nunomaduro/larastan/extension.neon
    - phpstan-baseline.neon

parameters:
    paths:
        - app
        - bootstrap
        - config
        - database
        - resources/views
        - routes
    level: 0

levelは0〜7まで設定可能。今回はまず0から始めました。(それでもいっぱいエラー出たけど)

PHPUnitのテストコード

tests/Feature/ExampleTest.php
<?php

namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class ExampleTest extends TestCase
{
    /**
     * A basic test example.
     *
     * @return void
     */
    public function test_example()
    {
        $response = $this->get('/');

        $response->assertStatus(200);
    }
}

こちらLaravelの元から入っているテストコードです。
php artisan testで実行できます。
レスポンスのステータスコードが200であるかをチェックします。
他にも色々なassert系が用意されていて、それらを使用してテストしていく感じです。

本題のCircleCIでの設定

version: 2.1

executors:
  deploy_container:
    docker:
      - image: cimg/php:8.0.13
        environment:
          APP_ENV: testing
          DB_CONNECTION: mysql
          DB_HOST: 127.0.0.1
          DB_DATABASE: testing
          DB_USERNAME: 'root'
          DB_PASSWORD: ''
      - image: circleci/mysql:8
        environment:
          MYSQL_DATABASE: testing
          MYSQL_ROOT_HOST: '%'
          MYSQL_ALLOW_EMPTY_PASSWORD: 'true'
          TZ: Asia/Tokyo
orbs:
  slack: circleci/slack@4.3.0

commands:
  run_test:
    steps:
      - checkout
      - restore_cache:
          keys:
            - v1-dependencies-{{ checksum "backend/composer.json" }}
            - v1-dependencies-
      - run:
          name: composer install
          working_directory: backend
          command: composer install
      - run:
          name: laravel setting
          working_directory: backend 
          environment:
            ENV: circleci
          command: sh ../infra/docker/php/laravel_setting.sh
      - save_cache:
          paths:
            - backend/vendor
          key: v1-dependencies-{{ checksum "backend/composer.json" }}
      - run: 
          name: run phpstan
          working_directory: backend
          command: ./vendor/bin/phpstan --memory-limit=1G analyse
      - run: 
          name: run cp env file
          working_directory: backend
          command: cp .env.example .env  
      - run: 
          name: run key generate
          working_directory: backend
          command: php artisan key:generate
      - run: 
          name: run migrate
          working_directory: backend
          command: php artisan migrate:fresh --seed
      - run: 
          name: run phpunit
          working_directory: backend
          command: php artisan test
  notify_slack_pass:
    steps:
      - slack/notify:
          event: pass
          custom: |
            {
              "blocks": [
                {
                  "type": "section",
                  "text": {
                    "type": "mrkdwn",
                    "text": ":tada::tada::tada: *Success!*"
                  }
                },
                {
                  "type": "section",
                  "fields": [
                    {
                      "type": "mrkdwn",
                      "text": "*Project:*\n${CIRCLE_PROJECT_REPONAME}"
                    },
                    {
                      "type": "mrkdwn",
                      "text": "*When:*\n$(TZ=Asia/Tokyo date +'%Y/%m/%d %T')"
                    },
                    {
                      "type": "mrkdwn",
                      "text": "*Job:*\n${CIRCLE_JOB}"
                    },
                    {
                      "type": "mrkdwn",
                      "text": "*Author:*\n${CIRCLE_USERNAME}"
                    },
                    {
                      "type": "mrkdwn",
                      "text": "*Branch:*\n${CIRCLE_BRANCH}"
                    },
                    {
                      "type": "mrkdwn",
                      "text": "*Tag:*\n${CIRCLE_TAG}"
                    }
                  ]
                },
                {
                  "type": "actions",
                  "elements": [
                    {
                      "type": "button",
                      "text": {
                        "type": "plain_text",
                        "text": "View Job"
                      },
                      "url": "${CIRCLE_BUILD_URL}"
                    }
                  ]
                }
              ]
            }

  notify_slack_fail:
    steps:
      - slack/notify:
          event: fail
          custom: |
            {
              "blocks": [
                {
                  "type": "section",
                  "text": {
                    "type": "mrkdwn",
                    "text": ":japanese_ogre::japanese_ogre::japanese_ogre: *Failed* :bangbang:"
                  }
                },
                {
                  "type": "section",
                  "fields": [
                    {
                      "type": "mrkdwn",
                      "text": "*Project:*\n${CIRCLE_PROJECT_REPONAME}"
                    },
                    {
                      "type": "mrkdwn",
                      "text": "*When:*\n$(TZ=Asia/Tokyo date +'%Y/%m/%d %T')"
                    },
                    {
                      "type": "mrkdwn",
                      "text": "*Job:*\n${CIRCLE_JOB}"
                    },
                    {
                      "type": "mrkdwn",
                      "text": "*Author:*\n${CIRCLE_USERNAME}"
                    },
                    {
                      "type": "mrkdwn",
                      "text": "*Branch:*\n${CIRCLE_BRANCH}"
                    },
                    {
                      "type": "mrkdwn",
                      "text": "*Tag:*\n${CIRCLE_TAG}"
                    }
                  ]
                },
                {
                  "type": "actions",
                  "elements": [
                    {
                      "type": "button",
                      "text": {
                        "type": "plain_text",
                        "text": "View Job"
                      },
                      "url": "${CIRCLE_BUILD_URL}"
                    }
                  ]
                }
              ]
            }

jobs:
  run_test:
    executor: deploy_container
    steps:
      - checkout
      - run_test
      - notify_slack_fail
      - notify_slack_pass

workflows:
  push-branch-workflow:
    jobs:
      - run_test:
          name: "run_test"
~~省略~~

CircleCIの設定はこんな感じです。
PHPUnitでテストコードを実行するためにMySqlのイメージが必要だったのですが、それがうまく動かなくて詰まりました。
主に詰まったポイント

  • DB_HOSTを127.0.0.1にする。local環境のdocker-compose.ymlではコンテナ名にしていたがそれだと繋げられ無いため。
  • mysqlにrootで接続させる。以下の設定にしました。
MYSQL_ROOT_HOST: '%'
MYSQL_ALLOW_EMPTY_PASSWORD: 'true'

ちなみに実際のjobを確認するとMySqlのイメージをビルドするところで、「-」と付いていますがこれは問題ないようです。

logメッセージの最後にmysqld: ready for connectionsとなっているかを確認する。

slackへの通知

テスト成功時と失敗時のslackへの通知はクラスメソッドさんの記事を参考にさせていただきました。
slackのアプリ追加して、CircleCIのorbsを使えば簡単です。

orbs:
  slack: circleci/slack@4.3.0


まとめ

実際にテストを通過するとこのように表示されているはず。

Larastan

PHPUnit

あとはLarastanの方は徐々にレベルを上げてソースコードの静的解析を厳しくしていく。
PHPUnitのテストコードの数を増やしていく。
これをどんどんやっていきたいですね。

参考にさせて頂いた神記事

https://dev.classmethod.jp/articles/circleci-orbs-notify-slack/
https://qiita.com/ngmy/items/4da6ec9a0b3d53617db4

Discussion