Gain more confidence when testing traits

Janez Cergolj • August 31, 2020

In my one of more recent articles that I wrote on LinkedIn, I presented how to test PHP traits in tow steps. But something was missing. The test assured us that trait is imported into the class. But is the method from the trait called? We don't know, and we don't have any test to back us up.

Let's fix that today and use Mockery to help us out. We'll use it as assurance that method from trait was called.

Let's say we have a test like this:

<?php

namespace Tests\Feature\Http\Controllers;

use App\Http\AcceptedInvitationAuth;
use App\Http\Controllers\AcceptedInvitationController;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\Request;

use Tests\TestCase;

/** @see \App\Http\Controllers\AcceptedInvitationController */
class AcceptedInvitationControllerTest extends TestCase
{
    use RefreshDatabase;

    /** @test */
    public function assert_controller_uses_accepted_invitation_auth_trait()
    {
        $this->assertContains(AcceptedInvitationAuth::class, class_uses(AcceptedInvitationController::class));
    }

    /** @test */
    public function assert_authorize_invitation_is_called()
    {
        $controller = $this->getMockBuilder(AcceptedInvitationController::class)
            ->setMethods(['authorizeInvitation'])
            ->getMock();

        $controller->expects($this->once())
            ->method('authorizeInvitation');

        $controller->create(new Request);
    }
}

You should already be familiar with assert_controller_uses_accepted_invitation_auth_trait test. However today we are interested in assert_authorize_invitation_is_called test. First, we mocked the AcceptedInvitationController's authorizeInvitation method, and then we expect that that method was called.

And here is controller's implementation:

<?php

namespace App\Http\Controllers;

use App\Http\AcceptedInvitationAuth;
use App\Models\User;
use Illuminate\Http\Request;

class AcceptedInvitationController extends Controller
{
    use AcceptedInvitationAuth;

    /**
     * Show the form for creating a new resource.
     *
     * @param \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function create(Request $request)
    {
        $user = User::find($request->user);
        $this->authorizeInvitation($request, $user);

        return view('accepted-invitations.create', ['user' => $user]);
    }
}

Tradeoffs

Such approach gives me enough confidence in my code. However, it isn't bulletproof. Noting guarantees me that everything works as it should. I have only unit tests in place and not a feature test. Adding a feature test for every happy path would improve this. Having said that with this approach, I avoided testing the same code in every class that uses that trait.

In the end, everything is a tradeoff. You gain something and lose something else.

p.s. Code example is from my open-source project.