Practical Approach for testing your software with PHPUnit
Mario Bittencourt
Agenda
Current situation
PHPUnit as part of the solution
Best Practices
Current situation
Complexity, dependencies and pace over time
Software Development Life Cycle
Testing
Tedious
Can only cover a small subset of the use cases
Takes a lot of time
Change the code to test it
Automation is key
Goal is to create tests that can be executed automatically to verify that the expected behaviour is met
PHPUnit as part of the solution
Example
class Account
{
protected $balance;
public function __construct($initialAmount)
{
$this->balance = $initialAmount;
}
public function getBalance()
{
return $this->balance;
}
public function deposit($amount)
{
$this->balance += $amount;
}
}
Unit Test
class AccountTest extends PHpUnit_Framework_TestCase{
public function testDepositIncreasesBalance(){
$account = new Account(10);$account->deposit(1);$this->assertEquals(11, $account->getBalance());
}}
Unit Test
PHPUnit 4.3.1 by Sebastian Bergmann.
Account
[x] Deposit increases balance
Best Practices
Give meaningful names to your tests
public function testDeposit() {
$account = new Account(10);$account->deposit(1);$this->assertEquals(
11,$account->getBalance()
);}
DON’T
Give meaningful names to your tests
PHPUnit 4.3.1 by Sebastian Bergmann.
Account
[ ] Deposit
Give meaningful names to your tests
public function testDepositIncreaseBalance() {
$account = new Account(10);$account->deposit(1);$this->assertEquals(
11,$account->getBalance()
);}
Use the most specific assertion available
public function testDepositIncreasesBalance(){
$account = new Account(10);$account->deposit(10);$this->assertGreaterThan(
10,$account->getBalance());
}
DON’T
Use the most specific assertion available
public function testDepositIncreasesBalance(){
$account = new Account(10);$account->deposit(10);$this->assertEquals(20, $account->getBalance());
}
Use the most specific assertion available
public function deposit($amount){
$this->balance += $amount;
if ($amount >= 10) {$this->balance += 1;
}
return true;}
Use the most specific assertion available
There was 1 failure:
1) AccountTest::testDepositIncreasesBalanceFailed asserting that 21 matches expected 20.
AccountTest.php:14
Don’t forget about protectedmethods
class Account{
public function deposit($amount){
if ($this->validateAmount($amount)) {$this->balance += $amount;
} else {throw new Exception('Invalid amount');
}}
}
Don't forget about your protected methods
protected function validateAmount($amount){
if ($amount < 0) {return false;
} else {return true;
}}
Don't forget about your protected methods
public function testValidateAmountWithNegativeIsFalse(){
$account = new Account(10);$this->assertFalse($account->validateAmount(-1));
}
Fatal error: Call to protected method Account::validateAmount()
Don't forget about your protected methods
public function testValidateAmountWithNegativeIsFalse(){
$account = new Account(10);$reflection = new ReflectionObject($account);$method = $reflection->getMethod('validateAmount');$method->setAccessible(true);$this->assertFalse(
$method->invokeArgs($account, array(-1)));
}
Don't forget about your protected methods
PHPUnit 4.3.1 by Sebastian Bergmann.
Account
[x] Deposit increases balance
[x] Validate amount with negative is false
Test just one thing at a time
public function deposit($amount){
if ($amount == 0) {return false;
}$this->balance += $amount;return true;
}
Test just one thing at a time
public function testDepositIncreasesBalance(){
$account = new Account(10);$this->assertTrue($account->deposit(1));$this->assertEquals(11, $account->getBalance());
$this->assertFalse($account->deposit(0));}
Test just one thing at a time
PHPUnit 4.3.1 by Sebastian Bergmann.
.
Time: 35 ms, Memory: 3.00Mb
OK (1 test, 3 assertions)
Test just one thing at a time
public function deposit($amount){
if ($amount < 0) {return false;
}$this->balance += $amount;return true;
}
NEW CRITERIA
Test just one thing at a time
PHPUnit 4.3.1 by Sebastian Bergmann.
There was 1 failure:
1) AccountTest::testDepositIncreasesBalance
Failed asserting that true is false.
AccountTest.php:15
FAILURES!
Tests: 1, Assertions: 3, Failures: 1.
Test just one thing at a time
public function testDepositIncreasesBalance(){$account = new Account(10);$this->assertTrue($account->deposit(1));$this->assertEquals(11, $account->getBalance());}
public function testDepositWithNegativeShouldReturnFalse(){
$account = new Account(10);$this->assertFalse($account->deposit(-1));
}
Test just one thing at a time
PHPUnit 4.3.1 by Sebastian Bergmann.
..
Time: 49 ms, Memory: 3.00Mb
OK (2 tests, 3 assertions)
Use Data Providers for repetitive tests
class Score{
protected $score;
public function update($balance, $numberCc, $amountInLoans)
{// do something ...return $newScore;
}}
Use Data Providers for repetitive tests
class ScoreTest extends PHpUnit_Framework_TestCase{
public function testScoreSomething1(){
$score = new Score(10);$this->assertEquals(300, $score->update(100, 0, 1));
}
public function testScoreSomething2(){
$score = new Score(10);$this->assertEquals(99, $score->update(100, 1, 2));
}}
Use Data Providers for repetitive tests
/*** @dataProvider scoreProvider*/public function testScore($balance, $numberCc, $amountInLoans, $expectedScore){
$score = new Score(10);$this->assertEquals(
$expectedScore, $score->update($balance, $numberCc, $amountInLoans)
);}
Use Data Providers for repetitive tests
public function scoreProvider(){
return array(array(100, 0, 1, 300),array(100, 1, 2, 99),
);}
Use Data Providers for repetitive tests
PHPUnit 4.3.1 by Sebastian Bergmann.
..
Time: 25 ms, Memory: 3.00Mb
OK (2 tests, 2 assertions)
Isolate your test
public function wire($amount, SwiftInterface $targetBank){
if ($targetBank->transfer($amount)) {$this->balance -= $amount;return true;
} else {return false;
}}
Isolate your test
class Hsbc implements SwiftInterface {public function transfer($amount){
// slow method}
}
Isolate your test
public function testWireTransferShouldReturnTrueIfSuccessful(){
$account = new Account(10);$bank = new Hsbc();$this->assertTrue($account->wire(5, $bank));
}
Isolate your test
public function testWireTransferShouldReturnFalseIfFailed(){
// how to fail?$account = new Account(10);$bank = new Hsbc();$this->assertFalse($account->wire(5, $bank));
}
Isolate your test
public function testWireTransferShouldReturnTrueIfSuccessful(){
$account = new Account(10);$bank = $this->getMock(‘Hsbc');$bank->expects($this->any())
->method('transfer')->will($this->returnValue(true));
$this->assertTrue($account->wire(5, $bank));}
Isolate your test
public function testWireTransferShouldReturnFalseIfFailed(){
$account = new Account(10);$bank = $this->getMock('Hsbc');$bank->expects($this->any())
->method('transfer')->will($this->returnValue(false));
$this->assertFalse($account->wire(5, $bank));}
Specify what you are testing with @covers
Use --coverage-html to generate the code coverage report of your code
Specify what you are testing with @covers
Specify what you are testing with @covers
class Exchange {public function convert($amountUsd, $targetCurrency){
// Do some conversion}
}
Specify what you are testing with @covers
public function convertBalance($targetCurrency){
$exchange = new Exchange();return $exchange->convert($this->balance, $targetCurrency);
}
Specify what you are testing with @covers
public function testConvertBalance(){
$account = new Account(10);$this->assertLessThan(
10, $account->convertBalance('GBP'));
}
Specify what you are testing with @covers
Specify what you are testing with @covers
Specify what you are testing with @covers
/**
* @covers Account::convertBalance
*/
public function testConvertBalance(){…
}
Specify what you are testing with @covers
That is all folks!
Contacts
Twitter - @bicatu
Email – [email protected]
https://joind.in/12279
http://slidesha.re/1xtcT4y