Test Double

Stub dan mock untuk menggantikan dependensi.

Daftar isi

Test double adalah objek pengganti yang ditempatkan pada posisi dependensi nyata saat pengujian berjalan, sebagaimana pengganti pemeran (stunt double) dalam produksi film. Topik ini membangun layanan laporan nilai yang bergantung pada penyedia data, lalu menggantikan penyedia tersebut dengan stub untuk memberi jawaban buatan dan dengan mock untuk memverifikasi interaksi. Setelah topik ini, pengujian atas logika yang bergantung pada basis data atau jaringan dapat berjalan cepat tanpa menyentuh keduanya.

Kebutuhan akan pengganti dependensi#

Kelas LaporanNilai berikut menentukan status kelulusan dari nilai tertinggi yang diterimanya. Sumber nilai dideklarasikan sebagai antarmuka PenyediaNilai, bukan sebagai kelas konkret, sehingga implementasinya dapat ditukar. Pada aplikasi nyata, implementasi tersebut membaca basis data atau memanggil API.

php PenyediaNilai.php
<?php

declare(strict_types=1);

interface PenyediaNilai
{
    public function nilaiTertinggi(): int;
}

final class LaporanNilai
{
    public function __construct(private readonly PenyediaNilai $penyedia)
    {
    }

    public function status(): string
    {
        return $this->penyedia->nilaiTertinggi() >= 75 ? 'lulus' : 'remedial';
    }
}

Pengujian atas LaporanNilai hanya perlu membuktikan dua hal, yaitu batas 75 diterapkan dengan benar dan metode nilaiTertinggi() dipanggil pada waktu yang tepat. Kedua hal itu tidak memerlukan basis data sungguhan. Pengujian yang menjalankan basis data menjadi lambat, bergantung pada keadaan data yang berubah, dan sulit diatur agar menghasilkan kasus yang diinginkan. Antarmuka inilah titik sambung tempat pengganti dipasang.

Deklarasi dependensi sebagai antarmuka bukan kebetulan desain, melainkan syarat agar penggantian mungkin. Kelas yang membuat sendiri koneksi basis data di dalam metode tidak menyediakan celah untuk penggantian, sedangkan kelas yang menerima koneksi tersebut lewat konstruktor menyediakannya secara eksplisit. Pola ini sering disebut injeksi dependensi (dependency injection), dan manfaat terbesarnya justru terasa pada pengujian: dependensi apa pun yang mengikuti kontrak yang sama dapat dipasang tanpa menyentuh kode layanan.

Stub: menggantikan jawaban#

Stub adalah test double yang jawabannya dikonfigurasi sebelum pengujian berjalan. Metode createStub() dari TestCase membuat implementasi antarmuka yang seluruh metodenya mengembalikan nilai default, lalu metode method() dan willReturn() mengatur jawaban yang diinginkan. Berkas uji di bawah ini dimuat oleh perintah phpunit, bukan dijalankan langsung dengan php.

php Berkas LaporanNilaiTest.php
// tidak-dijalankan
// berkas uji dimuat oleh perintah phpunit, bukan dijalankan langsung dengan php
<?php

declare(strict_types=1);

require_once __DIR__ . '/PenyediaNilai.php';

use PHPUnit\Framework\TestCase;

final class LaporanNilaiTest extends TestCase
{
    public function testStubMengembalikanNilaiYangDikonfigurasi(): void
    {
        $stub = $this->createStub(PenyediaNilai::class);
        $stub->method('nilaiTertinggi')->willReturn(88);

        $laporan = new LaporanNilai($stub);

        $this->assertSame('lulus', $laporan->status());
    }

    public function testMockMemverifikasiPemanggilanMetode(): void
    {
        $mock = $this->createMock(PenyediaNilai::class);
        $mock->expects($this->once())->method('nilaiTertinggi')->willReturn(60);

        $laporan = new LaporanNilai($mock);

        $this->assertSame('remedial', $laporan->status());
    }
}

Pada pengujian pertama, stub selalu menjawab 88 setiap kali nilaiTertinggi() dipanggil, sehingga hasil lulus sepenuhnya ditentukan konfigurasi dan bukan oleh data eksternal. Konfigurasi willReturn() menerima satu nilai atau deret nilai yang dikembalikan berurutan, dan willReturnMap() menautkan argumen tertentu ke jawaban tertentu bila metodenya berparameter.

