Tutorial : Implementasi Unit Test PHPUnit menggunakan mock, stub dan data provider pada project PHP

Muhammad Hakim
6 min readNov 27, 2022

--

https://wallpapercave.com/wp/wp10992146.png

Unit test digunakan untuk menguji perangkat lunak dalam cakupan unit terkecil yang dilakukan secara terisolasi.

Cakupan unit terkecil biasanya berupa sebuah method / fungsi.
Dilakukan secara terisolasi artinya method tersebut tidak memiliki ketergantungan terhadap objek lain.

Pada kesempatan kali ini, saya akan mencoba sharing tutorial implementasi unit test pada bahasa pemrograman PHP dengan memanfaatkan mock , stub dan data-provider yang telah disediakan oleh tools PHPUnit.

Requirements

Saat ini saya menggunakan sistem operasi windows 11 , PHP 7.4.29, PHPUnit versi 9.5, VSCode dan web server laragon. Adapun requirement yang berkaitan dengan tutorial kali ini adalah;

  • PHP Unit
    Pemaham dasar tools PHP Unit terkait bagaiamana melakukan instalasi, membaca report hasil pengujian, menjalankan command untuk melakukan pengujian, dll. Dokumentasi terkait tools ini dapat dilihat disini.
  • XDebug
    Ekstensi bahasa pemrograman PHP yang berfungsi untuk memberikan report terkait code coverage yang kita dapatkan setelah unit test dijalankan. Instalasinya bisa tonton video berikut.

Case

Terdapat sebuah projek aplikasi backend sederhana yang dikembangkan menggunakan bahasa pemrograman PHP dan ditulis tanpa menggunakan framework apapun (native code).

Kelas-kelas yang tersedia dari aplikasi tersebut adalah sebagai berikut ;

Class

Secara sederhana, aplikasi ini hanya berfungsi untuk menangani API request yang berkaitan dengan data-data employee seperti ;

1. mengambil data employee menggunakan ID.
2. mengkalkulasi gaji employee.
3. mengkalkulasi masa kerja dari employee.

Source code projek aplikasi ini bisa dilihat pada repository berikut.

Bagian mana yang mau ditesting ?

Dalam praktiknya, sangat baik apabila kita sebagai engineer menguji seluruh method yang terdapat dalam aplikasi. Namun, dalam beberapa kasus pembuatan unit testing akan sangat memakan waktu sehingga berdampak terhadap timeline yang telah ditentukan.

Subjectively, the teams experienced a 15–35% increase in initial development time after adopting Test-driven Development (Microsoft and IBM on testing their product) source

Oleh karena itu, pada kesempatan kali ini saya akan membuat testing terhadap core business logic saja ketimbang membuat testing untuk seluruh unit yang tersedia.

Pada tutorial ini saya akan membuat testing terhadap objek EmployeeServices method getEmployeeById.

<?php

class EmployeeServices
{

private EmployeeRepository $employeeRepository;
private ?AccountRepository $accountRepository;

// depedency injections
function __construct(
EmployeeRepository $employeeRepository,
AccountRepository $accountRepository = null
)
{
$this->employeeRepository = $employeeRepository;
$this->accountRepository = $accountRepository;
}

public function getEmployeeById(int $Id) : Employee
{
$employee = $this->employeeRepository->getEmployeeById($Id);
return $employee;
}
}

Organisasi folder dan file untuk testing

File testing perlu diatur agar tidak bercampur dengan file-file lain yang ada dalam projek aplikasi. Pada tutorial ini saya akan menempatkan folder khusus testing di dalam folder services dan menamakan file testing dengan format (Nama File yang di testing)Test.php.

Organisasi file dan folder unit testing

Penamaan class juga ditambahkan suffix Test sehingga menjadi EmployeeServicesTest.

Unit Test Coding

Setiap class testing harus meng-extends objek TestCase yang telah disediakan oleh PHPUnit. Penamaan method (disarankan) menggunakan prefix test atau menggunakan annotasi @test pada bagian atas method.

<?php declare(strict_types=1);

require_once "app/services/EmployeeServices.php";
require_once "app/services/NotificationServices.php";
require_once "app/repository/EmployeeRepository.php";
require_once "app/repository/AccountRepository.php";
require_once "app/model/Employee.php";

use PHPUnit\Framework\TestCase;

class EmployeeServicesTest extends TestCase
{
/*
* @test
*/
public function testGetEmployeeById()
{
/*
* put testing logic here
*/
}
}

Data-provider

Sebelum membuat logika testing sebaiknya kita menentukan data yang dibutuhkan untuk skenario testing yang dibuat. Penentuan data ini tentu saja mengacu pada kebutuhan data pada method yang akan di testing.

PHPUnit menyediakan addition provider (baca disini) untuk mengakomodasi kebutuhan data saat method testing dijalankan.

Pada tutorial kali ini, data yang dibutuhkan tentu saja berkaitan dengan data-data employee. Oleh karena itu saya akan membuat data-provider employee

    /*
* provide employee data for testing
*/
public function employeeAdditionProvider():array
{
$employee1 = new Employee();
$employee1->setId(10);
$employee1->setName("Sandro Tonali");
$employee1->setAge(25);
$employee1->setGrades(3);
$employee1->setJoinDate("2012-10-01");
$salaryEmployee1 = 7500000;
$yearsOfServicesEmployee1 = 10;

$testSuitesEmployee1 = [ $employee1, $salaryEmployee1, $yearsOfServicesEmployee1 ];

$employee2 = new Employee();
$employee2->setId(11);
$employee2->setName("Kevin De Bruyne");
$employee2->setAge(31);
$employee2->setGrades(8);
$employee2->setJoinDate("2002-10-01");
$salaryEmployee2 = 248000000;
$yearsOfServicesEmployee2 = 20;

$testSuitesEmployee2 = [ $employee2, $salaryEmployee2, $yearsOfServicesEmployee2 ];

return [
$testSuitesEmployee1,
$testSuitesEmployee2
];
}

