Jozsef Hocza

Lumen TDD: Testing a ToDo App

The Quest

First of all, why TDD? Sometimes manual testing took so much time from me. I found that if I were writing test that would take less them. If I went with TDD it may take the same amount of time as manual testing. But! As the code gets complex it pays off.

Yes. If you just would like to create an App with User login and user can create tasks and nothing more, well it will take more time to finish. But seriously, we never stop there. There is always room for improvement. If there are room for improvement then it will happen sooner or later.

Test. Code. Repeat.

I am not a strict follower of the “always do one step after a failed test.” You know, when you buy a book it tells you to do like this:

Write your test first.

<?php

public function testCreatingTask()
{
    $user = factory('App\User')->create();
    $task_data = [
        'name' => 'My Test Task',
        'description' => 'My Description that is a bit long but not that much.',
        'sub_tasks' => [],
        'date_due' => '2017-01-18',
        'date_remind' => '2017-01-16 10:00:00'
    ];
    $this->post('/api/tasks', $task_data, $this->headers($user))
        ->seeJson(['message' => 'task_created']);

    $task = \App\Models\Task::first();

    $this->assertArraySubset($task_data,$task->toArray());
}

Then run PHPunit and where it fails you implement only that.

1) UserCreatesTaskTest::testCreatingTask
Unable to find JSON fragment ["message":"task_created"] within [{"message":"404 Not Found","status_code":404}].
Failed asserting that false is true.

Now we created the route that points POST /api/tasks to TasksController@store we re-run your test.

1) UserCreatesTaskTest::testCreatingTask
Invalid JSON was returned from the route. Perhaps an exception was thrown?

At this point I really start to feel retarded. I could have already add the to the store function:

<?php
return response()->json(['message' => 'task_created']);

Actually I could have already implement the whole store, because this is not that complex.

<?php
public function store(Request $request)
{
    $user = JWTAuth::parseToken()->authenticate();
    $task = new Task($request->all());
    $task->done = false;
    $task->user_id = $user->id;
    $task->save();

    return response()->json(['message' => 'task_created', 'data' => $task]);
}

So I like to work ahead when the implementation will not be that robust.

What to test?

Test Driven Development was always a bit challenging for me. How should I test a ToDo App? Well I think it is really up to you.

First I would sketch up some scenarios. These will be a draft for a “single user (but multi tenant) todo app” Like an invoicing software. Where you sould not be able to create an invoice in a name of another company. :)

Well later we can implement “Task Lists” which can be shared with other users where they will be able to do whatever they want with the tasks within that list. But do not run ahead.

Test ‘em all!

Now I will assume that you have already set up authentication and if you are working with Lumen you already fixed that annoying bug with JWTAuth. (If not, you can find the fix in the previous post of this series)

I start with Task Creation.

UserCreatesTaskTest.php

<?php

use Laravel\Lumen\Testing\DatabaseMigrations;

class UserCreatesTaskTest extends TestCase
{
    use DatabaseMigrations;

    public function testCreatingTask()
    {
        $user = factory('App\User')->create();
        $task_data = [
            'name' => 'My Test Task',
            'description' => 'My Description that is a bit long but not that much.',
            'sub_tasks' => [],
            'date_due' => '2017-01-18',
            'date_remind' => '2017-01-16 10:00:00'
        ];
        $this->post('/api/tasks', $task_data, $this->headers($user))
            ->seeJson(['message' => 'task_created']);

        $task = \App\Models\Task::first();

        $this->assertArraySubset($task_data,$task->toArray());
    }

    public function testUserCannotCreateTaskForOtherUser()
    {
        $user = factory('App\User')->create();
        $user2 = factory('App\User')->create();
        $task_data = [
            'name' => 'My Test Task',
            'user_id' => $user2->id
        ];
        $this->post('/api/tasks', $task_data, $this->headers($user))
            ->seeJson(['message' => 'task_created']);

        $task = \App\Models\Task::first();

        $this->assertNotEquals($user2->id,$task->user_id);
        $this->assertEquals($user->id,$task->user_id);
    }

}

