もがき系プログラマの日常

もがき系エンジニアの勉強したこと、日常のこと、気になっている技術、備忘録などを紹介するブログです。

LaravelのFormRequestのテストを書く

こんばんは

今回もLaravelネタです。

FormRequestはLaravel使っているうえで結構使うのですが、それのテストって書いてなかったなぁと思ったので、書いてみようと思いました。

本題

まずこんな感じのFormRequest作ります。

UserRegisterRequest.php

<?php

namespace App\Requests;

use App\Requests\Rules\UserStatusExistsRule;
use Illuminate\Foundation\Http\FormRequest;

class UserRegisterRequest extends FormRequest
{
    /**
     * @return bool
     */
    public function authorize(): bool
    {
        return true;
    }

    /**
     * @return array
     */
    public function rules(): array
    {
        return [
            'name'    => 'required|string',
            'status'   => ['required', 'numeric', new UserStatusExistsRule()],
        ];
    }
}

name, status を リクエストでもらってそれを検証するようなかんたんな形です。

ちなみに、 UserStatusExistsRule は以下のような形です。

UserStatusExistsRule.php

<?php

namespace App\Requests\Rules;

use App\Domain\ValueObject\UserStatus;
use Illuminate\Contracts\Validation\Rule;

class UserStatusExistsRule implements Rule
{
    /**
     * @param $attribute
     * @param $value
     * @return bool
     */
    public function passes($attribute, $value): bool
    {
        try {
            new UserStatus((int)$value)); // 1 or 2でないとExceptionが発生する
            $result = true;
        } catch (\Throwable $e) {
            $result = false;
        }
        return $result;
    }
}

これに対してのテストコードは以下

UserRegisterRequestTest.php

<?php

namespace Tests\Requests;

use App\Requests\UserRegisterRequest;
use Illuminate\Routing\Route;
use Illuminate\Support\Facades\Validator;
use Symfony\Component\HttpFoundation\Request;
use Tests\TestCase;

class UserRegisterRequestTest extends TestCase
{
    /**
     * @test
     * @dataProvider validationDataProvider
     * @param array $parameters
     * @param bool $expected
     * @param string $message
     * @return void
     */
    public function validation(array $parameters, bool $expected, string $message): void
    {
        $request = new UserRegisterRequest();
        $validator = Validator::make($parameters, $request->rules(), $request->message());
        self::assertSame($expected, $validator->passes(), $message);
    }

    /**
     * @return array
     */
    public function validationDataProvider(): array
    {
        return [
            [
                [],
                false,
                'リクエストが送られていないのでfalse'
            ],
            [
                ['name' => 'aa'],
                false,
                'statusが送られていないのでfalse'
            ],
            [
                ['status' => '1'],
                false,
                'nameが送られていないのでfalse'
            ],
            [
                ['name' => 'ああああ', 'status' => 'ああ'],
                false,
                'statusが数値型ではないのでfalse'
            ],
            [
                ['name' => 'ああああ', 'status' => '1000'],
                false,
                'statusが許された値ではないのでfalse'
            ],
            [
                ['name' => 'ああああ', 'status' => '1'],
                true,
                '成功パターン'
            ],
        ];
    }
}

作った際のメリットとしては、以下の2点かなと思いました。

  • リクエストの全体を網羅的にかんたんにテストできる。
  • 上記ができるのでControllerのFeatureテストを薄く出来る。

デメリットは書くのめんどくせーと思うくらいですかね。。。

もっとやりやすいテスト方法あれば教えて下さい!

終わりに

最近やっと本読めるような時間ができてきました。

そういえば、Web+DB Press vol.127 のリファクタリングの話すごくためになりました。

特に凝集度の話勉強になりました。

読んでない方はぜひ

laravelのFormRequestで独自ルールの作成

こんばんは。

最近ずっとlaravelしてます。

だいぶやり方忘れています。

今回はバリデーションの独自ルールの作成のやり方を備忘録で残しておきます。

バージョンは以下

# php -v
PHP 7.4.26 (cli)

 # php artisan -V
Laravel Framework 8.83.0

本題

コマンド的には以下で作れます。

# php artisan make:rule TestRule
Rule created successfully.

作成されたRuleクラスは以下です。

<?php

namespace App\Rules;

