Nafies Luthfi

Life will always feel wonderful if we always think positively.

Testing Laravel: Menghapus dengan Form Button

Bismillahirrahmanirrahim.

Kali ini kita akan membuat fitur hapus task. Pada Route Resource Laravel, ada method request http khusus untuk melayani aktifitas penghapusan record, yaitu method “DELETE”. Method “DELETE” ini dapat dilayani oleh aplikasi laravel jika kita mengirimkan request dari submit form, padahal biasanya kita melakukan submit form kan untuk fungsi create dan update ya? Sedangkan fungsi delete atau hapus record ini hanya berupa aksi satu tombol, seperti halnya fungsi link atau anchor tag. Misal tombol “Hapus Item”. Ketika tombol diklik, maka record dihapus dari sistem dan user diberi respons redirect ke halaman tertentu.

Dari yang saya ketahui, ada 2 cara untuk mengirimkan request “delete” :

  1. Membuat form dengan action menuju method delete, yang hanya berisi tombol “Hapus”.
  2. Membuat tag anchor (<a href="#"></a>) yang dapat dikonversi dengan javascript agar berubah menjadi form (seperti ini).

Yang biasa saya terapkan adalah cara pertama, karena lebih familiar dan dapat dijangkau oleh paket browserkit-testing Laravel. Cara yang kedua saya dapatkan dari mempelajari source code sebuah package bernama Ngeblog, dan berdiskusi dengan authornya mas Antoni Putra.

Nah, pada artikel ini kita akan menggunakan cara pertama dengan form. Cara ini biasa saya sebut dengan “Form Button”, karena dia berupa sebuah form yang hanya berisi satu tombol.

Artikel ini adalah lanjutan dari artikel Testing Laravel: Form Edit Task. Jika teman-teman sudah mengikuti project TDD ini sampai artikel tersebut, silakan menggunakan projectnya. Saya mulai dari repo Laravel-TDD pada commit ini.

Baik, kita mulai dengan menjalankan PHPUnit untuk memastikan project kita tidak ada Failed test.

# 1
$ vendor/bin/phpunit

OK (7 tests, 31 assertions).

Membuat Method Test untuk Hapus Task

Kita kembali meng-edit file tests/Feature/ManageTasksTest.php dan mulai mengerjakan method user_can_delete_an_existing_task.

<?php

// ...

class ManageTasksTest extends TestCase
{
    // ... user_can_create_a_task()

    // ... user_can_browse_tasks_index_page()

    // ... user_can_edit_an_existing_task()

    /** @test */
    public function user_can_delete_an_existing_task()
    {
        $this->assertTrue(true);
    }
}

Buat PHP comment line dulu untuk mempermudah kita membuat testing script.

<?php

    /** @test */
    public function user_can_delete_an_existing_task()
    {
        // Generate 1 record task pada table `tasks`.

        // User membuka halaman Daftar Task.

        // User tekan tombol "Hapus Task"

        // Lihat halaman web ter-redirect ke halaman daftar task

        // Record task hilang dari database
    }

Sekarang kita buat testing script-nya.

<?php

    /** @test */
    public function user_can_delete_an_existing_task()
    {
        // Generate 1 record task pada table `tasks`.
        $task = factory(Task::class)->create();

        // User membuka halaman Daftar Task.
        $this->visit('/tasks');

        // User tekan tombol "Hapus Task" (tombol dengan id="edit_task_1")
        // Dimana angka 1 adalah id dari $task
        $this->press('delete_task_'.$task->id);

        // Lihat halaman web ter-redirect ke halaman daftar task
        $this->seePageIs('/tasks');

        // Record task hilang dari database
        $this->dontSeeInDatabase('tasks', [
            'id' => $task->id,
        ]);
    }

Simpan, kemudian jalankan PHPUnit.

# 2
$ vendor/bin/phpunit
Hasil: Error
InvalidArgumentException: Could not find a form that has
submit button [delete_task_1].
Penyebab

Kita belum memiliki form yang memiliki tombol delete_task_1. Di mana delete_task_1 bisa berupa tulisan atau atribut value pada tombol submit, bisa juga berupa atribut id dari tombol submit tersebut.

Solusi (Membuat Form Button)

Pada view tasks.index bagian daftar Task kita buat form dengan tombol delete_task_1.

<!-- resources/views/tasks/index.blade.php -->

<!-- @foreach ($tasks as $task) -->
<li class="list-group-item">
    <form action="{% raw" >}}{{ url('tasks/'.$task->id) }}{% endraw" >}}" method="post">
        <input type="submit" value="X" id="delete_task_{% raw" >}}{{ $task->id }}{% endraw" >}}" class="btn btn-xs">
    </form>
    <a
        href="{% raw" >}}{{ url('tasks') }}{% endraw" >}}?action=edit&id={% raw" >}}{{ $task->id }}{% endraw" >}}"
        id="edit_task_{% raw" >}}{{ $task->id }}{% endraw" >}}"
        class="pull-right">
        edit
    </a>
    {% raw" >}}{{ $task->name }}{% endraw" >}} <br>
    {% raw" >}}{{ $task->description }}{% endraw" >}}
</li>
<!-- @endforeach -->

Simpan, kemudian jalankan PHPUnit.

# 3
$ vendor/bin/phpunit
Hasil: Error
A request to [http://localhost/tasks/1] failed. Received status code [405].
...
Caused by
Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException
Penyebab

Request yang dikirimkan berupa “POST” ke tasks/1, seharusnya kita mengirim request berupa “DELETE”.

Solusi

Pada form berisi tombol delete di view tasks.index kita tambahkan {% raw" >}}{{ csrf_field() }}{% endraw" >}} dan {% raw" >}}{{ method_field('delete') }}{% endraw" >}}.

