Unit testing your API in Laravel

So let’s say you’ve build an API, but want to take it a step further by testing it. Well, Laravel makes it very easy.

At first, let’s set-up a new Laravel enviroment. I’m using Laravel’s 5.6 version for this article.

After laravel is installed make sure phpunit is installed on your machine. You can install it here: https://phpunit.de/getting-started/phpunit-7.html.

After installing phpunit, enter the following command in your terminal pointing at your laravel’s project root directory.

phpunit

If every is set-up properly you should get something like:

PHPUnit 7.3.0 by Sebastian Bergmann and contributors… 2 / 2 (100%)Time: 1.12 seconds, Memory: 12.00MBOK (2 tests, 2 assertions)

Now head over to /.phpunit.xml and override the database connection and disable the APP_DEBUG configuration of the .env file. So phpunit will use sqlite. Add the following in the <php> section:

<env name="DB_CONNECTION" value="sqlite"/>
<env name="APP_DEBUG" value="false"/>

So it should look like this:

...
<php>
<env name="APP_ENV" value="testing"/>
<env name="BCRYPT_ROUNDS" value="4"/>
<env name="CACHE_DRIVER" value="array"/>
<env name="SESSION_DRIVER" value="array"/>
<env name="QUEUE_DRIVER" value="sync"/>
<env name="MAIL_DRIVER" value="array"/>
<env name="DB_CONNECTION" value="sqlite"/>
<env name="APP_DEBUG" value="false"/>
</php>
...

Let’s create a new Model called Post with:

php artisan make:model Post -c -m -f

Adding -c, -m, -f after the model name will also generate a controller, migration and factory file. Head over to the migration file and add a few lines at the up() method:

public function up()
{
    Schema::create('posts', function (Blueprint $table) {
        $table->increments('id');
        $table->string('title');
        $table->text('content');
        $table->timestamps();
    });
}

Then in /database/factories/PostFactory.php add a few lines:

<?php
use Faker\Generator as Faker;
$factory->define(App\Post::class, function (Faker $faker) {
    return [
        'title' => $faker->sentence,
        'content' => $faker->paragraph
    ];
});

When the Post factory is set-up go to the Post model /app/Post.php and add protected fillable to be able to use the create method:

<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
    protected $fillable = [
        'title',
        'content'
    ];
}

In /app/Http/Controllers/PostController.php and add some basic crud methods for testing purpose:

<?php
namespace App\Http\Controllers;
use App\Post;
use Illuminate\Http\Request;
class PostController extends Controller
{
    public function index() {
        return Post::all();
    }
    
    public function show(Post $post) {
        return $post;
    }
    public function store(Request $request) {
        $post = Post::create($request->all());
        return response()->json($post, 201);
    }
    public function update(Request $request, Post $post) {
        $post->update($request->all());
        return response()->json($post);
    }
    public function delete(Post $post) {
        $post->delete();
        return response()->json(null, 204);
    }
}
  • Index (Returns all posts)
  • Show (Returns a single posted based on the id in the parameter)
  • Store (Returns the created post with a 201 status code, meaning: ‘Content created’)
  • Update (Returns the updated post with the default 200 status code)
  • Delete (Returns null with a 204 status code, meaning: ‘No content’)

Read more about status codes: https://httpstatuses.com/

In /routes/api.php add some routes to use our controller:

Route::group(['prefix' => 'posts'], function() {
    Route::get('/', '[email protected]')->name('posts');
    Route::get('/{post}', '[email protected]')->name('posts.show');
    Route::post('/', '[email protected]')->name('posts.store');
    Route::put('/{post}', '[email protected]')->name('posts.update');
    Route::delete('/{post}', '[email protected]')->name('posts.delete');
});
  • Be aware that there is no authentication on any of the routes currently.
  • All routes in /api.php will be prefixed with /api
  • In the group I prefixed all underlaying routes with /posts. So for instance to request the first route, you would call: /api/posts

Then run the migration file using:

php artisan migrate

Laravel ships with two test classes located in /tests/Feature and /tests/Unit . We’ll start by editing /tests/TestCase.php to:

<?php
namespace Tests;
use Faker\Factory;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
abstract class TestCase extends BaseTestCase
{
    use CreatesApplication, DatabaseMigrations;
    protected $faker;
    public function setUp() {
        parent::setUp();
        $this->faker = Factory::create();
    }
}

Use the following command to generate a new unit test file:

php artisan make:test PostTest --unit

Open the generated file /tests/Unit/PostTest.php and add the testing methods:

<?php
namespace Tests\Unit;
use App\Post;
use Tests\TestCase;
class PostTest extends TestCase
{
    public function test_can_create_post() {
        $data = [
            'title' => $this->faker->sentence,
            'content' => $this->faker->paragraph,
        ];
        $this->post(route('posts.store'), $data)
            ->assertStatus(201)
            ->assertJson($data);
    }
    public function test_can_update_post() {
        $post = factory(Post::class)->create();
        $data = [
            'title' => $this->faker->sentence,
            'content' => $this->faker->paragraph
        ];
        $this->put(route('posts.update', $post->id), $data)
            ->assertStatus(200)
            ->assertJson($data);
    }
    public function test_can_show_post() {
        $post = factory(Post::class)->create();
        $this->get(route('posts.show', $post->id))
            ->assertStatus(200);
    }
    public function test_can_delete_post() {
        $post = factory(Post::class)->create();
        $this->delete(route('posts.delete', $post->id))
            ->assertStatus(204);
    }
    public function test_can_list_posts() {
        $posts = factory(Post::class, 2)->create()->map(function ($post) {
            return $post->only(['id', 'title', 'content']);
        });
        $this->get(route('posts'))
            ->assertStatus(200)
            ->assertJson($posts->toArray())
            ->assertJsonStructure([
                '*' => [ 'id', 'title', 'content' ],
            ]);
    }
}
  • Every test has his own method prefixed with test, this is required for phpunit to find and use the test method.
  • We’re checking if the expected status codes are returned with assertStatus
  • With assertJson we’re testing if the JSON result matches our expectation.
  • In test_can_list_posts we’re removing all attributes of each post created so we’ll only have ‘id’, ‘title’ and ‘content’. And then we check that all items in the returned JSON array by the API contain ‘id’, ‘title’, ‘content’ by using assertJsonStructure .

To test, let’s run phpunit in the terminal:

phpunit

That’s it! It should pass all tests. Feel free to contact me if you have any questions or improvements, I’m happy to help.

Refer: https://medium.com/@mscherrenberg/unit-testing-your-api-in-laravel-5-6-7172bcdc593d