1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【Laravel】CircleCI 2.1でPHPunit自動テスト × Heroku自動デプロイ

Last updated at Posted at 2022-03-27

0. はじめに

大阪のLaravel初学者サウナーこと、kazumakishimoto(@kazuma_dev)です!
LaravelをCircleCIで自動テスト → Herokuに自動デプロイする方法です!

0-1. 全体の流れ

1.Heroku
2.CircleCI
3.補足
Reference

0-2. 本記事の対象者

  • CircleCIでHerokuに自動テスト自動デプロイしたい方

0-3. 事前準備

  • Heroku CLIインストール / Herokuログイン済み
  • Heroku手動デプロイ済み
  • CircleCIとGitHub連携済み

0-4. 要件

  • Heroku設定
  • CircleCI自動デプロイ設定
  • MySQLやnginxを使う方法もありますが、Herokuでは非推奨

1. Heroku

1-1. 外部API

  • HerokuのURLをリダイレクトURIに追加

リソースの管理 – Google Cloud Platform.png
image.png
image.png

https://(Herokuのアプリケーション名).herokuapp.com/login/google/callback

1-2. 環境変数

  • Config Varsに.env反映
.env
// 略
# Sendgrid
MAIL_DRIVER=smtp
MAIL_HOST=smtp.sendgrid.net
MAIL_PORT=587
MAIL_USERNAME=apikey
MAIL_PASSWORD=SG.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
MAIL_ENCRYPTION=tls
MAIL_FROM_NAME=memo
MAIL_FROM_ADDRESS=(Sendgridに設定したメールアドレス)

# Google
GOOGLE_CLIENT_ID=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxx

image.png

1-3. composer.json

  • Herokuの最新バージョン(heroku-20)がPHP7.2系をサポートしておらず、PHP8がインストールされエラーになります。

ERROR: Dependency installation failed! The 'composer install' process failed with an error.

image.png

  • composer.json編集