Note: I have already appended my TestCase with protected function headers as it was done in the previous post:

<?php
protected function headers($user = null)
{
    $headers = ['Accept' => 'application/json'];

    if (!is_null($user)) {
        $token = \Tymon\JWTAuth\Facades\JWTAuth::fromUser($user);
        $headers['Authorization'] = 'Bearer '.$token;
    }

    return $headers;
}

So at this point I would create the database table in advance, since I do not want to run phpunit until I have all the fields… :)

migrations/2017_01_14_213120_create_tasks_table.php

<?php
Schema::create('tasks', function (Blueprint $table) {
    $table->increments('id');
    $table->string('name');
    $table->text('description')->nullable();
    $table->text('sub_tasks')->nullable();
    $table->date('date_due')->nullable();
    $table->timestamp('date_remind')->nullable();
    $table->integer('user_id')->unsigned();
    $table->integer('assigned_to')->unsigned()->nullable();
    $table->boolean('done')->default(false);
    $table->timestamps();
});

And since I like planning ahead I know which fields will be fillable by mass assignment, so I add the following to Models/Task.php

<?php
protected $fillable = [
    'name',
    'description',
    'sub_tasks',
    'date_due',
    'date_remind',
    'done',
    'assigned_to'
];

Since I do not want them to ever change the user_id nor the created_at and updated_at fields.

And before running a single phpunit, I would also prepare the store() function.

<?php
public function store(Request $request)
{
    $user = JWTAuth::parseToken()->authenticate();
    $task = new Task($request->all());
    $task->done = false;
    $task->user_id = $user->id;
    $task->save();

    return response()->json(['message' => 'task_created', 'data' => $task]);
}

Now I would run the phpunit and it should fail at this point, because I forgot something important. :)

1) UserCreatesTaskTest::testCreatingTask
Unable to find JSON fragment ["message":"task_created"] within [{"message":"Array to string conversion (SQL: insert into \"tasks\" (\"name\", \"description\", \"sub_tasks\", \"date_due\", \"date_remind\", \"done\", \"user_id\", \"updated_at\", \"created_at\") values (My Test Task, My Description that is a bit long but not that much., , 2017-01-18, 2017-01-16 10:00:00, 0, 1, 2017-01-15 09:49:00, 2017-01-15 09:49:00))","status_code":500}].
Failed asserting that false is true.

Okay. Well the plan is to store the sub_tasks as a serialized array. Like:
["name" => "task's name", "done" => false]

But it whines about array to string conversion. Well then let’s cast it within our Eloquent model. Let’s add this to Models/Task.php

protected $casts = [
    'sub_tasks' => 'array',
    'done' => 'boolean'
];

I added the done while it was not necessary, but I would like it to be true or false instead of 1 or 0 when I return a task in the future. Now the test is OK.

So this is how I work TDD-ish. It is a bit accelerated than the traditional TDD. However when the code get more robust I am tending to get traditional again. But at the ‘kick-start’ I think it is just a time/money sink.

I am not going to go thru Update/Delete in details but I provide my Tests for you.

UserUpdatesTaskTest.php

<?php

use Laravel\Lumen\Testing\DatabaseMigrations;

class UserUpdatesTaskTest extends TestCase
{
    use DatabaseMigrations;

    public function testUpdatingTask()
    {
        $user = factory('App\User')->create();
        $task = $user->tasks()->create(['name' => 'Update This Task']);
        $update_data = [
            'name' => 'Updated!',
            'date_due' => '2017-01-01'
        ];
        $this->patch('/api/tasks/'.$task->id, $update_data, $this->headers($user))
            ->seeJson(['message' => 'task_updated']);

        $task = \App\Models\Task::first();

        $this->assertArraySubset($update_data,$task->toArray());
    }

