Nafies Luthfi

Life will always feel wonderful if we always think positively.

Testing Laravel: Refactor Register Feature Test

Bismillahirrahmanirrahim.

Pada artikel sebelumnya kita membuat feature test untuk fitur register, tetapi pada artikel itu, kita lumayan mengulang banyak kode yang sama saat membuat testing validasi form. Sekarang kita akan coba membahas cara melakukan refactor, menerapkan DRY Code (Don’t Repeat Your Code) untuk mengefisienkan penulisan testing.

Pada praktek ini, kita menggunakan repository github Laravel-TDD, yaitu pada commit fd58467. Jika ingin mengikuti sama persis, silakan teman-teman clone repo itu ke localhost.

Baik kita mulai. Ini adalah source keseluruhan dari file tests\Feature\Auth\RegisterTest.php:

<?php

namespace Tests\Feature\Auth;

use App\User; // Tambahkan use model App\User
use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;

class RegisterTest extends TestCase
{
    // Trait refresh database agar migration dijalankan
    use RefreshDatabase;

    /** @test */
    public function user_can_register()
    {
        // Kunjungi halaman '/register'
        $this->visit('/register');

        // Submit form register dengan nama, email dan password 2 kali
        $this->submitForm('Register', [
            'name'                  => 'John Thor',
            'email'                 => 'username@example.net',
            'password'              => 'secret',
            'password_confirmation' => 'secret',
        ]);

        // Lihat halaman ter-redirect ke url '/home' (register sukses).
        $this->seePageIs('/home');

        // Kita melihat halaman tulisan "Dashboard" pada halaman itu.
        $this->seeText('Dashboard');

        // Lihat di database, tabel users, data user yang register sudah masuk
        $this->seeInDatabase('users', [
            'name'  => 'John Thor',
            'email' => 'username@example.net',
        ]);

        // Cek hash password yang tersimpan cocok dengan password yang diinput
        $this->assertTrue(app('hash')->check('secret', User::first()->password));
    }

/** @test */
    public function user_name_is_required()
    {
        // Submit form untuk register dengan field 'name' kosong.
        $this->post('/register', [
            'name'                  => '',
            'email'                 => 'username@example.net',
            'password'              => 'secret',
            'password_confirmation' => 'secret',
        ]);

        // Cek pada session apakah ada error untuk field 'name'.
        $this->assertSessionHasErrors(['name']);
    }

    /** @test */
    public function user_name_maximum_is_255_characters()
    {
        // Submit form untuk register dengan field 'name' 260 karakter.
        $this->post('/register', [
            'name'                  => str_repeat('John Thor ', 26),
            'email'                 => 'username@example.net',
            'password'              => 'secret',
            'password_confirmation' => 'secret',
        ]);

        // Cek pada session apakah ada error untuk field 'name'.
        $this->assertSessionHasErrors(['name']);
    }

    /** @test */
    public function user_email_is_required()
    {
        // Submit form untuk register dengan field 'email' kosong.
        $this->post('/register', [
            'name'                  => 'John Thor',
            'email'                 => '',
            'password'              => 'secret',
            'password_confirmation' => 'secret',
        ]);

        // Cek pada session apakah ada error untuk field 'email'.
        $this->assertSessionHasErrors(['email']);
    }

    /** @test */
    public function user_email_must_be_a_valid_email()
    {
        // Submit form untuk register dengan field 'email' tidak valid.
        $this->post('/register', [
            'name'                  => 'John Thor',
            'email'                 => 'username.example.net',
            'password'              => 'secret',
            'password_confirmation' => 'secret',
        ]);

        // Cek pada session apakah ada error untuk field 'email'.
        $this->assertSessionHasErrors(['email']);
    }

    /** @test */
    public function user_email_maximum_is_255_characters()
    {
        // Submit form untuk register dengan field 'email' 260 karakter.
        $this->post('/register', [
            'name'                  => 'John Thor',
            'email'                 => str_repeat('username@example.net', 13),
            'password'              => 'secret',
            'password_confirmation' => 'secret',
        ]);

        // Cek pada session apakah ada error untuk field 'email'.
        $this->assertSessionHasErrors(['email']);
    }

    /** @test */
    public function user_email_must_be_unique_on_users_table()
    {
        // Buat satu user baru
        $user = factory(User::class)->create(['email' => 'emailsama@example.net']);

        // Submit form untuk register dengan field
        // 'email' yang sudah ada di tabel users.
        $this->post('/register', [
            'name'                  => 'John Thor',
            'email'                 => 'emailsama@example.net',
            'password'              => 'secret',
            'password_confirmation' => 'secret',
        ]);

        // Cek pada session apakah ada error untuk field 'email'.
        $this->assertSessionHasErrors(['email']);
    }

    /** @test */
    public function user_password_is_required()
    {
        // Submit form untuk register dengan field 'password' kosong.
        $this->post('/register', [
            'name'                  => 'John Thor',
            'email'                 => 'username@example.net',
            'password'              => '',
            'password_confirmation' => 'secret',
        ]);

        // Cek pada session apakah ada error untuk field 'password'.
        $this->assertSessionHasErrors(['password']);
    }