Saya mempersiapkan dua buah objek data employee yang akan dijadikan sebagai data pengujian. Setiap objek employee ini akan diconsume oleh method testing yang telah kita buat.

Selain mempersiapkan data-provider dengan cara tersebut, kita juga dapat mempersiapkan data-provider menggunakan faker.

Selanjutnya tambahkan parameter objek employee dan anotasi @dataProvider pada method testing yang kita buat sehingga method testing tersebut dapat meng-consume data-data employee.

class EmployeeServicesTest extends TestCase
{
/*
* @test
* @dataProvider employeeAdditionProvider
*/
public function testGetEmployeeById(Employee $employee)
{
/*
* put testing logic here
*/
}
}

Mocking an object

Tidak ada objek lain yang dilibatkan dalam pengujian sebuah unit ( terisolasi ). Lalu bagaimana jika method yang akan kita uji memerlukan objek lain untuk menjalankan fungsinya (?).

Buat tiruan dari objeknya.

Dalam praktiknya tidak mungkin kita melibatkan objek asli secara langsung ketika menjalankan pengujian unit. Bayangkan apabila kita akan menguji sebuah method yang melakukan insert pada basis-data. Apakah kita akan membanjiri basis-data dengan data-data testing (?).

Pada tutorial kali ini, method getEmployeeById memerlukan objek dari kelas EmployeeRepository untuk mendapatkan data employee dari basis-data menggunakan parameter id employee.

Oleh karena itu, kita akan membuat tiruan objek dari kelas EmployeeRepository.

class EmployeeServicesTest extends TestCase
{
/*
* @test
* @dataProvider employeeAdditionProvider
*/
public function testGetEmployeeById(Employee $employee)
{
// mocking EmployeeRepository
$employeeRepository = $this->createMock(EmployeeRepository::class);
}
}

Stub the mock object

Lalu bagaimana jika objek kelas EmployeeRepository akan mengembalikan sebuah data tertentu melalui salah satu method-nya yang berkaitan dengan method yang sedang kita uji (?) :(.

Buat tiruan kembalian datanya

PHPUnit menyediakan fungsi agar kita dapat memanipulasi behaviour dari sebuah method objek yang di-mocking. Nama lain dari fungsi ini adalah stub (baca disini).

Pada tutorial kali ini, method getEmployeeById dari objek EmployeeRepository akan mengembalikan objek employee berdasarkan Id employee yang kita masukkan pada paremeternya.

Oleh karena itu, kita akan membuat tiruan data dari kembalian method getEmployeeById dari objek EmployeeRepository.

class EmployeeServicesTest extends TestCase
{
/*
* @test
* @dataProvider employeeAdditionProvider
*/
public function testGetEmployeeById(Employee $employee)
{
// mocking EmployeeRepository
$employeeRepository = $this->createMock(EmployeeRepository::class);

// stub the return value of getEmployeeById
$employeeRepository->method("getEmployeeById")->willReturn($employee);
}
}

Execute the test method

Ketika seluruh kebutuhan dari method yang akan kita uji telah terpenuhi. Langkah selanjutnya adalah membuat code untuk menjalankan fungsi dari method tersebut.

class EmployeeServicesTest extends TestCase
{
/*
* @test
* @dataProvider employeeAdditionProvider
*/
public function testGetEmployeeById(Employee $employee)
{
// mocking EmployeeRepository
$employeeRepository = $this->createMock(EmployeeRepository::class);

// stub the return value of getEmployeeById
$employeeRepository->method("getEmployeeById")->willReturn($employee);

// Execute the test method
$services = new EmployeeServices($employeeRepository);
$employeeResult = $services->getEmployeeById($employee->getId());
$this->assertEquals($employee, $employeeResult, "Get employee data by id");
}
}

Gunakan assertion untuk membandingkan output yang diberikan oleh method yang kita uji dengan ekspektasi output yang telah kita definisikan sebelumnya pada data-provider (baca assertion disini).

Report of code coverage

Selain menilai kualitas pengujian melalui skenario uji, kita juga dapat menilai seberapa banyak baris kode yang diuji.

PHPUnit menyediakan code coverage analysis untuk memberikan laporan seberapa banyak baris kode yang kita uji. Metriks laporan ini dibuat dalam bentuk presentase(%) (baca disini). Setiap perusahaan biasanya memiliki ketentuan masing-masing dalam menentukan berapa persentase yang harus di capai dalam membuat pengujian.

Laporan yang diberikan dapat disajikan dalam berbagai bentuk penyajian. Pada tutorial kali ini, saya akan menjalan sebuah command PHPUnit untuk mendapatkan laporan code coverage analysis yang disajikan dalam bentuk file HTML.

./vendor/bin/phpunit app/services --testdox --colors --coverage-filter app/services --coverage-html coverage/
report code coverage analysis of EmployeeServices.php

Selain laporan persentase coverage untuk tiap-tiap method, baris kode yang berhasil diuji, tidak diuji atau tidak bisa diuji pun bisa diperlihatkan.

line of executed-code, not-executed-code or dead-code

Demikianlah sharing singkat saya untuk tutorial kali ini. Sampai jumpa di kesempatan lainnya…

Contact me let’s make some fun , huehuehue :
https://github.com/Mhakimamransyah
https://www.linkedin.com/in/hakim-amr/
mailto: m.hakim.amransyah.hakim@gmail.com

--

--

Muhammad Hakim

Software Engineer | Backend Enthusiast | Long-lived learner