Composable tests with laravel

Draft Disclaimer: Please note that this article is currently in draft form and may undergo revisions before final publication. The content, including information, opinions, and recommendations, is subject to change and may not represent the final version. We appreciate your understanding and patience as we work to refine and improve the quality of this article. Your feedback is valuable in shaping the final release.

Language Mismatch Disclaimer: Please be aware that the language of this article may not match the language settings of your browser or device.
Do you want to read articles in English instead ?

Composable tests with laravel

tags: laravel

I don't think we can emphasize enough how useful php trait has been since introduced. Today in an attempt to put php trait in the spotlight I would like to share with you a quick usage over my casual coding over the weekend.

TLDR; Trait allowed me to write test cases once and use it n times in other places. No repetitions.

Scenario

  • Needed to log all activities on an app
  • activity log package from spatie
  • of course match my expectation
  • example of how it works
  • installation composer require spatie/activity-log
  • usage
<?php

use Illuminate\Database\Eloquent\Model;
use Spatie\Activitylog\Traits\LogsActivity;

class Region extends Model {

	use LogsActivity;
}
  • that's it
  • now we get to the part I wanted to share
  • HasActivityLog
<?php

namespace Tests\Feature\Models\Concerns;

use Spatie\Activitylog\Models\Activity;

/**
 * Trait HasActivityLog
 * @package Tests\Feature\Models\Concerns
 *
 * @property string $model
 */
trait HasActivityLog {

  public function testLogActivities () {
    $count = Activity::count();

    $model = $this->model::factory()->create();
    $this->assertEquals($count + 1, Activity::count());

    $model->update($this->model::factory()->make()->toArray());
    $this->assertEquals($count + 2, Activity::count());

    $model->delete();
    $this->assertEquals($count + 3, Activity::count());
  }
}

  • Test case
<?php

namespace Tests\Feature\Models;

use Tests\TestCase;
use App\Models\Region;
use Tests\Feature\Models\Concerns\HasActivityLog;
use Illuminate\Foundation\Testing\RefreshDatabase;

class RegionTest extends TestCase
{
    use HasActivityLog;
    use RefreshDatabase;

    protected string $model = Region::class;
}

Test event listeners

Trait

<?php

namespace Tests\Feature\Listeners\Concerns;

use Illuminate\Support\Facades\Event;

/**
 * @property string listener
 * @property string event
 */
trait HasListener
{
    public function testBinding()
    {
        Event::fake();
        Event::assertListening(
            $this->event,
            $this->listener
        );
    }

    protected function eventFactory()
    {
        return new $this->event();
    }
}

Example with airtime listener

Event:

<?php

namespace App\Events\Device;

use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Broadcasting\InteractsWithSockets;

class AirtimeRechargedEvent implements ShouldQueue
{
    use Dispatchable;
    use InteractsWithSockets;
    use SerializesModels;
    use Queueable;

    public function __construct(public int $deviceId, public int $amount, public string $date, public string $duration)
    {
    }

    public function broadcastOn()
    {
        return new PrivateChannel('channel-name');
    }
}

Listener

<?php

namespace App\Listeners;

use App\Models\Device;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use App\Events\Device\AirtimeRechargedEvent;

class CreateAirtimeTransaction implements ShouldQueue
{
    use InteractsWithQueue;

    public function handle(AirtimeRechargedEvent $event)
    {
        $device = Device::where('id', $event->deviceId)->firstOrFail();
        $device->transactions()->create([
            'label' => __('Airtime'),
            'amount' => $event->amount * -1,
            'created_at' => $event->date,
        ]);
    }
}

Test

<?php

namespace Tests\Feature\Listeners\Device;

use Tests\TestCase;
use App\Models\Device;
use App\Models\Transaction;
use App\Listeners\CreateAirtimeTransaction;
use App\Events\Device\AirtimeRechargedEvent;
use Tests\Feature\Listeners\Concerns\HasListener;

class CreateAirtimeTransactionTest extends TestCase
{
    use HasListener;

    protected $listener = CreateAirtimeTransaction::class;

    protected $event = AirtimeRechargedEvent::class;

    public function testCreatesTransaction()
    {
        $device = Device::factory()->create();
        $event = new AirtimeRechargedEvent($device->id, mt_rand(3000, 30000), now(), 30);
        $listener = new CreateAirtimeTransaction();
        $listener->handle($event);
        $this->assertDatabaseHas(Transaction::class, [
            'label' => 'Airtime',
            'created_at' => $event->date,
            'entity_id' => $device->id,
            'entity_type' => $device->getMorphClass(),
            'amount' => $event->amount * -1,
        ]);
    }
}