use Illuminate\Contracts\Validation\Rule;

class TestRule implements Rule
{
    /**
     * Create a new rule instance.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Determine if the validation rule passes.
     *
     * @param  string  $attribute
     * @param  mixed  $value
     * @return bool
     */
    public function passes($attribute, $value)
    {
        //
    }

    /**
     * Get the validation error message.
     *
     * @return string
     */
    public function message()
    {
        return 'The validation error message.';
    }
}

ようするに Illuminate\Contracts\Validation\Rule Interfaceをimplementsしたクラスを作ればいいって感じです。

使う側はこんな感じ

    public function rules(): array
    {
        return [
            'item_id'  => ['required', 'integer', new TestRule()],
        ];
    }

独自ルールになんらかをコンストラクタインジェクションしたいときとかは以下

    public function rules(): array
    {
        return [
            'item_id'  => ['required', 'integer', $this->container->get(TestRule::class)],
        ];
    }

ある程度の形だったら FormRequestには prepareForValidation() とかあるからバリデーション前に 展開して 確認とかもできちゃうから なかなか新規では作らなそうなイメージですが、ある特殊なフォーマットで正規表現して確認したいとか、API通信して存在確認したいとか、そんな感じが多いかなと思います。

いろんな機能があっておもしろいっすね。

終わりに

地獄の2月が終わったので、今月は少し楽になりますww

来週からはちゃんと本読んで勉強しようと思います。

Mockeryのwith()を使う際に引数にオブジェクトをそのまま渡せない

はじめに

こんばんは。

phpunitのモックライブラリの prophecy をずっと使っていたのですが、最近 laravelの案件をやりはじめてMockery使ってテスト書くにあたりちょっと勝手が違った部分があったので、備忘録で残しておきます。

内容はタイトルのとおりです。

php version, laravel versionは以下

 # php -v
PHP 7.4.26

# php artisan -V
Laravel Framework 8.83.0

本題

今回の場合は引数にlaravelの collection を渡すようなメソッドのモックを作りたい感じでした。

prophecy の場合は以下みたいな感じでだと思います。

$mockA = $this->prophesize(aaa::class);
$mockA->toArray()->willReturn([xxxx]);

$mockB = $this->prophesize(bbb::class);
$mockB->register($pk, $mockA->reveal())->shouldBeCalledOnce();

Mockery の場合は以下みたいな感じでした。

$collection = collect([xxxx]);
$mockA = \Mockery::mock(aaa::class)->makePartial();
$mockA->shouldReceive('register')
        ->with(\Mockery::on(function ($actual) use ($collection) {
            $this->assertEquals($actual, $collection);
            return true;
        }))->once();

症状的には こちら の記事と同じだと思います。

prophecy をずっとやってるからなのかもしれませんが、 ちょっと使いづらいと感じてしまいました...

また、こちら の記事では \Hamcrest\Matchers を使う方法も紹介されています。

$collection = collect([xxxx]);
$mockA = \Mockery::mock(aaa::class)->makePartial();
$mockA->shouldReceive('register')
        ->with((\Hamcrest\Matchers::equalTo($collection)))->once();

後者のほうがスッキリしていいですね!

終わりに

ちょっと前に短期案件で久々にテストコードがないプロジェクトにアサインしたのですが、テストコード書きなれると、テストコードがないプロダクトコード触るのが怖すぎて、開発スピードが明らかに落ちてました。

t_wadaさんのスライドにあった

テストを書く時間がないのではなく、テストを書かないから時間がなくなるのです。

っていうのがすごく身にしみました。

speakerdeck.com

現場からは以上です。

circlec ciを回す際に composer install でエラー出た

はじめに

最近、昔から運用されているphpプロジェクトのお手伝いをさせてもらうことになり、いろいろな環境の再構築なども行っております。

その過程でcircleciを使用するということになり早速導入したのですが、妙なエラーが出ました。

めっちゃ簡単ですが今回はそちらの対応の備忘録です。

本題

原因はフレームワークが古く、 composer2 を使うことで起きたエラーだと理解しました。

blog.packagist.com

Invalid PSR-0 / PSR-4 configurations will not autoload anymore in optimized-autoloader mode, as per the warnings introduced in Composer 1.10. Mostly these warnings were for classes that were not meant to autoload anyway so I don't expect major issues, but it's safer to clean these up before upgrading.

