Penggunaan fungsi TEST.PHP pada PHP

Selamat siang, kali ini saya ingin membagikan hasil belajar saya mengenai unit testing menggunakan PHPUnit di bahasa pemrograman PHP.

Instalasi

Jalankan perintah berikut untuk menginstall PHPUnit versi terbaru melalui composer

composer require --dev phpunit/phpunit

setelah itu install package XDebug melalui website https://xdebug.org/

  1. PHP_Invoker
    composer require --dev phpunit/php-invoker
  2. DbUnit
    composer require --dev phpunit/dbunit

Mengorganisir Unit Test

Unit test sebaiknya diletakkan di folder tests agar lebih rapi dan mudah di organisir. Standar penamaan unit test adalah sebagai berikut :

[NamaClass]Test.php

Note:
Apabila menggunakan composer manfaatkan fitur autoload dari PSR-4 atau PSR-0 agar kita tidak perlu melakukan include class di tiap file unit test.

Contoh konfigurasi dari PSR-0 di file composer.json di file belajar menulis PHPUnit.

{
    "name": "nekoding/belajar-phpunit-test",
    "description": "Belajar menulis PHPUnit",
    "type": "project",
    "license": "MIT",
    "authors": [
        {
            "name": "Enggar Tivandi",
            "email": "[email protected]"
        }
    ],
    "require-dev": {
        "phpunit/phpunit": "9"
    },
    "scripts": {
        "test": "./vendor/bin/phpunit tests"
    },
    "autoload": {
        "psr-0": {
            "Nekoding\\App\\": "src/"
        }
    }
}

File hasil latihan saya dapat diakses melalui link berikut ini :

nekoding/belajar-phpunit

Repository belajar PHPUnit. Contribute to nekoding/belajar-phpunit development by creating an account on GitHub.

Penggunaan fungsi TEST.PHP pada PHP
GitHubnekoding

Anda dapat mengirimkan pull request ke repository diatas apabila ingin menambahkan contoh unit testing lain.

Konfigurasi Unit Testing menggunakan berkas XML

Konfigurasi PHPUnit bisa juga dilakukan menggunakan kode konfigurasi di XML. buat file dengan nama phpunit.xml lalu isikan kode berikut :

<phpunit bootstrap="src/autoload.php">
    <testsuites>
        <testsuite name="namaUnitTest">
            <directory>tests</directory>
        </testsuite>
    </testsuites>
</phpunit>

atau juga bisa mengikuti format yang digunakan laravel

<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
         bootstrap="vendor/autoload.php"
         colors="true"
>
    <testsuites>
        <testsuite name="Unit">
            <directory suffix="Test.php">./tests/Unit</directory>
        </testsuite>
        <testsuite name="Feature">
            <directory suffix="Test.php">./tests/Feature</directory>
        </testsuite>
    </testsuites>
    <coverage processUncoveredFiles="true">
        <include>
            <directory suffix=".php">./app</directory>
        </include>
    </coverage>
    <php>
        <server name="APP_ENV" value="testing"/>
        <server name="BCRYPT_ROUNDS" value="4"/>
        <server name="CACHE_DRIVER" value="array"/>
        <!-- <server name="DB_CONNECTION" value="sqlite"/> -->
        <!-- <server name="DB_DATABASE" value=":memory:"/> -->
        <server name="MAIL_MAILER" value="array"/>
        <server name="QUEUE_CONNECTION" value="sync"/>
        <server name="SESSION_DRIVER" value="array"/>
        <server name="TELESCOPE_ENABLED" value="false"/>
    </php>
</phpunit>

Contoh penulisan test case dasar

<?php
use PHPUnit\Framework\TestCase;
class SampleTest extends TestCase
{
    public function testSomething()
    {
        // Optional: Test anything here, if you want.
        $this->assertTrue(true, 'This should already work.');

        // Stop here and mark this test as incomplete.
        $this->markTestIncomplete(
            'This test has not been implemented yet.'
        );
    }
}
?>

Penjelasan :

kode $this->assertTrue(actual, message) menerima 2 inputan parameter dimana nilai actual harus bernilai true, apabila nilai actual mereturn nilai selain nilai true maka test case akan gagal.

kode $this->markTestIncomplete digunakan untuk menandai bahwa test case tersebut masih belum lengkap.

Penamaan method unit test

secara default PHPUnit akan mendeteksi semua method yang memiliki nama awalan test didalam class yang melakukan extend dari class TestCase.

contoh :

<?php
use PHPUnit\Framework\TestCase;
class StackTest extends TestCase
{
    public function testStringIsString()
    {
        $this->assertEquals('string', 'string');
    }
}

tapi kita juga bisa menambahkan annotation apabila nama method test case kita tidak ada awalan test nya

<?php
use PHPUnit\Framework\TestCase;
class StackTest extends TestCase
{
    /**
     * @test
     */
    public function string_is_string()
    {
        $this->assertEquals('string', 'string');
    }
}

atau bisa juga seperti ini