composer.json
"require": {
-   "php": "^7.2.5|^8.0",
+   "php": "^7.3.0,",
local
$ docker-compose exec app composer update

1-4. サブディレクトリ設定

  • heroku-buildpack-monorepoを追加
local
$ heroku buildpacks:add -a (アプリ名) https://github.com/lstoll/heroku-buildpack-monorepo
  • サブディレクトリのパスをconfigに追加
local
$ heroku config:add APP_BASE=(サブディレクトリパス) -a (アプリ名)
  • buildpackの順番入れ替え(※言語のbuildpackより上にする必要がある)

image.png

1-5. 強制HTTPS化

環境変数

local
$ heroku config:set APP_ENV=production

AppServiceProvider

app/Providers/AppServiceProvider.php
public function boot()
{
    if (\App::environment(['production'])) {
        \URL::forceScheme('https');
    }
}

ForceHttps

local
$ docker-compose exec app php artisan make:middleware ForceHttps
app/Http/Middleware/ForceHttps.php
public function handle($request, Closure $next)
{
  if (\App::environment(['production']) && $_SERVER["HTTP_X_FORWARDED_PROTO"] != 'https') {
    return redirect()->secure($request->getRequestUri());
  }
  return $next($request);
}

Kernel

app/Http/Kernel.php
protected $middleware = [
  \App\Http\Middleware\ForceHttps::class, // 追加
];

2. CircleCI

2-1. PHPunit

.env.testing

local
$ cp .env .env.testing
$ docker-compose exec app php artisan key:generate --env=testing
base64:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=
$ vi .env.testing
.env.testing
APP_NAME=(アプリ名)
APP_ENV=testing
+ APP_KEY=base64:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=
APP_DEBUG=true
APP_URL=http://127.0.0.1

LOG_CHANNEL=daily

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel-test
DB_USERNAME=root
DB_PASSWORD=

# 略

ExampleTest削除

  • Unit ディレクトリが無いと CI 実行時エラーになる
local
$ rm ./tests/Feature/ExampleTest.php
$ rm ./tests/Unit/ExampleTest.php
$ touch ./tests/Unit/.gitkeep

ArticleControllerTest.php

local
$ docker-compose exec app php artisan make:test ArticleControllerTest
tests/ArticleControllerTest.php
<?php

namespace Tests\Feature;

use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;

class ArticleControllerTest extends TestCase
{
    use RefreshDatabase;

    public function testIndex()
    {
        $response = $this->get(route('articles.index'));

        $response->assertStatus(200)
            ->assertViewIs('articles.index');
    }

    public function testGuestCreate()
    {
        $response = $this->get(route('articles.create'));

        $response->assertRedirect(route('login'));
    }

    public function testAuthCreate()
    {
        // テストに必要なUserモデルを「準備」
        $user = factory(User::class)->create();

        // ログインして記事投稿画面にアクセスすることを「実行」
        $response = $this->actingAs($user)
            ->get(route('articles.create'));

        // レスポンスを「検証」
        $response->assertStatus(200)
            ->assertViewIs('articles.create');
    }
}

ArticleTest.php

local
$ docker-compose exec app php artisan make:test ArticleTest
tests/ArticleTest.php
<?php

namespace Tests\Feature;

use App\Models\Article;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;

class ArticleTest extends TestCase
{
    use RefreshDatabase;

    public function testIsLikedByNull()
    {
        $article = factory(Article::class)->create();

        $result = $article->isLikedBy(null);

        $this->assertFalse($result);
    }

    public function testIsLikedByTheUser()
    {
        $article = factory(Article::class)->create();
        $user = factory(User::class)->create();
        $article->likes()->attach($user);

        $result = $article->isLikedBy($user);

        $this->assertTrue($result);
    }

    public function testIsLikedByAnother()
    {
        $article = factory(Article::class)->create();
        $user = factory(User::class)->create();
        $another = factory(User::class)->create();
        $article->likes()->attach($another);

        $result = $article->isLikedBy($user);

        $this->assertFalse($result);
    }
}

ArticleFactory.php

local
$ docker-compose exec app php artisan make:factory ArticleFactory --model=Article
database/factories/ArticleFactory.php
<?php

/** @var \Illuminate\Database\Eloquent\Factory $factory */

use App\Models\Article;
use App\Models\User;
use Faker\Generator as Faker;

$factory->define(Article::class, function (Faker $faker) {
    return [
        'title' => $faker->text(50),
        'body' => $faker->text(500),
        'user_id' => function () {
            return factory(User::class);
        }
    ];
});

2-2. CircleCI

database.php

config/database.php
        'circle_testing' => [
            'driver' => 'mysql',
            'host' => '127.0.0.1',
            'port' => '3306',
            'database' => 'circle_test',
            'username' => 'root',
            'password' => '',
            'charset' => 'utf8mb4',
            'collation' => 'utf8mb4_unicode_ci',
            'prefix' => '',
            'prefix_indexes' => true,
            'strict' => true,
            'engine' => null,
        ],

config.yml

local
$ mkdir .circleci && touch .circleci/config.yml
.circleci/config.yml
version: 2.1

executors:
  laravel-circleci:
    docker:
      - image: circleci/php:7.4-node-browsers
      - image: circleci/mysql:5.7
    environment:
      - APP_DEBUG: true
      - APP_ENV: testing
      - APP_KEY: base64:oQbVvWs3tHsouVVGIhue/ZQX3p7OuQ8Z4s6oDOhXK7I=
      - DB_CONNECTION: circle_testing
      - MYSQL_ALLOW_EMPTY_PASSWORD: true
    working_directory: ~/repo

commands:
  install-dockerize:
    steps:
      - run:
          name: Install dockerize
          command: wget https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz && sudo tar -C /usr/local/bin -xzvf dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz && rm dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz
          environment:
            DOCKERIZE_VERSION: v0.6.1
  install-php-extensions:
    steps:
      - run:
          name: Install PHP Exetensions
          command: sudo docker-php-ext-install pdo_mysql
          working_directory: src
  restore-cache-composer:
    steps:
      - restore_cache:
          key: v1-dependencies-{{ checksum "src/composer.json" }}
  install-composer:
    steps:
      - run:
          name: Install Composer
          command: composer install -n --prefer-dist
          working_directory: src
  save-cache-composer:
    steps:
      - save_cache:
          key: v1-dependencies-{{ checksum "src/composer.json" }}
          paths:
            - vendor
  npm-ci:
    steps:
      - run:
          name: npm CI
          command: |
            if [ ! -d node_modules ]; then
              npm ci
            fi
          working_directory: src
  restore-cache-npm:
    steps:
      - restore_cache:
          key: npm-cache-{{ checksum "src/package-lock.json" }}
  npm-run-dev:
    steps:
      - run:
          name: Run npm
          command: npm run dev
          working_directory: src
  save-cache-npm:
    steps:
      - save_cache:
          key: npm-cache-{{ checksum "src/package-lock.json" }}
          paths:
            - node_modules
  migration-seeding:
    steps:
      - run:
          name: Migration & Seeding
          command: php artisan migrate --seed
          working_directory: src
  test-unittest:
    steps:
      - run:
          name: Run PHPUnit
          command: vendor/bin/phpunit
          working_directory: src

jobs:
  build:
    executor:
      name: laravel-circleci
    steps:
      - checkout
      - install-dockerize
      - install-php-extensions
      - restore-cache-composer
      - install-composer
      - save-cache-composer
      - restore-cache-npm
      - npm-ci
      - save-cache-npm
      - npm-run-dev
      - migration-seeding
      - test-unittest
  deploy:
    docker:
      - image: circleci/php:7.4-node-browsers
    steps:
      - checkout
      - run:
          name: heroku deploy
          command: |
            git push https://heroku:$HEROKU_API_KEY@git.heroku.com/$HEROKU_APP_NAME.git master

workflows:
  version: 2
  build_deploy:
    jobs:
      - build
      - deploy:
          requires:
            - build
          filters:
            branches:
              only:
                - master

2-3. CircleCIでMySQLを使用する

phpunit.xml
# 略
    <php>
        <server name="APP_ENV" value="testing"/>
        <server name="BCRYPT_ROUNDS" value="4"/>
        <server name="CACHE_DRIVER" value="array"/>
-       <server name="DB_CONNECTION" value="sqlite"/>
-       <server name="DB_DATABASE" value=":memory:"/>
+       <server name="DB_CONNECTION" value="mysql"/>
+       <server name="DB_DATABASE" value="laravel-test"/>
        <server name="MAIL_DRIVER" value="array"/>
        <server name="QUEUE_CONNECTION" value="sync"/>
        <server name="SESSION_DRIVER" value="array"/>
    </php>
</phpunit>

2-4. テスト失敗時にmasterブランチへマージ不可にする

  • 小生はgit-flowのGit管理のためdevelopブランチに設定していますが、マージ先のブランチを指定してください。
    image.png

2-5. 環境変数

  • Heroku API KeyをCircleCIの環境変数に追加
    image.png
    image.png
    image.png

2-6. 動作確認

  • GitHubでPR merge → CircleCIのJobsがSUCCESSになっていればデプロイ成功
    image.png

3. 補足

3-1. 開発環境(FW/ツールのバージョンなど)

ツール バージョン
Vue.js 2.6.14
jQuery 3.4.1
PHP 7.4.1
Laravel 6.20.43
MySQL 5.7.36
Nginx 1.18.0
Composer 2.0.14
npm 6.14.6
Git 2.33.1
Docker 20.10.11
docker-compose v2.2.1
PHPUnit 8.0
CircleCI 2.1
heroku 7.59.4
MacBook Air M1,2020
macOS Monterey 12.3
Homebrew 3.3.8

3-2. ディレクトリ構造

【ルートディレクトリ】
├─ .circleci
│   └─ config.yml
├─ aws / CloudFormation
│   └─ ec2.yml
├─ docker
│   └─ mysql
│   └─ nginx
│   └─ php
│   └─ phpmyadmin
├─ src
│   └─ 【Laravelのパッケージ】
│─ .env
│─ .gitignore
└─ docker-compose.yml

Reference

  • Laravel × CircleCI × AWSで学ぶCI/CD | Techpit

  • 絶対に失敗しないLaravel+Vue.jsのアプリをHerokuにデプロイする方法〜画像付きで丁寧に説明〜

  • 【Heroku】LaravelでHerokuデプロイするとThe 'composer install' process failed with an error.が発生 - Qiita

  • Heroku PHP Support | Heroku Dev Center

  • HerokuにGitHubリポジトリの一部だけデプロイする - Qiita

  • nuxtのサブディレクトリをHerokuにデプロイする方法 - Qiita

  • LaravelでURLをHTTPS化させるメモ (Heroku) - Qiita

  • LaravelをCircleCI 2.0で自動テストしherokuへ自動デプロイ - Qiita

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?