Unit Testing from
Setup to Deployment
Mark Niebergall
Thank You!
• Entrata - Venue, Food

• JetBrains - 1 free license

• Platform.sh - Zoom
https://analyze.co.za/wp-content/uploads/2018/12/441-1170x500.jpg
Objective
• Be familiar with how to setup PHPUnit

• Familiar with how to test existing code

• Know how to write unit tests using PHPUnit with
Prophecy

• Convince team and management to leverage automated
testing
Overview
• Bene
fi
ts of Unit Testing

• PHPUnit Setup

• Writing Unit Tests

• Testing Existing Code
Bene
fi
ts of Unit Testing
Bene
fi
ts of Unit Testing
public static function add($a, $b)
{
return $a + $b;
}
Bene
fi
ts of Unit Testing
public static function add($a, $b)
{
return $a + $b;
}
public function add(float ...$numbers): float
{
$return = 0;
foreach ($numbers as $value) {
$return = bcadd(
(string) $return,
(string) $value,
10
);
}
return (float) $return;
}
Bene
fi
ts of Unit Testing
http://www.ambysoft.com/artwork/comparingTechniques.jpg
Bene
fi
ts of Unit Testing
• Automated way to test code

- Regression Testing
Bene
fi
ts of Unit Testing
• Automated way to test code

- Continuous Integration

- Continuous Deployment
Bene
fi
ts of Unit Testing
• Automated way to test code

- Other ways to automatically test code beside functional
tests

‣ Behavioral tests (behat)

‣ phpspec (functional)

‣ Selenium (browser automation)

‣ Others?
Bene
fi
ts of Unit Testing
• Decrease bugs introduced with code

- Decreased time to deployment

- Better use of QA team time
Bene
fi
ts of Unit Testing
• Decrease bugs introduced with code

- High con
fi
dence in delivered code
Bene
fi
ts of Unit Testing
• Con
fi
dence when refactoring

- Tests covering code being refactored

- TDD

‣ Change tests

‣ Tests fail

‣ Change code

‣ Tests pass
PHPUnit Setup
PHPUnit Setup
• Install via composer

• Setup `phpunit.xml` for con
fi
guration (if needed)

• Run unit tests
PHPUnit Setup
• phpunit/phpunit

• phpspec/prophecy-phpunit

• fakerphp/faker
PHPUnit Setup
composer require --dev phpunit/phpunit
composer require --dev phpspec/prophecy-phpunit
composer require --dev fakerphp/faker
PHPUnit Setup
• File phpunit.xml

- PHPUnit con
fi
guration for that project

- Documentation: https://phpunit.readthedocs.io/en/9.5/
con
fi
guration.html



<?xml version="1.0" encoding="UTF-8"?>

<phpunit colors="true"
verbose="true"
bootstrap="./tests/Bootstrap.php">
<testsuite name="All Tests">
<directory>./tests</directory>
</testsuite>
</phpunit>
PHPUnit Setup
• Running PHPUnit



vendor/bin/phpunit tests/
PHPUnit 9.5.6 by Sebastian Bergmann and contributors.
Runtime: PHP 8.0.8
Con
fi
guration: /Users/mniebergall/projects/training/phpunit/
phpunit.xml
......... 9 / 9 (100%)
Time: 00:00.032, Memory: 6.00 MB
OK (9 tests, 12 assertions)
PHPUnit Setup
• Running PHPUnit

- Within PhpStorm
PHPUnit Setup
• Directory Structure

- PHP
fi
les in src/

‣ Ex: src/Math/Adder.php

- tests in tests/src/, ‘Test’ at end of
fi
lename