public function test_it_can_be_true()
{
    $this->assertTrue(true);
}

Di PHPUnit tiap method unit test dapat berjalan meskipun salah satu method gagal / overlapping. Namun kita juga bisa mengeset agar kode unit test saling menunggu. Jadi anggap saja kita punya 2 kode unit test yang bernama methodA dan methodB. Kita dapat membuat methodB ini jalan apabila methodA sukses dengan cara menambahkan annotation @depends

<?php
use PHPUnit\Framework\TestCase;
class StackTest extends TestCase
{
    public function testEmpty()
    {
        $stack = [];
        $this->assertEmpty($stack);
        return $stack;
    }

    /**
     * @depends testEmpty
     */
    public function testPush(array $stack)
    {
        array_push($stack, 'foo');
        $this->assertEquals('foo', $stack[count($stack)-1]);
        $this->assertNotEmpty($stack);
        return $stack;
    }
    /**
     * @depends testPush
     */
    public function testPop(array $stack)
    {
        $this->assertEquals('foo', array_pop($stack));
        $this->assertEmpty($stack);
    }
}
?>

pada kode diatas method testPush tidak akan dijalankan sampai method testEmpty berhasil dan testPop tidak akan dijalankan sampai method testPush berhasil.

Melompati test case

Untuk melompati test case kita dapat menambahkan kode ini ke dalam method test case yang dibuat

public function test_it_can_skipped()
{
    $this->markTestSkipped(string $message)
}

maka test case it can skipped akan dilompati / tidak dijalankan. Selain menggunakan kode diatas kita juga bisa menambahkan annotation agar test case hanya dijalankan ketika memenuhi konfig yang sudah dibuat. apabila environment tidak sesuai maka test case akan dilompati.

@requires PHP x.x.x => untuk menjalankan test case maka dibutuhkan versi PHP x.x.x

@requires PHPUnit x.x.x => untuk menjalankan test case maka dibutukan versi PHPUnit x.x.x

@requires OS Linux => untuk menjalankan test case maka dibutuhkan sistem operasi linux

@requires OSFAMILY Solaris => untuk menjalankan test case maka dibutuhkan sistem operasi turunan solaris

@requires function imap_open => untuk menjalankan test maka case dibutuhkan fungsi imap_open

@requires extension xxxx => untuk menjalankan test case maka dibutuhkan ekstensi php xxxx

contoh penggunaan dalam unit test

<?php

use PHPUnit\Framework\TestCase;

/**
 * @requires extension mysqli
 */
class DatabaseTest extends TestCase
{
    /**
     * @requires PHP 5.3
     */
    public function testConnection()
    {
        // Test requires the mysqli extension and PHP >= 5.3
    }

    // ... All other tests require the mysqli extension
}
?>

Fixture

Fixture adalah sebuah mekanisme dimana kita mengeset sebuah nilai state yang nantinya akan digunakan dalam unit test. di PHPUnit ada 2 fungsi yang berhubungan dengan fixture ini yaitu setUp() dan tearDown() .

SetUp Digunakan untuk mengeset nilai state sewaktu file unit test dipanggil.

<?php
use PHPUnit\Framework\TestCase;
class StackTest extends TestCase
{
    protected $stack;
    protected function setUp()
    {
        $this->stack = [];
    }
    public function testEmpty()
    {
        $this->assertTrue(empty($this->stack));
    }
}

TearDown Digunakan untuk menghapus nilai state apabila sudah tidak digunakan didalam unit test.

<?php
use PHPUnit\Framework\TestCase;
class StackTest extends TestCase
{
    protected $stack;
    protected function setUp()
    {
        $this->stack = [];
    }
    public function tearDown()
    {
        $this->stack = null;
    }
}

State yang sudah dibuat tadi sebenarnya juga bisa dishare ke method testcase lain.

<?php
use PHPUnit\Framework\TestCase;
class DatabaseTest extends TestCase
{
    protected static $dbh;
    public static function setUpBeforeClass()
    {
        self::$dbh = new PDO('sqlite::memory:');
    }
    public static function tearDownAfterClass()
    {
        self::$dbh = null;
    }
}
?>

Jika menggunakan kode diatas nilai $dbh hanya diset sekali saja ketika class unit test terpanggil, sehingga method lain yang memerlukan $dbh tidak perlu menjalankan ulang fungsi setUp. Apabila menggunakan kode setUp seperti sebelumnya ketika method unit test terpanggil maka akan mentrigger method setUp

Data provider

Data provider digunakan untuk mendefinisikan / mengisi parameter yang dibutuhkan oleh unit test.

contoh :

<?php
use PHPUnit\Framework\TestCase;
class DataTest extends TestCase
{
    /**
     * @dataProvider additionProvider
     */
    public function testAdd($a, $b, $expected)
    {
        $this->assertEquals($expected, $a + $b);
    }

    public function additionProvider()
    {
        return [
            [0, 0, 0],
            [0, 1, 1],
            [1, 0, 1],
            [1, 1, 3]
        ];
    }
}
?>