    public function testUserCannotUpdateOtherUserTask()
    {
        $user = factory('App\User')->create();
        $user2 = factory('App\User')->create();
        $task = $user2->tasks()->create(['name' => 'Update This Task']);
        $update_data = [
            'name' => 'Updated!',
            'date_due' => '2017-01-01'
        ];
        $this->patch('/api/tasks/'.$task->id, $update_data, $this->headers($user))
            ->assertResponseStatus(403);
    }

}

UserDeletesTaskTest.php

<?php

use Laravel\Lumen\Testing\DatabaseMigrations;

class UserDeletesTaskTest extends TestCase
{
    use DatabaseMigrations;

    public function testDeletingTask()
    {
        $user = factory('App\User')->create();
        $task = $user->tasks()->create(['name' => 'Update This Task']);
        $this->delete('/api/tasks/'.$task->id, [], $this->headers($user))
            ->seeJson(['message' => 'task_deleted']);

        $task = \App\Models\Task::find($task->id);

        $this->assertEmpty($task);
    }

    public function testUserCannotDeleteOtherUserTask()
    {
        $user = factory('App\User')->create();
        $user2 = factory('App\User')->create();
        $task = $user2->tasks()->create(['name' => 'Update This Task']);
        $this->delete('/api/tasks/'.$task->id, [], $this->headers($user))
            ->assertResponseStatus(403);
    }

}

UserViewTasksTest.php

Okay here in my project I only allow the user to view his posts. Actually the index() only returning his.

<?php
public function index()
{
    $user = JWTAuth::parseToken()->authenticate();
    $tasks = $user->tasks;

    return $tasks->toJson();
}

But even in this case it is a must to test against it. Because what happens if for some reason you change the code and expose other users’ tasks?

<?php

use Laravel\Lumen\Testing\DatabaseMigrations;

class UserViewTasksTest extends TestCase
{
    use DatabaseMigrations;

    public function testGettingTasksIndex()
    {
        $user = factory('App\User')->create();
        $tasks_data = [
            ['name' => 'First Task'],
            ['name' => 'Second Task'],
            ['name' => 'Third Task'],
        ];
        foreach($tasks_data as $task)
            $user->tasks()->create($task);
        $this->get('/api/tasks', $this->headers($user));

        $response = $this->response->getContent();

        $tasks = $user->tasks;

        $this->assertEquals($tasks->toJson(),$response);
    }

    public function testUserGettingOnlyHisTasksIndex()
    {
        $user = factory('App\User')->create();
        $user2 = factory('App\User')->create();
        $tasks_data = [
            ['name' => 'First Task'],
            ['name' => 'Second Task'],
            ['name' => 'Third Task'],
        ];
        $other_tasks_data = [
            ['name' => 'Build: Death Star'],
            ['name' => 'Build: Second Death Star'],
            ['name' => 'Build: Starkiller Base'], // those engineers really sucked at their work
        ];
        foreach($tasks_data as $task)
            $user->tasks()->create($task);
        foreach($other_tasks_data as $task)
            $user2->tasks()->create($task);

        $this->get('/api/tasks', $this->headers($user));

        $response = $this->response->getContent();

        $tasks = $user->tasks;

        $this->assertEquals($tasks->toJson(),$response);
    }

}

So even if something feels trivial that “Well It is not going to happen.” you should prepare a test that ensures that.

You may noticed that there is no
GET /api/tasks/{id}
Well in this phase this App will be getting all the tasks so there is no point (at this moment) in implementing the show() function. However If you noticed that it is great! :) We surely need to implement something that would warn us if someone implements that function.

So I quickly added a test:

<?php
public function testShowIsNotYetImplemented()
{
    $user = factory('App\User')->create();
    $this->get('/api/tasks/1', $this->headers($user))
        ->assertResponseStatus(501);
}

It will fail because the returned HTTP code is 200. Let’s resolve it:

<?php
public function show($id)
{
    return $this->response->error('Not Implemented',501);
}

This reponse error is a Dingo API helper.

This is it for now

Testing can be fun.


Share this:

SUBSCRIBE
Subscribe to my e-mail list.
You can unsubscribe anytime.