    /** @test */
    public function user_password_minimum_is_6_characters()
    {
        // Submit form untuk register dengan field 'password' 5 karakter.
        $this->post('/register', [
            'name'                  => 'John Thor',
            'email'                 => 'username@example.net',
            'password'              => 'ecret',
            'password_confirmation' => 'ecret',
        ]);

        // Cek pada session apakah ada error untuk field 'password'.
        $this->assertSessionHasErrors(['password']);
    }

    /** @test */
    public function user_password_must_be_same_with_password_confirmation_field()
    {
        // Submit form untuk register dengan field 'password'
        // beda dengan 'password_confirmation'.
        $this->post('/register', [
            'name'                  => 'John Thor',
            'email'                 => 'username@example.net',
            'password'              => 'secret',
            'password_confirmation' => 'escret',
        ]);

        // Cek pada session apakah ada error untuk field 'password'.
        $this->assertSessionHasErrors(['password']);
    }
}

Kalau kita lihat is file di atas, script yang paling banyak berulang adalah isian form register: name, email, password, dan password_confirmation. Berarti kita perlu membuat DRY Code untuk itu.

Jalankan Testing Sebelum Mulai

Seperti biasa, kita mulai kerja dengan menjalankan testing dulu.

# 1
$ vendor/bin/phpunit

OK (21 tests, 86 assertions)

Membuat Private Helper Method

Caranya adalah dengan membuat sebuah private helper method getRegisterFields yang berisi array attribute/isian yang lolos validasi form, seperti ini:

<?php
    // Class RegisterTest.php

    private function getRegisterFields()
    {
        return [
            'name'                  => 'John Thor',
            'email'                 => 'username@example.net',
            'password'              => 'secret',
            'password_confirmation' => 'secret',
        ];
    }
}

Menggunakan Helper Method

Sekarang kita gunakan helper method tersebut pada test method user_can_register:

<?php
    // Class RegisterTest.php

    /** @test */
    public function user_can_register()
    {        
        // Kunjungi halaman '/register'
        $this->visit('/register');

        // Submit form register dengan name, email dan password 2 kali
        $this->submitForm('Register', $this->getRegisterFields());

        // Lihat halaman ter-redirect ke url '/home' (register sukses).
        $this->seePageIs('/home');

        // Kita melihat halaman tulisan "Dashboard" pada halaman itu.
        $this->seeText('Dashboard');

        // Lihat di database, tabel users, data user yang register sudah masuk
        $this->seeInDatabase('users', [
            'name'  => 'John Thor',
            'email' => 'username@example.net',
        ]);

        // Cek hash password yang tersimpan cocok dengan password yang diinput
        $this->assertTrue(app('hash')->check('secret', User::first()->password));
    }
}

Jalankan PHPUnit:

# 2
$ vendor/bin/phpunit
Hasil: Passed

OK (21 tests, 86 assertions). Refactor pertama berhasil. Sekarang, untuk menggunakan helper method yang sama untuk test method yang lain, kita harus memodifikasinya sedikit.

Kalau kita lihat di test method yang lain, bagian attribute/isiannya juga array yang memiliki key yang sama, tetapi value nya yang berbeda-beda. Maka kita perlu menggunakan function array_merge() pada helper method kita.

<?php
    // Class RegisterTest.php

    private function getRegisterFields($overrides = [])
    {
        return array_merge([
            'name'                  => 'John Thor',
            'email'                 => 'username@example.net',
            'password'              => 'secret',
            'password_confirmation' => 'secret',
        ], $overrides);
    }
}

Pada method ini, kita memberikan variable $overrides sebagai parameter yang akan mengganti isi dari array yang di-return oleh method.

Baik sekarang coba kita gunakan method getRegisterFields ke test method validasi untuk name dulu, untuk memastikan modifikasi kita sudah benar.

<?php
    // Class RegisterTest.php
    
    // ... getRegisterFields()
    
    // ... user_can_register()

    /** @test */
    public function user_name_is_required()
    {
        // Submit form untuk register dengan field 'name' kosong.
        $this->post('/register', $this->getRegisterFields(['name' => '']));

        // Cek pada session apakah ada error untuk field 'name'.
        $this->assertSessionHasErrors(['name']);
    }

    /** @test */
    public function user_name_maximum_is_255_characters()
    {
        // Submit form untuk register dengan field 'name' 260 karakter.
        $this->post('/register', $this->getRegisterFields([
            'name' => str_repeat('John Thor ', 26)
        ]));

        // Cek pada session apakah ada error untuk field 'name'.
        $this->assertSessionHasErrors(['name']);
    }

Oke, sekarang kita jalankan PHPUnit:

# 3
$ vendor/bin/phpunit
Hasil: Passed

OK (21 tests, 86 assertions). Sip seharusnya memang kita tetap dapat hijau. Jika sampai di sini teman-teman dapat test failure, silakan lihat kembali 2 potongan kode di atas.

Sudah? Sekarang kita gunakan method helper kita pada test method validasi form yang lain.