‣ Ex: tests/src/Math/AdderTest.php
Writing Unit Tests
Writing Unit Tests
public function add(float ...$numbers): float
{
$return = 0;
foreach ($numbers as $value) {
$return = bcadd(
(string) $return,
(string) $value,
10
);
}
return (float) $return;
}
Writing Unit Tests
use PHPUnitFrameworkTestCase;
class AdderTest extends TestCase
{
protected Adder $adder;
public function setUp(): void
{
$this->adder = new Adder();
}
public function testAdderWithSetup()
{
$sum = $this->adder->add(3, 7);
$this->assertSame(10.0, $sum);
}
Writing Unit Tests
public function testAdderThrowsExceptionWhenNotANumber()
{
$this->expectException(TypeError::class);
$adder = new Adder();
$adder->add(7, 'Can't add this');
}
Writing Unit Tests
public function testAdderAddsIntegers()
{
$adder = new Adder();
$sum = $adder->add(7, 3, 5, 5, 6, 4, 1, 9);
$this->assertSame(40.0, $sum);
}
public function testAdderAddsDecimals()
{
$adder = new Adder();
$sum = $adder->add(1.5, 0.5);
$this->assertSame(2.0, $sum);
}
Writing Unit Tests
/**
* @dataProvider dataProviderNumbers
*/
public function testAdderAddsNumbers(
float $expectedSum,
...$numbers
) {
$adder = new Adder();
$sum = $adder->add(...$numbers);
$this->assertSame($expectedSum, $sum);
}
public function dataProviderNumbers(): array
{
return [
[2, 1, 1],
[2, 1.5, 0.5],
];
}
Writing Unit Tests
• Test Coverage

- Percent of code covered by tests

- Not aiming for 100%

- No need to test language constructs
Writing Unit Tests
• Code should be self-contained

- No actual database connections

- No API calls should occur

- No external code should be called

‣ Use testing framework
Writing Unit Tests
• Assertions

$this->assertInstanceOf(Response::class, $response);
$this->assertEquals(200, $response->getStatusCode());
$this->assertSame(401, $responseActual->getStatusCode());
$this->assertTrue($dispatched);
$this->assertFalse($sent);
Writing Unit Tests
• Assertions

$this->expectException(RuntimeException::class);
$this->expectExceptionCode(403);
$this->expectExceptionMessage(‘Configuration not found.');
Writing Unit Tests
• Prophecy

- Mock objects

- Expected method calls

- Reveal object when injecting

use PHPUnitFrameworkTestCase;
use ProphecyPhpUnitProphecyTrait;
class RectangleTest extends TestCase
{
use ProphecyTrait;
Writing Unit Tests
• Prophecy

$adderMock = $this->prophesize(Adder::class);
$multiplierMock = $this->prophesize(Multiplier::class);
$adderMock->add($length, $width)
->shouldBeCalled()
->willReturn(10.34001);
$multiplierMock->multiply(2, 10.34001)
->shouldBeCalled()
->willReturn(20.68002);
$rectangle = new Rectangle(
$length,
$width,
$adderMock->reveal(),
$multiplierMock->reveal()
);
Writing Unit Tests
• Prophecy

$dbMock->fetchRow(Argument::any())
->shouldBeCalled()
->willReturn([]);
$asyncBusMock
->dispatch(
Argument::type(DoSomethingCmd::class),
Argument::type('array')
)
->shouldBeCalled()
->willReturn((new Envelope(new stdClass())));
Testing Existing Code
Testing Existing Code
• Problematic Patterns

- Long and complex functions
Testing Existing Code
• Problematic Patterns

- Missing Dependency Injection (DI)

‣ `new Thing();` in functions to be tested
Testing Existing Code
• Problematic Patterns

- exit and die

- print_r

- var_dump

- echo

- other outputs in-line
Testing Existing Code
• Problematic Patterns

- Database interactions

- Resources
Testing Existing Code
• Problematic Patterns

- Out of classes code execution

- Functional code
Testing Existing Code
• Helpful Patterns

- Unit testing promotes good code patterns
Testing Existing Code
• Helpful Patterns

- Dependency Injection

- Classes and functions focused on one thing

- Abstraction

- Interfaces

- Clean code
Testing Existing Code
• Helpful Patterns

- Code that is SOLID

‣ Single-responsibility: should be open for extension,
but closed for modi
fi
cation

‣ Open-closed: should be open for extension, but
closed for modi
fi
cation

‣ Liskov substitution: in PHP, use interface/abstract

‣ Interface segregation: Many client-speci
fi
c interfaces
are better than one general-purpose interface

‣ Dependency inversion: Depend upon abstractions,
[not] concretions
Testing Existing Code
• Helpful Patterns

- KISS
Testing Existing Code
• Test Coverage

- Percent of code covered by tests

- Not aiming for 100%

- No need to test language constructs

- Outputs report in various formats

- Uses Xdebug as driver
Testing Existing Code
class ShapeService
{
public function create(string $shape): int
{
$db = new Db();
return $db->insert('shape', ['shape' => $shape]);
}
public function smsArea(Rectangle $shape, string $toNumber): bool
{
$sms = new Sms([
'api_uri' => 'https://example.com/sms',
'api_key' => 'alkdjfoasifj0392lkdsjf',
]);
$sent = $sms->send($toNumber, 'Area is ' . $shape->area());
(new Logger())
->log('Sms sent to ' . $toNumber . ': Area is ' . $shape->area());
return $sent;
}
}
Testing Existing Code
class ShapeService
{
public function create(string $shape): int
{
$db = new Db();
return $db->insert('shape', ['shape' => $shape]);
}
public function smsArea(Rectangle $shape, string $toNumber): bool
{
$sms = new Sms([
'api_uri' => 'https://example.com/sms',
'api_key' => 'alkdjfoasifj0392lkdsjf',
]);
$sent = $sms->send($toNumber, 'Area is ' . $shape->area());
(new Logger())
->log('Sms sent to ' . $toNumber . ': Area is ' . $shape->area());
return $sent;
}
}
Testing Existing Code
class ShapeServiceCleanedUp
{
public function __construct(
protected Db $db,
protected Sms $sms
) {}
public function create(string $shape): int
{
return $this->db->insert('shape', ['shape' => $shape]);
}
public function smsArea(ShapeInterface $shape, string $toNumber): bool
{
$area = $shape->area();
return $this->sms->send($toNumber, 'Area is ' . $area);
}
Testing Existing Code
use ProphecyTrait;
protected Generator $faker;
public function setUp(): void
{
$this->faker = Factory::create();
}
public function testCreate()
{
$dbMock = $this->prophesize(Db::class);
$smsMock = $this->prophesize(Sms::class);
$shape = $this->faker->word;
$dbMock->insert('shape', ['shape' => $shape])
->shouldBeCalled()
->willReturn(1);
$shapeServiceCleanedUp = new ShapeServiceCleanedUp(
$dbMock->reveal(),
$smsMock->reveal()
);
$shapeServiceCleanedUp->create($shape);
}
Testing Existing Code
public function testSmsArea()
{
$dbMock = $this->prophesize(Db::class);
$smsMock = $this->prophesize(Sms::class);
$shapeMock = $this->prophesize(ShapeInterface::class);
$area = $this->faker->randomFloat();
$shapeMock->area()
->shouldBeCalled()
->willReturn($area);
$toNumber = $this->faker->phoneNumber;
$smsMock->send($toNumber, 'Area is ' . $area)
->shouldBeCalled()
->willReturn(1);
$shapeServiceCleanedUp = new ShapeServiceCleanedUp(
$dbMock->reveal(),
$smsMock->reveal()
);
$shapeServiceCleanedUp->smsArea(
$shapeMock->reveal(),
$toNumber
);
}
Live Examples
Discussion Items
• Convincing Teammates

• Convincing Management
Discussion Items
• Does unit testing slow development down?
Discussion Items
• “I don’t see the bene
fi
t of unit testing”
Discussion Items
• Unit tests for legacy code
Discussion Items
• Other?
Review
• Bene
fi
ts of Unit Testing

• PHPUnit Setup

• Writing Unit Tests

• Testing Existing Code
Unit Testing from Setup to Deployment
• Questions?
Mark Niebergall @mbniebergall
• PHP since 2005

• Masters degree in MIS

• Senior Software Engineer

• Drug screening project

• Utah PHP Co-Organizer

• CSSLP, SSCP Certi
fi
ed and SME

• Endurance sports, outdoors
References
• https://analyze.co.za/wp-content/uploads/
2018/12/441-1170x500.jpg

• http://www.ambysoft.com/artwork/
comparingTechniques.jpg

• https://en.wikipedia.org/wiki/SOLID

Unit Testing from Setup to Deployment

  • 1.
    Unit Testing from Setupto Deployment Mark Niebergall
  • 2.
    Thank You! • Entrata- Venue, Food • JetBrains - 1 free license • Platform.sh - Zoom
  • 3.
  • 4.
    Objective • Be familiarwith how to setup PHPUnit • Familiar with how to test existing code • Know how to write unit tests using PHPUnit with Prophecy • Convince team and management to leverage automated testing
  • 5.
    Overview • Bene fi ts ofUnit Testing • PHPUnit Setup • Writing Unit Tests • Testing Existing Code
  • 6.
  • 7.
    Bene fi ts of UnitTesting public static function add($a, $b) { return $a + $b; }
  • 8.
    Bene fi ts of UnitTesting public static function add($a, $b) { return $a + $b; } public function add(float ...$numbers): float { $return = 0; foreach ($numbers as $value) { $return = bcadd( (string) $return, (string) $value, 10 ); } return (float) $return; }
  • 9.
    Bene fi ts of UnitTesting http://www.ambysoft.com/artwork/comparingTechniques.jpg
  • 10.
    Bene fi ts of UnitTesting • Automated way to test code - Regression Testing
  • 11.
    Bene fi ts of UnitTesting • Automated way to test code - Continuous Integration - Continuous Deployment
  • 12.
    Bene fi ts of UnitTesting • Automated way to test code - Other ways to automatically test code beside functional tests ‣ Behavioral tests (behat) ‣ phpspec (functional) ‣ Selenium (browser automation) ‣ Others?
  • 13.
    Bene fi ts of UnitTesting • Decrease bugs introduced with code - Decreased time to deployment - Better use of QA team time
  • 14.
    Bene fi ts of UnitTesting • Decrease bugs introduced with code - High con fi dence in delivered code
  • 15.
    Bene fi ts of UnitTesting • Con fi dence when refactoring - Tests covering code being refactored - TDD ‣ Change tests ‣ Tests fail ‣ Change code ‣ Tests pass
  • 16.
  • 17.
    PHPUnit Setup • Installvia composer • Setup `phpunit.xml` for con fi guration (if needed) • Run unit tests
  • 18.
    PHPUnit Setup • phpunit/phpunit •phpspec/prophecy-phpunit • fakerphp/faker
  • 19.
    PHPUnit Setup composer require--dev phpunit/phpunit composer require --dev phpspec/prophecy-phpunit composer require --dev fakerphp/faker
  • 20.
    PHPUnit Setup • Filephpunit.xml - PHPUnit con fi guration for that project - Documentation: https://phpunit.readthedocs.io/en/9.5/ con fi guration.html 
 <?xml version="1.0" encoding="UTF-8"?> <phpunit colors="true" verbose="true" bootstrap="./tests/Bootstrap.php"> <testsuite name="All Tests"> <directory>./tests</directory> </testsuite> </phpunit>
  • 21.
    PHPUnit Setup • RunningPHPUnit 
 vendor/bin/phpunit tests/ PHPUnit 9.5.6 by Sebastian Bergmann and contributors. Runtime: PHP 8.0.8 Con fi guration: /Users/mniebergall/projects/training/phpunit/ phpunit.xml ......... 9 / 9 (100%) Time: 00:00.032, Memory: 6.00 MB OK (9 tests, 12 assertions)
  • 22.
    PHPUnit Setup • RunningPHPUnit - Within PhpStorm
  • 23.
    PHPUnit Setup • DirectoryStructure - PHP fi les in src/ ‣ Ex: src/Math/Adder.php - tests in tests/src/, ‘Test’ at end of fi lename ‣ Ex: tests/src/Math/AdderTest.php
  • 24.
  • 25.
    Writing Unit Tests publicfunction add(float ...$numbers): float { $return = 0; foreach ($numbers as $value) { $return = bcadd( (string) $return, (string) $value, 10 ); } return (float) $return; }
  • 26.
    Writing Unit Tests usePHPUnitFrameworkTestCase; class AdderTest extends TestCase { protected Adder $adder; public function setUp(): void { $this->adder = new Adder(); } public function testAdderWithSetup() { $sum = $this->adder->add(3, 7); $this->assertSame(10.0, $sum); }
  • 27.
    Writing Unit Tests publicfunction testAdderThrowsExceptionWhenNotANumber() { $this->expectException(TypeError::class); $adder = new Adder(); $adder->add(7, 'Can't add this'); }
  • 28.
    Writing Unit Tests publicfunction testAdderAddsIntegers() { $adder = new Adder(); $sum = $adder->add(7, 3, 5, 5, 6, 4, 1, 9); $this->assertSame(40.0, $sum); } public function testAdderAddsDecimals() { $adder = new Adder(); $sum = $adder->add(1.5, 0.5); $this->assertSame(2.0, $sum); }
  • 29.
    Writing Unit Tests /** *@dataProvider dataProviderNumbers */ public function testAdderAddsNumbers( float $expectedSum, ...$numbers ) { $adder = new Adder(); $sum = $adder->add(...$numbers); $this->assertSame($expectedSum, $sum); } public function dataProviderNumbers(): array { return [ [2, 1, 1], [2, 1.5, 0.5], ]; }
  • 30.
    Writing Unit Tests •Test Coverage - Percent of code covered by tests - Not aiming for 100% - No need to test language constructs
  • 31.
    Writing Unit Tests •Code should be self-contained - No actual database connections - No API calls should occur - No external code should be called ‣ Use testing framework
  • 32.
    Writing Unit Tests •Assertions $this->assertInstanceOf(Response::class, $response); $this->assertEquals(200, $response->getStatusCode()); $this->assertSame(401, $responseActual->getStatusCode()); $this->assertTrue($dispatched); $this->assertFalse($sent);
  • 33.
    Writing Unit Tests •Assertions $this->expectException(RuntimeException::class); $this->expectExceptionCode(403); $this->expectExceptionMessage(‘Configuration not found.');
  • 34.
    Writing Unit Tests •Prophecy - Mock objects - Expected method calls - Reveal object when injecting use PHPUnitFrameworkTestCase; use ProphecyPhpUnitProphecyTrait; class RectangleTest extends TestCase { use ProphecyTrait;
  • 35.
    Writing Unit Tests •Prophecy $adderMock = $this->prophesize(Adder::class); $multiplierMock = $this->prophesize(Multiplier::class); $adderMock->add($length, $width) ->shouldBeCalled() ->willReturn(10.34001); $multiplierMock->multiply(2, 10.34001) ->shouldBeCalled() ->willReturn(20.68002); $rectangle = new Rectangle( $length, $width, $adderMock->reveal(), $multiplierMock->reveal() );
  • 36.
    Writing Unit Tests •Prophecy $dbMock->fetchRow(Argument::any()) ->shouldBeCalled() ->willReturn([]); $asyncBusMock ->dispatch( Argument::type(DoSomethingCmd::class), Argument::type('array') ) ->shouldBeCalled() ->willReturn((new Envelope(new stdClass())));
  • 37.
  • 38.
    Testing Existing Code •Problematic Patterns - Long and complex functions
  • 39.
    Testing Existing Code •Problematic Patterns - Missing Dependency Injection (DI) ‣ `new Thing();` in functions to be tested
  • 40.
    Testing Existing Code •Problematic Patterns - exit and die - print_r - var_dump - echo - other outputs in-line
  • 41.
    Testing Existing Code •Problematic Patterns - Database interactions - Resources
  • 42.
    Testing Existing Code •Problematic Patterns - Out of classes code execution - Functional code
  • 43.
    Testing Existing Code •Helpful Patterns - Unit testing promotes good code patterns
  • 44.
    Testing Existing Code •Helpful Patterns - Dependency Injection - Classes and functions focused on one thing - Abstraction - Interfaces - Clean code
  • 45.
    Testing Existing Code •Helpful Patterns - Code that is SOLID ‣ Single-responsibility: should be open for extension, but closed for modi fi cation ‣ Open-closed: should be open for extension, but closed for modi fi cation ‣ Liskov substitution: in PHP, use interface/abstract ‣ Interface segregation: Many client-speci fi c interfaces are better than one general-purpose interface ‣ Dependency inversion: Depend upon abstractions, [not] concretions
  • 46.
    Testing Existing Code •Helpful Patterns - KISS
  • 47.
    Testing Existing Code •Test Coverage - Percent of code covered by tests - Not aiming for 100% - No need to test language constructs - Outputs report in various formats - Uses Xdebug as driver
  • 48.
    Testing Existing Code classShapeService { public function create(string $shape): int { $db = new Db(); return $db->insert('shape', ['shape' => $shape]); } public function smsArea(Rectangle $shape, string $toNumber): bool { $sms = new Sms([ 'api_uri' => 'https://example.com/sms', 'api_key' => 'alkdjfoasifj0392lkdsjf', ]); $sent = $sms->send($toNumber, 'Area is ' . $shape->area()); (new Logger()) ->log('Sms sent to ' . $toNumber . ': Area is ' . $shape->area()); return $sent; } }
  • 49.
    Testing Existing Code classShapeService { public function create(string $shape): int { $db = new Db(); return $db->insert('shape', ['shape' => $shape]); } public function smsArea(Rectangle $shape, string $toNumber): bool { $sms = new Sms([ 'api_uri' => 'https://example.com/sms', 'api_key' => 'alkdjfoasifj0392lkdsjf', ]); $sent = $sms->send($toNumber, 'Area is ' . $shape->area()); (new Logger()) ->log('Sms sent to ' . $toNumber . ': Area is ' . $shape->area()); return $sent; } }
  • 50.
    Testing Existing Code classShapeServiceCleanedUp { public function __construct( protected Db $db, protected Sms $sms ) {} public function create(string $shape): int { return $this->db->insert('shape', ['shape' => $shape]); } public function smsArea(ShapeInterface $shape, string $toNumber): bool { $area = $shape->area(); return $this->sms->send($toNumber, 'Area is ' . $area); }
  • 51.
    Testing Existing Code useProphecyTrait; protected Generator $faker; public function setUp(): void { $this->faker = Factory::create(); } public function testCreate() { $dbMock = $this->prophesize(Db::class); $smsMock = $this->prophesize(Sms::class); $shape = $this->faker->word; $dbMock->insert('shape', ['shape' => $shape]) ->shouldBeCalled() ->willReturn(1); $shapeServiceCleanedUp = new ShapeServiceCleanedUp( $dbMock->reveal(), $smsMock->reveal() ); $shapeServiceCleanedUp->create($shape); }
  • 52.
    Testing Existing Code publicfunction testSmsArea() { $dbMock = $this->prophesize(Db::class); $smsMock = $this->prophesize(Sms::class); $shapeMock = $this->prophesize(ShapeInterface::class); $area = $this->faker->randomFloat(); $shapeMock->area() ->shouldBeCalled() ->willReturn($area); $toNumber = $this->faker->phoneNumber; $smsMock->send($toNumber, 'Area is ' . $area) ->shouldBeCalled() ->willReturn(1); $shapeServiceCleanedUp = new ShapeServiceCleanedUp( $dbMock->reveal(), $smsMock->reveal() ); $shapeServiceCleanedUp->smsArea( $shapeMock->reveal(), $toNumber ); }
  • 53.
  • 54.
    Discussion Items • ConvincingTeammates • Convincing Management
  • 55.
    Discussion Items • Doesunit testing slow development down?
  • 56.
    Discussion Items • “Idon’t see the bene fi t of unit testing”
  • 57.
    Discussion Items • Unittests for legacy code
  • 58.
  • 59.
    Review • Bene fi ts ofUnit Testing • PHPUnit Setup • Writing Unit Tests • Testing Existing Code
  • 60.
    Unit Testing fromSetup to Deployment • Questions?
  • 61.
    Mark Niebergall @mbniebergall •PHP since 2005 • Masters degree in MIS • Senior Software Engineer • Drug screening project • Utah PHP Co-Organizer • CSSLP, SSCP Certi fi ed and SME • Endurance sports, outdoors
  • 62.