bash
# jalankan kedua pengujian pada berkas LaporanNilaiTest
phpunit --no-progress LaporanNilaiTest.php 2>&1 | grep -v '^Time:' | cat -s
text Keluaran
PHPUnit 11.5.57 by Sebastian Bergmann and contributors.

Runtime:       PHP 8.3.27

OK (2 tests, 3 assertions)

Mock: memverifikasi interaksi#

Mock melakukan semua yang dilakukan stub, ditambah satu kemampuan yang menjadi intinya, yaitu memverifikasi bahwa metode tertentu dipanggil dengan cara tertentu. Baris expects($this->once()) pada pengujian kedua menyatakan harapan berupa pemanggilan tepat satu kali, dan PHPUnit menegakkan harapan itu sebagai bagian dari keberhasilan pengujian.

Apabila LaporanNilai diubah sehingga tidak pernah memanggil nilaiTertinggi(), pengujian stub pertama tetap berhasil selama status() mengembalikan nilai yang cocok, sedangkan pengujian mock kedua gagal karena harapan once() dilanggar. Perbedaan perilaku ini menunjukkan batas keduanya, yaitu stub menguji hasil terhadap data yang disediakan, sedangkan mock menguji komunikasi antara objek yang diuji dan dependensinya.

Kapan stub, kapan mock#

Pilih stub ketika yang diperiksa adalah nilai kembalian atas input tertentu, sebagaimana pengujian pertama membuktikan batas 75. Pilih mock ketika efek samping atau frekuensi pemanggilan adalah bagian dari perilaku yang diuji, misalnya memastikan layanan kiriman hanya dijalankan sekali. Keduanya dipakai bersama secara wajar dalam satu berkas uji, sebagaimana terlihat pada LaporanNilaiTest.php.

Penggunaan mock yang berlebihan membuat pengujian terkunci pada cara kerja internal, bukan pada hasilnya. Setiap penataan ulang kode yang tidak mengubah perilaku akan membuat pengujian yang terlalu banyak menegakkan panggilan internal menjadi gagal. Praktik yang seimbang adalah menggunakan stub untuk mayoritas kebutuhan data dan menyisakan mock hanya untuk interaksi yang kegagalannya langsung dirasakan pengguna, seperti penyimpanan dan pengiriman data.

Istilah test double pada PHPUnit mencakup beberapa varian lain. Dummy hanya mengisi parameter tanpa pernah dipakai, fake menggunakan implementasi ringan seperti tabel simpanan di memori, dan spy mencatat pemanggilan untuk diperiksa belakangan. createStub() dan createMock() menutup kebutuhan sehari-hari, sedangkan varian lain dibutuhkan ketika skenarionya lebih spesifik. Pengujian yang menggantikan basis data dengan tabel di memori, misalnya, lebih cocok disebut fake daripada stub karena implementasinya sungguhan, hanya saja versi ringannya.

Kesalahan umum#

  • Mengonfigurasi stub untuk metode yang tidak pernah dipanggil. Stub tidak menegakkan apa pun, sehingga konfigurasi yang tak terpakai hanya menambah kebisingan. Bila frekuensi pemanggilan penting, itu tugas mock.
  • Memakai nilai nyata dari basis data pada harapan mock. Harapan sebaiknya berupa nilai sederhana yang ditentukan pengujian itu sendiri, karena ketergantungan pada data nyata menghadirkan kembali masalah yang ingin dihindari.
  • Membuat test double untuk nilai sederhana. Dependensi berupa array atau objek nilai tidak perlu diganti, cukup diisi langsung pada konstruksi pengujian.
  • Lupa expects() harus ditulis sebelum pemanggilan. Harapan dinyatakan pada test double sebelum objek yang diuji berjalan, bukan sesudahnya, karena PHPUnit memeriksanya pada saat pengujian dituntaskan.

Ringkasan#

  • Test double menggantikan dependensi nyata melalui antarmuka yang dideklarasikan kelas yang diuji, sehingga pengujian berjalan cepat tanpa basis data maupun jaringan.
  • Stub memberi jawaban terkonfigurasi lewat createStub() dan willReturn(), sedangkan mock menambahkan penegakan harapan lewat expects() dan createMock().
  • Stub menguji hasil atas data buatan, mock menguji komunikasi antarobjek, dan kombinasi keduanya dalam satu berkas uji adalah pola yang wajar.
  • Mock yang berlebihan mengunci detail internal, sehingga simpanlah untuk interaksi yang kegagalannya benar-benar terasa.

Referensi#