警告は出ていたのですがローカルで問題なかったので、無視しておりました...

とりあえずバージョン下げてみたらうまくいくのかな?と思って

      - run:
          name: Composer Downgrade
          command: |
            sudo composer self-update --1

を使うと問題なく対応できましたw

終わりに

過去と自分にメッセージ

警告無視すんな

laravel8のFactoryを使用したテストデータ作成

はじめに

こんばんは。

夜中子供が寝ているときに粛々と仕事と勉強をしております。

表題通りlaravel8でテストデータを作ったのですが、昔やってたlaravelのバージョンとは作り方違ってたので、備忘録として残しておきます。

本番

1. コンフィグの修正

config/app.php を修正して fakerで日本語を使用できるようにします。

'faker_locale' => 'ja_JP',

1. Factory作成

makeコマンド実行

$ php artisan make:factory UserProfileFactory
Factory created successfully.

できたFactoryクラスを以下みたいに修正

<?php

namespace Database\Factories;

use App\Model\UserProfile;
use Illuminate\Database\Eloquent\Factories\Factory;

class UserProfileFactory extends Factory
{
    protected $model = UserProfile::class; // 対象となるModelクラスを指定

    public function definition()
    {
        // return値の値を作成(fakerを使ってダミーデータを作成)
        return [
            'name'        => $this->faker->name,
            'postal_code' => $this->faker->postcode(),
            'prefecture'  => $this->faker->prefecture(),
            'address'     => $this->faker->city() . $this->faker->streetName() . $this->faker-> streetAddress() 
        ];
    }
}

2. Modelに必要なtraitをuseする

makeコマンド実行

php artisan make:model UserProfile
Model created successfully.

作成されたmodelには HasFactory traitがuseされていると思いますが、makeコマンド介さずにmodelクラスを作った場合は以下のように HasFactory traitをuseします。

<?php

namespace App\Model;

use Database\Factories\UserProfileFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;

class UserProfile extends Model
{
    use HasFactory; // ← 対象のtrait

    protected static function newFactory()
    {
        return UserProfileFactory::new(); // ← 対象のFactoryを指定する
    }
}

3. SeederでFactoryを指定してテストデータを作成する

makeコマンド実行

$ php artisan make:seeder UserProfileSeeder
Seeder created successfully.

できたSeederファイルでFactoryを使用してデータを作成

<?php

namespace Database\Seeders;

use App\Model\UserProfile;
use Illuminate\Database\Seeder;

class UserProfileSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        UserProfile::factory(100)->create(); // これで100件のテストデータが作成できる

        // テストデータの一部 or 全てを上書きしたい場合
        UserProfile::factory()->create([
            'name' => 'あいうえお' // nameだけテストデータではなく 「あいうえお」 にする
        ]);
    }
}

番外編 factoryで使用するfakerに新たなメソッドを生やす

作成したfactoryクラスの親クラスである Illuminate\Database\Eloquent\Factories\Factory に存在している withFaker() をオーバライドして以下のように新たにproviderを作ります。

    protected function withFaker()
    {
        $faker = parent::withFaker();
        $faker->addProvider(new class($faker) extends \Faker\Provider\Base {
            public function ngWord(): string
            {
                return $this->generator->randomElement([
                    'バカ',
                    'アホ',
                    'マヌケ'
                ]);
            }
        });

        return $faker;
    }

これで $this->faker->ngWord() が使えるようになります。

baseクラスを新たに作って生やしてもいいし、 HasFactory trait側のメソッドをオーバライドして対応とか、色々やり方はありそうです。

終わりに

久々にlaravel使っているんですが、テストデータとか作りやすいですね。

現場からは以上です。

freeeでの電子帳簿保存法の対応(はじめの一歩)

こんばんは。

そろそろめんどくさい確定申告の時期が近づいてきました。

みなさん頑張ってますでしょうか?

そして今年から 電子帳簿保存法 がはじまりますよね。

今までPDFダウンロードしていつでも紙に出来るように持ってたんですが、そもそも電子領収書は電子で持っとく?みたいな感じですよね?

ちがうのかな?

パット見読んでもよくわかりません。

僕は今freee使ってますが、freeeでも対応されているそうです。

