Testing Laravel: Mendapatkan Passed Test

Bismillahirrahmanirrahim

Pada artikel terdahulu, kita sudah membahas dan memahami konsep penerapan Test-Driven Development, 5 tahap yang kita kerjakan :

  1. Buat Kode Pengujian (Add Test).
  2. Jalankan test, hasilnya pasti fail (Watch Test Fail).
  3. Tulis Kode Fitur Sistem (Write Code).
  4. Jalankan test (Run Tests) hingga testing passed.
  5. Refactor Kode Fitur Sistem (Refactor).

Empat Langkah Perulangan

Berdasarkan konsep itu, pada artikel sebelumnya kita mengerjakan tahap pertama, yaitu membuat Kode Pengujian (Failing Test). Kali ini kita akan mengerjakan tahap ke 2 dan ke 3, dengan melakukan 4 langkah berikut secara berulang-ulang :

  1. Jalankan vendor/bin/phpunit
  2. Error tampil
  3. Temukan Penyebab
  4. Tentukan Solusi

Keempat langkah ini dilakukan setiap kita menemukan error baru yang ditampilkan oleh PHPUnit. Setiap ekseksusi PHPUnit pada artikel ini kita berikan hashtag (#) angka. Saya menghitung ada 19 proses yang dilakukan dari awal sampai kita mendapatkan "Hijau".

Oke, kita mulai

# 1
$ vendor/bin/phpunit
Hasil: Error

A request to [http://localhost/tasks] failed. Received status code [404].
Pada artikel sebelumnya kita sampai di sini.

Penyebab

Belum ada route untuk melayani request ke URL /tasks.

Solusi

Buat route untuk melayani request ke /tasks pada file routes/web.php. Tambahkan route get ke url tasks dengan controller dan method yang melayani requestnya.

<?php
// routes/web.php

Route::get('/', function () {
    return view('welcome');
});

// Tambahkan route dan actionnya (controller dan method)
Route::get('tasks', 'TasksController@index');



# 2
$ vendor/bin/phpunit
Hasil: Error

A request to [http://localhost/tasks] failed. Received status code [500].

Caused by
ReflectionException: Class App\Http\Controllers\TasksController does not exist
Penyebab

TasksController belum ada.

Solusi
$ php artisan make:controller TasksController



# 3
$ vendor/bin/phpunit
Hasil: Error

A request to [http://localhost/tasks] failed. Received status code [500].

Caused by
BadMethodCallException: Method [index] does not exist.
Penyebab

Pada TasksController belum ada method index.

Solusi

Tambahkan method index pada app/Http/Controllers/TasksController.php.

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class TasksController extends Controller
{
    public function index()
    {

    }
}



# 4
$ vendor/bin/phpunit
Hasil: Error

InvalidArgumentException: Could not find a form that has submit button [Create Task].

Penyebab

Ini karena pada TasksController method index, kita tidak return view, sehingga hanya tampil halaman web kosong.

Solusi

Kita return view pada method index.

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class TasksController extends Controller
{
    public function index()
    {
        return view('tasks.index');
    }
}



# 5
$ vendor/bin/phpunit
Hasil: Error

InvalidArgumentException: View [tasks.index] not found.

Penyebab

File index.blade.php tidak ditemukan pada direktori resources/views/tasks.

Solusi

Kita buat file index.blade.php pada direktori resources/views/tasks.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>Tasks Management</title>
</head>
<body>
    
</body>
</html>



# 6
$ vendor/bin/phpunit
Hasil: Error

InvalidArgumentException: Could not find a form that has submit button [Create Task].

Penyebab

Pada file resources/views/tasks/index.blade.php atau disebut juga view tasks.index tidak ditemukan Tombol Submit Create Task.

Solusi

Kita buat file form dengan tombol submit Create Task pada view tasks.index.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>Tasks Management</title>
</head>
<body>
    <form action="{{ url('tasks') }}" method="post">
        <input type="submit" value="Create Task">
    </form>
</body>
</html>



# 7
$ vendor/bin/phpunit
Hasil: Error

InvalidArgumentException: Unreachable field "name"

Penyebab

Pada view tasks.index tidak ditemukan input text field dengan nama name.

Solusi

Dalam form pada view tasks.index tambahkan input text dengan nama name.

<!-- body -->
    <form action="{{ url('tasks') }}" method="post">
        <input type="text" name="name">
        <input type="submit" value="Create Task">
    </form>
<!-- /body -->



# 8
$ vendor/bin/phpunit
Hasil: Error

InvalidArgumentException: Unreachable field "description"

Penyebab

Pada view tasks.index tidak ditemukan input textarea field dengan nama description.

Solusi

Pada view tasks.index kita buat input textarea dengan nama description.

<!-- body -->
    <form action="{{ url('tasks') }}" method="post">
        <input type="text" name="name">
        <textarea name="description"></textarea>
        <input type="submit" value="Create Task">
    </form>
<!-- /body -->



# 9
$ vendor/bin/phpunit
Hasil: Error

A request to [http://localhost/tasks] failed. Received status code [405].

Caused by
Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException
Penyebab

Pada route sudah ada route tasks yang kita buat, tetapi ini untuk method get, sedangkan saat kita submit form, kita mengirimkan method POST artinnya kita perlu route juga untuk melayani method POST ini.

Solusi

Pada routes/web.php, buat route untuk melayani method POST.

<?php
// routes/web.php

Route::get('/', function () {
    return view('welcome');
});

// Tambahkan route dan actionnya (controller dan method)
Route::get('tasks', 'TasksController@index');
Route::post('tasks', 'TasksController@store');



# 10
$ vendor/bin/phpunit
Hasil: Error

A request to [http://localhost/tasks] failed. Received status code [500].

Caused by
BadMethodCallException: Method [store] does not exist.
Penyebab

Pada TasksController belum ada method store.

Solusi

Kita buat methodnya, buka app/Http/Controllers/TasksController.php. Tambahkan method store

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class TasksController extends Controller
{
    public function index()
    {
        return view('tasks.index');
    }

    public function store()
    {

    }
}



# 11
$ vendor/bin/phpunit
Hasil: Error

PDOException: SQLSTATE[HY000]: General error: 1 no such table: tasks

Penyebab

Belum ada tabel tasks di database.

Solusi

Nah sekarang kita berurusan dengan database, kita buat model Task dengan file migrations untuk tabel tasks.

# Membuat model `Task` beserta file database migrationnya
$ php artisan make:model Task -m



# 12
$ vendor/bin/phpunit
Hasil: Error

PDOException: SQLSTATE[HY000]: General error: 1 no such table: tasks

Penyebab

Kok Errornya masih sama? Yap, PHPUnit belum tahu file migration yang barusan dibuat bersama model Task itu harus diapakan.

Solusi

Solusinya kita gunakan Trait RefreshDatabase pada kelas tests\Feature\ManageTasksTest.php agar setiap PHPUnit jalan, dia melakukan database migration.

<?php

namespace Tests\Feature;

use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;

class ManageTasksTest extends TestCase
{
    use RefreshDatabase; // Tambahkan baris ini

    /** @test */
    public function user_can_create_a_task()
    {
        // ...
    }
}



# 13
$ vendor/bin/phpunit
Hasil: Error

Unable to find row in database table [tasks] that matched attributes [{"name":"My First Task", ...

Penyebab

Belum ada action yang dilakukan pada method store untuk mengisi database dengan data yang telah diinput melalui form.

Solusi

Kita kerjakan kode pembuatan record task yang diproses dari form pada method store.

<?php

namespace App\Http\Controllers;

use App\Task; // Tambahkan ini
use Illuminate\Http\Request;

class TasksController extends Controller
{
    // ... method index

    public function store()
    {
        Task::create(request()->only('name', 'description'));
    }
}

Jangan lupa use App\Task; di atas class.



# 14
$ vendor/bin/phpunit
Hasil: Error

Illuminate\Database\Eloquent\MassAssignmentException: name

Penyebab

MassAssignmentException

Solusi

Kita tambahkan protected property $fillable pada model Task untuk kolom name dan description.

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Task extends Model
{
    protected $fillable = ['name', 'description'];
}



# 15
$ vendor/bin/phpunit
Hasil: Error

PDOException: SQLSTATE[HY000]: General error: 1 table tasks has no column named name

Penyebab

Tidak ditemukan kolom name pada tabel task di database.

Solusi

Edit file xxxxx_create_tasks_table kemudian tambahkan attribute name, description, dan is_done;

<?php

class CreateTasksTable extends Migration
{
    // ...
    public function up()
    {
        Schema::create('tasks', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name');
            $table->string('description');
            $table->boolean('is_done')->default(0);
            $table->timestamps();
        });
    }



# 16
$ vendor/bin/phpunit
Hasil: Error
1) Tests\Feature\ManageTasksTest::user_can_create_a_task
InvalidArgumentException: The current node list is empty.

...
...
/home/nafies/www/lv/riset/laravel-tdd/tests/Feature/ManageTasksTest.php:35

ERRORS!
Tests: 2, Assertions: 7, Errors: 1.
Penyebab

The current node list is empty, artinya PHPUnit tidak menemukan apa-apa di halaman webnya, hanya berupa halaman kosong. Ini karena kita belum memberikan return apa-apa pada method store nya.

Solusi

Kita berikan kode return back(); pada method store, agar Laravel mengembalikan kita ke halaman sebelumnya (index tasks).

<?php
    // ... TasksController

    public function store()
    {
        Task::create(request()->only('name', 'description'));

        return back();
    }



# 17
$ vendor/bin/phpunit
Hasil: Error

Failed asserting that the page contains the HTML [My First Task].

Penyebab

Pada halaman view tasks.index kita belum menampilkan daftar Task yang ada di dalam tabel tasks di database.

Solusi

Kita edit TasksController agar mengambil record task dari database, kemudian kita tampilkan di view tasks.index.

<?php

namespace App\Http\Controllers;

use App\Task; // Tambahkan ini
use Illuminate\Http\Request;

class TasksController extends Controller
{
    public function index()
    {
        $tasks = Task::all();
        return view('tasks.index', compact('tasks'));
    }

    // ... method store
}

Jangan lupa use App\Task; di atas class.

Kemudian pada view tasks.index :

<!-- body -->
    <form action="{{ url('tasks') }}" method="post">
        <input type="text" name="name">
        <textarea name="description"></textarea>
        <input type="submit" value="Create Task">
    </form>
    <h1>Tasks Management</h1>
    <ul>
        @foreach ($tasks as $task)
            <li>
                {{ $task->name }} <br>
                {{ $task->description }}
            </li>
        @endforeach
    </ul>
<!-- /body -->



# 18
$ vendor/bin/phpunit
Hasil: Passed

Failing Test Passed

Yeeyyy… akhirnya setelah perjalanan panjang, kita dapat "Hijau" :) Ini artinya testing kita pada ManageTasksTest method user_can_create_a_task() sudah berhasil.

Mencoba Fitur Input Task pada Browser

Asumsinya, kita belum mencoba untuk membuka aplikasi ini dengan browser. Sekarang kita coba aplikasinya melalui browser, kira-kira seperti apa bentuknya?

$ php artisan serve
Laravel development server started: <http://127.0.0.1:8000>

Buka URL di browser : http://127.0.0.1:8000/tasks.

Open in Browser after Passed Test 01

Kok error, ya? Iya, karena kita belum melakukan konfigurasi database di sisi aplikasi, kita baru setting database di . Silakan teman-teman buat database pada MySQL atau SQLite, kemudian konfigurasikan databasenya pada file .env. Di PC saya konfigurasinya menggunakan SQLite :

...
APP_LOG_LEVEL=debug
APP_URL=http://localhost

DB_CONNECTION=sqlite

BROADCAST_DRIVER=log
CACHE_DRIVER=file
...

Matikan development server (artisan serve) tadi. Kemudian migrasikan database :

$ php artisan migrate
Migrating: 2017_10_14_051259_create_tasks_table
Migrated:  2017_10_14_051259_create_tasks_table

Kemudian

$ php artisan serve
Laravel development server started: <http://127.0.0.1:8000>

Hasil di browser :

Open in Browser after Passed Test 02

Hehe.. masih jelek ya, tidak apa-apa sekarang silakan coba input data task nya. Tapi ternyata belum bisa …

Passed Test Before CSRF Field

Masalahnya adalah hidden field CSRF Token belum ada pada form. Sekarang kita tambahkan {{ csrf_field() }} persis di bawah tag form:

<!-- body -->
    <form action="{{ url('tasks') }}" method="post">
        {{ csrf_field() }}
        <input type="text" name="name">
        <textarea name="description"></textarea>
        <input type="submit" value="Create Task">
    </form>
<!-- /body -->

Jalankan lagi phpunit.

# 19
$ vendor/bin/phpunit

Passed Test After CSRF Field

Sip, no problem, kita masih dapat "Hijau". Kita coba input Task dan Deskripsi lagi, seharusnya sudah bisa berhasil.

Uji Coba Input Record Manual

Oke berhasil input task. Sampai di sini proses TDD untuk pembangunan sub-fitur Input Task sudah selesai. Silakan teman-teman berkreasi untuk menambakan validasi form, atau mempercantik tampilan pada browser.

Kesimpulan

  1. Dalam penerapan TDD, testing memandu kita membangun fitur (atau sub-fitur) dengan cara memperbaiki error yang tampil.
  2. Empat langkah berulang dalam TDD Laravel :
    1. Jalankan vendor/bin/phpunit
    2. Error tampil
    3. Temukan Penyebab
    4. Tentukan Solusi
  3. Jika setiap perubahan yang kita lakukan di kode aplikasi membuat error yang berbeda pada PHPUnit, berarti progress kita ada kemajuan.
  4. Proses TDD hanya berfokus untuk pembangunan fitur sistem, tidak terpengaruh bagaimana tampilan yang muncul pada browser.
  5. Selama perubahan yang dilakukan tidak “merusak” fitur pada kode aplikasi, testing akan tetap menghasilkan "Hijau". Salah satunya telah kita buktikan saat kita menambahkan csrf_field() pada form tadi.

Pada artikel berikutnya insyaallah kita akan mempercantik tampilan view tasks.index, agar ketika melanjutkan proses TDD pada fitur berikutnya, kita sudah memiliki tampilan yang bagus.

Jika teman-teman ada yang mencoba praktek dan menemui kendala atau stuck di salah satu langkah, atau mendapatkan pesan error yang berbeda dengan proses di atas, silakan tuliskan di kolom komentar, kita bahas sama-sama.

Terima kasih atas waktunya.

comments powered by Disqus