October, 25 2019

Mocking And Testing PHP Traits In A Laravel Application

Trait is the fastest way to create a reusable code. A Trait is intended to reduce some limitations of single inheritance. This means that you can use multiple traits in a PHP class.

The difference between a PHP Trait and a PHP class is that you cannot instantiate a Trait on its own.

Here is an example of a PHP trait.

<?php
// file upload trait
trait UploadFile{
    function upload($path) {
        // performs file upload and return the new file path
    }
}

// file upload controller
class UploadController{
    use UploadFile;

    function store($request){
        $this->upload($request->file);
    }
}

Because we can't instantiate a PHP Trait, testing it can be a little challenging. I will be showing you how you can mock a PHP Trait and test it. We are going to build a web app for managing users via API. The focus will be on how we can use and test PHP Traits in this application.

Let's start by creating a new Laravel application.

composer create-project --prefer-dist laravel/laravel UserApi

Create a database and update database credentials inside your .env file.

We are going to make use of Spatie fractal wrapper to transform our API data, so let's add it to our project.

composer require spatie/laravel-fractal

This project will only do 3 things: create user, list users and show a single user. We have only three API routes to deal with so let's add it to our routes/api.php file.

Route::group(['prefix' => 'v1'], function () {
    Route::get('users', 'UserController@index');
    Route::get('users/{id}', 'UserController@show');
    Route::post('users', 'UserController@store');
});

Create a user transformer.

php artisan make:transformer UserTransformer

Our user transformer is going to be pretty simple, just returning user name, email, date of creation and modification.

<?php
namespace App\Transformers;

use App\User;
use League\Fractal\TransformerAbstract;

class UserTransformer extends TransformerAbstract
{
    public function transform(User $user)
    {
        return [
            'name' => $user->name,
            'email' => $user->email,
            'created_at' => $user->created_at->format('d-m-Y'),
            'updated_at' => $user->updated_at->format('d-m-Y'),
        ];
    }
}

Next is to create our user controller.

php artisan make:controller UserController --api

This command will scaffold the basic Laravel controller methods we will need. Our controller methods are all going to return JSON responses.

We are going to create a custom response trait inside the app\Traits directory. This will take care of formatting our response status and data before its returned to the client using our API.

Note: The app\Traits directory does not come with Laravel by default, so you have to create it yourself.

<?php
namespace App\Traits;

trait CustomResponse
{
    /**
     * @param array $data
     * @param string $key
     * @param int $status
     * @param string $message
     * @return \Illuminate\Http\JsonResponse
     */
    public function customData(
        array $data,
        $key,
        $status = 200,
        $message = "success"
    ) {
        $response = [
            "data" => [
                "status" => $this->setStatus($status, $message),
                "$key" => $data['data'],
            ]
        ];

        return response()->json($response, $status);
    }

    /** 
     * @param string $message
     * @return \Illuminate\Http\JsonResponse
     */
    public function notFound($message = 'Resource not found')
    {
        $response = $this->formatStatusData(404, $message);
        return response()->json($response, 404);
    }

    /** 
     * @param int $status
     * @param string $message
     * @return \Illuminate\Http\JsonResponse
     */
    private function formatStatusData($status, $message): array
    {
        return [
            'code' => $status,
            'message' => $message,
        ];
    }
}

Currently we have only two public methods, the notFound and the customData method. I believe the methods are self explanatory. They format the data and return a JSON data.

Next we are going to update our UserController class to perform the basic CRUD operations that I mentioned earlier.

<?php
namespace App\Http\Controllers;

use App\Traits\CustomResponse;
use App\Transformers\UserTransformer;
use App\User;
use Illuminate\Http\Request;

class UserController extends Controller
{
    use CustomResponse;

    /**
     * Display a listing of the resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function index()
    {
        $users = User::all();

        $data = fractal($users, new UserTransformer())->toArray();

        return $this->customData($data, 'users');
    }

    /**
     * Store a newly created resource in storage.
     *
     * @return \Illuminate\Http\Response
     */
    public function store()
    {
        //validate request 
        $data = request()->validate([
            'name' => 'required|string',
            'email' => 'required|email',
            'password' => 'string|min:8',
        ]);

        $user = User::create($data);

        $data = fractal($user, new UserTransformer())->toArray();

        return $this->customData($data, 'user');
    }

    /**
     * Display the specified resource.
     *
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function show($id)
    {
        if (is_null($user = User::find($id))) {
            return $this->notFound('user not found');
        }

        $data = fractal($user, new UserTransformer())->toArray();

        return $this->customData($data, 'user');
    }
}

So inside our controller we do basically two things; return notFound response whenever we cant find a user or return customData response if we found the user.

Pretty simple right? Cool.

Testing our CustomResponse Trait

It's time to test our custom response trait. We will be testing two basic things, the response status code and a valid JSON string. Let's create our test class.

php artisan make:test CustomResponseTest --unit

Note: Unit tests can be found in the <em>test/Unit</em> directory inside our Laravel project.

As we know traits cant be instantiated but we can mock it to be able to get an instance of it.

Mocking our response trait will be very easy since Laravel ships with mockery. In unit tests, mock objects simulate the behavior of real objects.

We will be using the getMockBuilder method to create an instance of our CustomResponse trait and then on each test method we call the response method we want to test.

<?php

namespace Tests\Unit;

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

class CustomResponseTest extends TestCase
{
    use RefreshDatabase;

    protected $customResponse;

    protected function setUp(): void
    {
        parent::setUp();
        $this->customResponse = $this->getMockBuilder('App\Traits\CustomResponse')->getMockForTrait();
    }

    /**
     *
     * @Test
     */
    public function test_should_return_not_found_response()
    {
        $response = $this->customResponse->notFound();

        $this->assertEquals(404, $response->getStatusCode());

        $this->assertJson($response->getContent());
    }

    /**
     *
     * @Test
     */
    public function test_should_return_custom_data_response()
    {
        $user = factory(User::class)->create();

        $data = fractal($user, new UserTransformer())->toArray();

        $response = $this->customResponse->customData($data, 'user');

        $this->assertEquals(200, $response->getStatusCode());

        $this->assertJson($response->getContent());
    }
}

Let me explain the test_should_return_custom_data_response method. First we have to seed the database with user data and then transform that data to an array so we can pass it to the customData method. Finally we have to assert that it returns 200 status code and a valid JSON response. Same assertions goes for test_should_return_not_found_response method.

You can run the custom response test using this:

vendor/bin/phpunit --filter=CustomResponseTest

Conclusion

Testing makes a confident developer. It's a discipline that I encourage every developer to imbibe. I will be writing more on the few things I have learned about testing.

The source code for this post is on github, feel free to fork it.

You can follow me on twitter, let's continue the discussion over there.

Join my inner circle newsletter

Be the first to hear about anything I publish, launch, or think is helpful for you. Subscribe here

Hey, have you tried Litehost lately ?

Litehost is a web hosting platform for PHP & Laravel developers with Composer, Git, PHP & CLI pre-installed. Try it now