<?php
    // Class RegisterTest.php
    
    // ... getRegisterFields()
    
    // ... user_can_register()
    
    // ... user_name_is_required()
    
    // ... user_name_maximum_is_255_characters()

    /** @test */
    public function user_email_is_required()
    {
        // Submit form untuk register dengan field 'email' kosong.
        $this->post('/register', $this->getRegisterFields(['email' => '']));

        // Cek pada session apakah ada error untuk field 'email'.
        $this->assertSessionHasErrors(['email']);
    }

    /** @test */
    public function user_email_must_be_a_valid_email()
    {
        // Submit form untuk register dengan field 'email' tidak valid.
        $this->post('/register', $this->getRegisterFields([
            'email' => 'username.example.net',
        ]));

        // Cek pada session apakah ada error untuk field 'email'.
        $this->assertSessionHasErrors(['email']);
    }

    /** @test */
    public function user_email_maximum_is_255_characters()
    {
        // Submit form untuk register dengan field 'email' 260 karakter.
        $this->post('/register', $this->getRegisterFields([
            'email' => str_repeat('username@example.net', 13),
        ]));

        // Cek pada session apakah ada error untuk field 'email'.
        $this->assertSessionHasErrors(['email']);
    }

    /** @test */
    public function user_email_must_be_unique_on_users_table()
    {
        // Buat satu user baru
        $user = factory(User::class)->create(['email' => 'emailsama@example.net']);

        // Submit form untuk register dengan field
        // 'email' yang sudah ada di tabel users.
        $this->post('/register', $this->getRegisterFields([
            'email' => 'emailsama@example.net',
        ]));

        // Cek pada session apakah ada error untuk field 'email'.
        $this->assertSessionHasErrors(['email']);
    }

    /** @test */
    public function user_password_is_required()
    {
        // Submit form untuk register dengan field 'password' kosong.
        $this->post('/register', $this->getRegisterFields(['password' => '']));

        // Cek pada session apakah ada error untuk field 'password'.
        $this->assertSessionHasErrors(['password']);
    }

    /** @test */
    public function user_password_minimum_is_6_characters()
    {
        // Submit form untuk register dengan field 'password' 5 karakter.
        $this->post('/register', $this->getRegisterFields(['password' => 'ecret']));

        // Cek pada session apakah ada error untuk field 'password'.
        $this->assertSessionHasErrors(['password']);
    }

    /** @test */
    public function user_password_must_be_same_with_password_confirmation_field()
    {
        // Submit form untuk register dengan field 'password'
        // beda dengan 'password_confirmation'.
        $this->post('/register', $this->getRegisterFields([
            'password'              => 'secret',
            'password_confirmation' => 'escret',
        ]));

        // Cek pada session apakah ada error untuk field 'password'.
        $this->assertSessionHasErrors(['password']);
    }

Nah seluruh test method sudah menggunakan private helper method kita, sekarang kita jalankan PHPUnit lagi:

# 4
$ vendor/bin/phpunit
Hasil: Passed

OK (21 tests, 86 assertions). Sip seharusnya kita tetap dapat hijau. Jika sampai di sini teman-teman dapat test failure, silakan lihat kembali script di atas, bandingkan dengan yang teman-teman punya.

Selesai proses refactoring kita sampai di sini.

Kesimpulan

Sekarang kalau kita lihat, proses yang ada di artikel ini agak panjang, tetapi aktualnya, yang dikerjakan tidak banyak. Teman-teman bisa membandingkan before dan after refactor-nya. Dari sebelumnya 183 baris, menjadi 154 baris (lumayan dipangkas 29 baris kode). Perbedaan baris yang ditambah (hijau) dan dihapus (merah) bisa teman-teman cek di commit ini.

Sekarang apa yang kita pelajari dari proses refactor ini? Kira-kira begini:

  1. DRY code bertujuan kita tidak menulis kode yang sama berulang-ulang (seperti contoh atribute/isian form register ini).
  2. Dengan DRY Code, ketika ada perubahan pada source code, kita hanya cukup mengubah kode satu kali. Misal kita diminta untuk menambah field phone_number pada form register, dengan testing seperti sebelumnya kita harus menambah field baru di setiap test method yang ada.
    Tetapi dengan testing yang sudah direfactor barusan, kita cukup menambah field baru di method getRegisterFields saja, maka semua test otomatis menggunakan field baru tersebut.
  3. Metode refactor ini dapat kita terapkan pada (hampir) semua feature test yang berurusan dengan CRUD.
  4. Jika sudah mengetahui metode refactor ini, maka teman-teman bisa langsung menerapkan teknik ini, tanpa harus mengerjakan before dan after.
  5. Jadi pada artikel sebelumnya dan artikel ini, saya ingin menunjukkan kepada teman-teman cara melakukan refactor saja. Aktualnya, silakan langsung terapkan DRY code jika kita memang sudah memahaminya :D

Saya mohon bantuan teman-teman untuk review proses dan hasil refactor ini, jika masih ada yang perlu di-refactor, mohon berikan komentar di bawah, agar kita diskusikan sama-sama. :)

Semoga belajar TDD di Laravel tetap menyenangkan. Demikian dan terima kasih atas waktu teman-teman, semoga bermanfaat.