kode diatas dapat diartikan bahwa method testAdd akan mengisi nilai $a, $b, $expected yang berasal dari method additionProvider

Menguji Exceptions

PHPUnit menyediakan api untuk melakukan testing terhadap nilai Exception. ada 2 cara untuk melakukan test terhadap exception :

  • Menggunakan method expectException()
public function testException()
{
    $this->expectException(InvalidArgumentException::class);
    // kode yang bakalan kena exception dengan tipe exception invalid argument
}
  • Menggunakan Annotations
/**
* @expectedException InvalidArgumentException
*/
public function testException()
{
	// kode yang bakalan kena InvalidArgumentException
}

apabila ingin menguji nilai dari error, warning, dan notices di PHP dapat menggunakan kode berikut

<?php
use PHPUnit\Framework\TestCase;
class ExpectedErrorTest extends TestCase
{
    /**
    * @expectedException PHPUnit\Framework\Error
    */
    public function testFailingInclude()
    {
    	include 'not_existing_file.php';
    }
}
?>

Melakukan test terhadap output

Disini kita dapat menguji nilai keluaran / hasil dari echo atau print di PHP dengan cara

<?php
use PHPUnit\Framework\TestCase;
class OutputTest extends TestCase
{
    public function testExpectFooActualFoo()
    {
        $this->expectOutputString('foo');
        print 'foo';
    }
    public function testExpectBarActualBaz()
    {
        $this->expectOutputString('bar');
        print 'baz';
    }
}
?>

Membuat mock / stub dari unit test

Di PHPUnit kita dapat membuat hasil dari sebuah method di dalam suatu class agar sesuai dengan yang kita inginkan meskipun di method tersebut sudah ada return value yang didefinisikan.

<?php

class Something
{
    public function getName()
    {
        return 'kirintux';
    }
}
<?php

use PHPUnit\Framework\TestCase;

class UnitTest extends TestCase
{
    public function test_stub_test_case()
    {
        $stub = $this->createStub(Something::class);

        $stub->method('getName')->willReturn('nekoding');

        // hasil yang akan direturn oleh method getName sekarang adalah nekoding
        // bukan kirintux lagi
        $this->assertEquals('nekoding', $stub->getName());
    }
}

atau jika ingin explicit bisa menggunakan Mock Builder API milik phpunit

<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;

final class StubTest extends TestCase
{
    public function testStub(): void
    {
        // Create a stub for the SomeClass class.
        $stub = $this->getMockBuilder(SomeClass::class)
                     ->disableOriginalConstructor()
                     ->disableOriginalClone()
                     ->disableArgumentCloning()
                     ->disallowMockingUnknownTypes()
                     ->getMock();

        // Configure the stub.
        $stub->method('getName')
             ->willReturn('foo');

        // Calling $stub->getName() will now return
        // 'foo'.
        $this->assertSame('foo', $stub->getName());
    }
}

Untuk lebih lengkapnya bisa mengunjungi link berikut ini

8. Test Doubles — PHPUnit 9.5 Manual

code coverage

Adalah mekanisme untuk melakukan analisa sejauh mana unit test telah dilakukan pada suatu class / object. Ada 6 metriks untuk mengukur code coverage :

  1. Line coverage
    Diukur berdasarkan apakah semua executable line atau method didalam class sudah tercover didalam unit test
  2. Branch coverage
    Diukur berdasarkan apakah semua test sudah mencoba membuat method tersebut menjadi salah dan benar
  3. Path coverage
    Diukur berdasarkan apakah setiap kemungkinan tercover dalam unit test
  4. Function and Method coverage
    Diukur berdasarkan setiap function atau method telah terpanggil
  5. Clean and Trait coverage
    Diukur berdasarkan apakah setiap method di dalam trait yang digunakan di dalam class telah ditest
  6. Change Risk Anti-pattern (CRAP Index)
    Diukur berdasarkan komplektivitas kode testnya.

Contoh code coverage pada unit test yang saya buat

Penggunaan fungsi TEST.PHP pada PHP
Contoh unit test sederhana yang saya buat

Referensi :

PHPUnit Manual — PHPUnit 9.5 Manual

Apabila dari tulisan saya ada yang keliru atau kurang jelas bisa komen dibawah. Sekian terima kasih.

Apa itu PHP unit testing?

Unit Testing adalah tes terkecil dalam serangkaian test untuk menguji sebuah fungsi atau kelas pada kode kita. Sebenarnya, beberapa tahapan testing dalam pengembangan aplikasi.

Apa yang dimaksud dengan unit testing dan berfungsi untuk apa?

Unit testing adalah metode yang masing-masing unit dari kode sumber diuji untuk menentukan apakah mereka cocok untuk digunakan. Unit tes pada dasarnya ditulis dan dieksekusi oleh pengembang perangkat lunak untuk memastikan kode yang memenuhi desain dan persyaratan dan berperilaku seperti yang diharapkan.