<!-- resources/views/tasks/index.blade.php -->

<!-- @foreach ($tasks as $task) -->
<li class="list-group-item">
    <form action="{% raw" >}}{{ url('tasks/'.$task->id) }}{% endraw" >}}" method="post">
        {% raw" >}}{{ csrf_field() }}{% endraw" >}}
        {% raw" >}}{{ method_field('delete') }}{% endraw" >}}
        <input type="submit" value="X" id="delete_task_{% raw" >}}{{ $task->id }}{% endraw" >}}" class="btn btn-xs">
    </form>
    <a
        href="{% raw" >}}{{ url('tasks') }}{% endraw" >}}?action=edit&id={% raw" >}}{{ $task->id }}{% endraw" >}}"
        id="edit_task_{% raw" >}}{{ $task->id }}{% endraw" >}}"
        class="pull-right">
        edit
    </a>
    {% raw" >}}{{ $task->name }}{% endraw" >}} <br>
    {% raw" >}}{{ $task->description }}{% endraw" >}}
</li>
<!-- @endforeach -->

Sebelum jalan PHPUnit, kita tambahkan dulu route untuk melayani requestnya di file routes/web.php.

<?php
// routes/web.php

// ..

Route::get('/tasks', 'TasksController@index');
Route::post('/tasks', 'TasksController@store');
Route::patch('/tasks/{task}', 'TasksController@update');
Route::delete('/tasks/{task}', 'TasksController@destroy');

// ..

Jalankan PHPUnit

# 4
$ vendor/bin/phpunit
Hasil: Error
BadMethodCallException: Method [destroy] does not exist.
Penyebab

Belum ada method destroy di TasksController.

Solusi

Kita buat method TasksController@destroy sekaligus dengan script action untuk menghapus recordnya.

<?php

// TasksController

    // ... method update

    public function destroy(Task $task)
    {
        $task->delete();

        return redirect('/tasks');
    }

Jalankan PHPUnit

# 5
$ vendor/bin/phpunit

Passed Delete Task

Yayy.. hijau. Sekarang lebih cepet ya dapat hijau? Iya, begitulah TDD. Semakin sering metode ini digunakan, maka semakin cepat kita menyelesaikan pekerjaan.

Baik sekarang kita coba buka browser untuk melihat hasilnya.

Posisi Delete Task

Posisi tombol hapusnya kurang pas ya, kita perbaiki sedikit posisi tampilannya. Kira-kira begini.

<!-- resources/views/tasks/index.blade.php -->

<!-- @foreach ($tasks as $task) -->
<li class="list-group-item">
    <form action="{% raw" >}}{{ url('tasks/'.$task->id) }}{% endraw" >}}" method="post" class="pull-right"
        onsubmit="return confirm('Are U sure to delete this task?')">
        {% raw" >}}{{ csrf_field() }}{% endraw" >}}
        {% raw" >}}{{ method_field('delete') }}{% endraw" >}}
        <input type="submit" value="X" id="delete_task_{% raw" >}}{{ $task->id }}{% endraw" >}}" class="btn btn-link btn-xs">
        <a
            href="{% raw" >}}{{ url('tasks') }}{% endraw" >}}?action=edit&id={% raw" >}}{{ $task->id }}{% endraw" >}}"
            id="edit_task_{% raw" >}}{{ $task->id }}{% endraw" >}}">
            edit
        </a>
    </form>
    {% raw" >}}{{ $task->name }}{% endraw" >}} <br>
    {% raw" >}}{{ $task->description }}{% endraw" >}}
</li>
<!-- @endforeach -->

Ada tambahan class bootstrap pada form dan tombol hapus, Link Edit kita masukkan ke dalam form, dan ada tambahan dialog konfirmasi javascript ketika akan menghapus task.

Tombol Delete Task

Baik, sekarang kita jalankan lagi PHPUnit untuk memastikan tidak ada masalah setelah perubahan view barusan.

# 6
$ vendor/bin/phpunit

Seharusnya kita tetap dapat hasil OK (7 tests, 36 assertions).

Tapi ada pertanyaan : Itu kan ada “dialog konfirmasi” di browser pada saat kita menghapus task, kenapa tidak ada perubahan di sisi testingnya untuk mengkonfirmasi dialog itu? Yap, bagian itu (afaik) memang tidak dapat “dijangkau” oleh Browserkit Testing Laravel. Sebagaimana yang sebelumnya pernah kita bahas, Browserkit dibantu oleh DOM Crawler Symfony, hanya dapat mendeteksi elemen html yang di-render pada browser, sehingga tidak bisa berinteraksi dengan javascript.

Jika ingin memiliki paket testing yang lengkap hingga interaksi dengan javascript, teman-teman bisa mencoba Browser Test - Laravel Dusk milik Laravel. Akan tetapi dalam waktu dekat kita tidak akan membahas Laravel Dusk di sini, karena saya sendiri belum familiar dengan paket browser test tersebut. :)

Baik, sampai disini artikel menghapus task dengan Form Button. Jika teman-teman ada yang mempraktekkan dan mengalami permasalahan, silakan disampaikan melalui komentar.

Selanjutnya kita akan membahas tentang Continuous Integration dan setup Continuous Integration dengan Travis CI pada project ini. Teman-teman juga boleh langsung lompat ke artikel toggle status task dengan form button jika tidak belum berminat dengan Continuous Integration.

Terima kasih atas waktunya.