とりあえずなんか設定必要なのかな?

と思って、freeeのヘルプ見てみました。

https://support.freee.co.jp/hc/ja/articles/4410254921497-%E9%9B%BB%E5%AD%90%E5%B8%B3%E7%B0%BF%E4%BF%9D%E5%AD%98%E6%B3%95%E3%81%AE%E6%A6%82%E8%A6%81-%E6%89%8B%E7%B6%9A%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6

なるほどわからん。というか長すぎる。

とりあえず ファイルボックスの電子帳簿保存機能 というのを使うようです。

調べて使えるようにしてみました。

f:id:kojirooooocks:20220206024137p:plain

f:id:kojirooooocks:20220206024140p:plain

f:id:kojirooooocks:20220206024143p:plain

f:id:kojirooooocks:20220206024147p:plain

これでファイルボックスが使えるようになりました。

そこに請求書をアップします。

アップしただけでは請求書は未確認の状態になるので、確認をして、その後登録している取引とアップした請求書紐付けます。

f:id:kojirooooocks:20220206025105p:plain

いったんこれでよいのかな??

タイムスタンプとかあるけど大丈夫なのかな...?

もうちょい調べますが、一旦今日はここまで。

あー確定申告めんどくさー

phpの日付系クラスのよく使うやつの比較表

はじめに

こんばんは。

1月は体調不良のツケでめっちゃ忙しく、2月は契約の切れ目で更に忙しく、ヘロヘロの僕です。

運悪く3〜4件くらいの案件がかぶってしまって、てんやわんやしております。

どれもPHP案件ですが、日付を扱うときに datetime だったり carbon だったり chronos だったりを使っていてこんがらがってきます。

carbonchronos はそんな変わんないんですけど、、、

というわけでよく使うやつを備忘録として少しだけまとめてました。

本題

format

DateTime Carbon Chronos
format('Y-m-d') format('Y-m-d') format('Y-m-d')

一月前

DateTime Carbon Chronos
subMonthNoOverflowByDateTime(自作関数) subMonthNoOverflow() subMonth()
 function subMonthNoOverflowByDateTime(\DateTimeImmutable $dateTimeImmutable) {
    $day        = (int)$dateTimeImmutable->format('j');
    $targetDate = $dateTimeImmutable->modify("-1 month");
    if ($day !== (int)$targetDate->format('j')) {
        $targetDate = $targetDate->modify('last day of last month');
    }
    return $targetDate;
}

一月後

DateTime Carbon Chronos
addMonthNoOverflowByDateTime(自作関数) addMonthNoOverflow() addMonth()
function addMonthNoOverflowByDateTime(\DateTimeImmutable $dateTimeImmutable) {
    $day        = (int)$dateTimeImmutable->format('j');
    $targetDate = $dateTimeImmutable->modify("+1 month");
    if ($day !== (int)$targetDate->format('j')) {
        $targetDate = $targetDate->modify('last day of last month');
    }
    return $targetDate;
}

先月頭

DateTime Carbon Chronos
modify('first day of last month') subMonthNoOverflow()->firstOfMonth() subMonth()->firstOfMonth()

来月頭

DateTime Carbon Chronos
modify('first day of next month') addMonthNoOverflow()->firstOfMonth() addMonth()->firstOfMonth()

先月末

DateTime Carbon Chronos
modify('last day of last month') subMonthNoOverflow()->lastOfMonth() subMonth()->lastOfMonth()

来月末

DateTime Carbon Chronos
modify('last day of next month') addMonthNoOverflow()->lastOfMonth() addMonth()->lastOfMonth()

同じかどうかの比較

DateTime Carbon Chronos
$date === $date eq() eq()

違うかどうかの比較

DateTime Carbon Chronos
$date !== $date ne() ne()

比較(>=)

DateTime Carbon Chronos
$date >= $date gte() gte()

比較(<=)

DateTime Carbon Chronos
$date <= $date lte() lte()

土日かどうか

DateTime Carbon Chronos
in_array((int)$dateByWeekEnd->format('w'), [0, 6], true) isWeekend() isWeekend()

終わりに

オーバーフローするかどうかで処理は変わりますが、今ん所オーバーフローする処理しか書いたことないのでこれで備忘録とっておきます。

来月も干からびるほどに忙しいけど、がんばります...