Unit Testing with PHP

Dikirimkan pada - Kali Terakhir Diubah Suai pada

Testing is an important part of the software development process because it improves the quality of the code and the quality of the final product. Since PHP is still one of the most widely-used programming languages around the world, I thought it would be a good idea to cover testing basics using PHP.

PHPUnit is one of the most widely-used testing frameworks for PHP. The installation of PHPUnit is simple, and on an Ubuntu system it can be installed using:

	sudo apt-get install phpunit

We can also download the phar file from the PHPUnit website and do the installation manually. You don’t need to have a Web server running on the system to run the code, but you're going to need PHP or PHP5 and the related CLI (command line interface).

installing phpunit

Now let's go through the steps on how to write a unit test and how to execute these using PHPUnit. Here's a simple code base that mimics the functionality of a bank account:

namespace Banks\BankAccountFramework;

class BankAccount {

	private $balance;
	private $accountNumber;
	private $owner;

	public function __construct($owner, $initialBalance) {

		if($initialBalance < 0) {
			throw new \Exception("Initial Balance Cannot Be Negative.", 1001);
		}

		if(empty($owner)) {
			throw new \Exception("Owner should not be empty.", 1002);
		}

		$this->balance = $initialBalance;
		$this->owner = $owner;
	}

	public function getBalance() {
		return $this->balance;
	}

	public function getOwner() {
		return $this->owner;
	}

	public function withdraw($amount) {
		if ($this->balance < $amount) {
			throw new Exception("Cannot withdraw more than actual balance:".$this->balance, 1003);
		}

		$this->balance-=$amount;

		return $this->balance;
	}

	public function deposit($amount) {
		if($amount > 10000) {
			throw new \Exception("Deposit amount is bigger than 10000 USD/EUR, needs additional approvers.", 1004);
		}

		$this->balance += $amount;
	}
}

Our BankAccount class supports deposit and withdraw. Of course, we added some logic to allow us to cover more advanced test cases. We will create eight tests where we can cover so called Fail and Successful (OK) tests. Let's look at the code of the test class:

namespace Banks\BankAccountFramework\Tests;

use Banks\BankAccountFramework\BankAccount;
use \Exception;

class BankAccountTest extends \PHPUnit_Framework_TestCase {

	public function test_creation_Fail1() {
		$exception = null;
		try {
			$ba = new BankAccount('John Doe', -3);	
		} catch (Exception $e) {
			$exception = $e;
		}
		
		$this->assertEquals("Initial Balance Cannot Be Negative.", $exception->getMessage());
		$this->assertEquals(1001, $exception->getCode());
	}

	public function test_creation_Fail2() {
		$exception = null;
		try {
			$ba = new BankAccount(null, 30);	
		} catch (Exception $e) {
			$exception = $e;
		}
		
		$this->assertEquals("Owner should not be empty.", $exception->getMessage());
		$this->assertEquals(1002, $exception->getCode());
	}

	public function test_creation_OK() {
		$name = 'John Doe';
		$balance = 100;
		$ba = new BankAccount($name, $balance);

		$this->assertEquals($balance, $ba->getBalance());
		$this->assertEquals($name, $ba->getOwner());

		return $ba;
	}

	public function test_getBalance_OK() {
		$ba = new BankAccount('John Doe', 110);

		$this->assertEquals(110, $ba->getBalance());
	}

	public function test_withdraw_OK() {
		$ba = new BankAccount('John Doe', 110);
		$currentBalance = $ba->withdraw(60);

		$this->assertEquals(50, $currentBalance);
		$this->assertEquals(50, $ba->getBalance());
	}

	public function test_withdraw_Fail() {
		$exception = null;
		
		try {
			$ba = new BankAccount('John Doe', 110);
			$ba->withdraw(600);	
		} catch (Exception $e) {
			$exception = $e;
		}
		
		$this->assertEquals("Cannot withdraw more than actual balance:110", $exception->getMessage());
		$this->assertEquals(1003, $exception->getCode());
	}

	public function test_deposit_OK() {
		$ba = new BankAccount('John Doe', 110);
		$ba->deposit(90);

		$this->assertEquals(200, $ba->getBalance());
	}

	public function test_deposit_Fail() {
		$exception = null;
		try {
			$ba = new BankAccount('John Doe', 30);
			$ba->deposit(40000);	
		} catch (Exception $e) {
			$exception = $e;
		}
		
		$this->assertEquals("Deposit amount is bigger than 10000 USD/EUR, needs additional approvers.", $exception->getMessage());
		$this->assertEquals(1004, $exception->getCode());
	}

	/**
	* @depends test_creation_OK
	* @dataProvider data_provider_workflow_1
	*/

	public function test_workflow_1() {
		$args = func_get_args();
		$amountToDeposit = $args[0];
		$expectedBalance = $args[1];
		$ba = $args[2];

		$ba->deposit($args[0]);

		$this->assertEquals($args[1], $ba->getBalance());
	}

	public function data_provider_workflow_1() {
		return array(
			array(100,200), 
			array(100,300),
			array(-90,210),
		);
	}
}

The test class—BankAccountTest—is extending the PHPUnit_Framework_TestCase class. This is a base class that implements methods as assertEquals(), assertTrue(), @depends and @dataProvider annotations.

When using PHPUnit, every test method should have a name starting with the word test. I like to name tests using underscores and adding a final OK/Fail suffix to the test names to indicate which scenarios the test cover. We have tests for successfully creating a new bank account (test_creation_OK), and tests to cover validation logic as well(test_creation_Fail1, test_creation_Fail2).

There is one test (test_workflow_1) which has two annotations, @depends and @dataProvider. The @depends annotation specifies which test it depends on, and which test needs to be executed before this one. The workflow test depends on the test_creation_OK test, which creates and returns a new BankAccount object. The returned value is passed to the dependent test—this is what we are accessing when getting the values from the func_get_args() method.

The @dataProvider annotation specifies a method as a data source for the actual test, and the function can be considered as a data generator for our tests. The test_workflow_1 test will be executed as many times as the number of test data is returned by the data provider function. In this case, it's three times.  

So, when the test_workflow_1 is executed the func_get_args() method will return:

First: 100, 200, BankAccount { balance : 100 }

Second: 100, 300, BankAccount { balance : 200 }

Third: -90, 210, BankAccount { balance : 300 }

The array returned by func_get_args() is populated with the values from the data provider function, followed by the values from dependent tests. By using these two annotations, complex test scenarios and work-flows can be modeled and tested.

To execute the test, we can use the command line:

running unit tests from command line

Using the --testdox-html flag, we can enforce the generation of HTML reports and coverage-html (we will need to have the php5-xdebug package installed) to generate code coverage in HTML format.

code coverage using phpunit

Dipaparkan 11 Jun, 2015

Greg Bogdan

Software Engineer, Blogger, Tech Enthusiast

I am a Software Engineer with over 7 years of experience in different domains(ERP, Financial Products and Alerting Systems). My main expertise is .NET, Java, Python and JavaScript. I like technical writing and have good experience in creating tutorials and how to technical articles. I am passionate about technology and I love what I do and I always intend to 100% fulfill the project which I am ...

Artikel Seterusnya

5 Qualities Startups Look for in New Hires