Leveling Up With
Unit Testing
Mark Niebergall
https://joind.in/talk/906c6
👏 Thank You!
• LonghornPHP Organizers, Sponsors
- Nucleus Security
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, Mockery
✓ 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 (CI)
- Continuous Deployment (CD)
😀 Bene
fi
ts of Unit Testing
• 💻 Automated way to test code
- Other ways to automatically test code
‣ Behavioral (BDD): behat, phpspec
‣ Functional
‣ Acceptance: Selenium
‣ 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
• mockery/mockery
• fakerphp/faker
⚙ PHPUnit Setup
composer require --dev phpunit/phpunit
composer require --dev phpspec/prophecy-phpunit
composer require --dev mockery/mockery
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 Setup
• 💻 Running PHPUnit
vendor/bin/phpunit tests/
PHPUnit 9.5.25 by Sebastian Bergmann and contributors.
Runtime: PHP 8.1.10
Con
fi
guration: /Users/mniebergall/projects/phpunit/phpunit.xml
........... 11 / 11 (100%)
Time: 00:00.025, Memory: 8.00 MB
OK (11 tests, 15 assertions)
⚙ PHPUnit Setup
• 💻 Running PHPUnit
- Within PhpStorm
- https://www.jetbrains.com/help/phpstorm/using-
phpunit-framework.html
⚙ 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
/**
* @dataProvider dataProviderNumbers
*/
public function testAdderAddsNumbers(
float $expectedSum,
...$numbers
) {
$adder = new Adder();
$sum = $adder->add(...$numbers);
$this->assertSame($expectedSum, $sum);
}
public function dataProviderNumbers(): iterable
{
yield 'integer' => [2, 1, 1];
yield 'integers with decimals' => [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
• ⛔ 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
• Mocking
- Built-in Mock Objects
- Prophecy
- Mockery
🧑💻 Writing Unit Tests
• ⚠ Scratching the surface
- Dive deep into each
- See what you like, what works
- Leverage a mix
🧑💻 Writing Unit Tests
• Built-in Mock Objects
- Only mock certain methods, let others run as-is
- Pattern: ->method(…)->with($arg)->willReturn($value)
use PHPUnitFrameworkTestCase;
class RectangleTest extends TestCase
🧑💻 Writing Unit Tests
• Built-in Mock Objects
$adderMock = $this->getMockBuilder(Adder::class)
->onlyMethods(['add'])
->getMock();
$adderMock->method('add')
->with($length, $width)
->willReturn(10.34001);
$multiplierMock = $this->getMockBuilder(Multiplier::class)
->onlyMethods(['multiply'])
->getMock();
$multiplierMock->method('multiply')
->with(2, 10.34001)
->willReturn(20.68002);
$rectangle = new Rectangle(
$length,
$width,
$adderMock,
$multiplierMock
);
🧑💻 Writing Unit Tests
• Prophecy
- Pattern: ->shouldBeCalled()->willReturn($value)
🧑💻 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())));
🧑💻 Writing Unit Tests
• Mockery
- Similar style to built-in
🧑💻 Writing Unit Tests
• Mockery
$adderMock = Mockery::mock(Adder::class);
$multiplierMock = Mockery::mock(Multiplier::class);
$adderMock->shouldReceive('add')
->with($length, $width)
->andReturn(10.34001);
$multiplierMock->shouldReceive('multiply')
->with(2, 10.34001)
->andReturn(20.68002);
$rectangle = new Rectangle(
$length,
$width,
$adderMock,
$multiplierMock
);
⌨ 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
- die
- print_r
- var_dump
- echo
- other outputs in-line
‣ Hint: use expectOutputString
⌨ Testing Existing Code
• ⚠ Problematic Patterns
- Time-sensitive
- sleep
⌨ Testing Existing Code
• ⚠ Problematic Patterns
- Database interactions
- Resources
⌨ Testing Existing Code
• ⚠ Problematic Patterns
- Out of class 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: every class should have only
one responsibility
‣ 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
- DDD
⌨ Testing Existing Code
• ✅ Helpful Patterns
- KISS
- DRY
- YAGNI
⌨ 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 ShapeService
{
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
);
}
💬 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?
• https://joind.in/talk/906c6
Mark Niebergall @mbniebergall
• PHP since 2005
• Masters degree in MIS
• Senior Software Engineer
• Vulnerability Management project (security scans)
• Utah PHP Co-Organizer
• CSSLP, SSCP Certi
fi
ed and Exam Developer
• 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

Leveling Up With Unit Testing - LonghornPHP 2022

  • 1.
    Leveling Up With UnitTesting Mark Niebergall https://joind.in/talk/906c6
  • 2.
    👏 Thank You! •LonghornPHP Organizers, Sponsors - Nucleus Security
  • 4.
  • 5.
    ✔ Objective ✓ Befamiliar with how to setup PHPUnit ✓ Familiar with how to test existing code ✓ Know how to write unit tests using PHPUnit with Prophecy, Mockery ✓ Convince team and management to leverage automated testing
  • 6.
    👀 Overview • 😀Bene fi ts of Unit Testing • ⚙ PHPUnit Setup • 🧑💻 Writing Unit Tests • ⌨ Testing Existing Code
  • 7.
    😀 Bene fi ts ofUnit Testing
  • 8.
    😀 Bene fi ts ofUnit Testing public static function add($a, $b) { return $a + $b; }
  • 9.
    😀 Bene fi ts ofUnit 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; }
  • 10.
    😀 Bene fi ts ofUnit Testing http://www.ambysoft.com/artwork/comparingTechniques.jpg
  • 11.
    😀 Bene fi ts ofUnit Testing • 💻 Automated way to test code - Regression Testing
  • 12.
    😀 Bene fi ts ofUnit Testing • 💻 Automated way to test code - Continuous Integration (CI) - Continuous Deployment (CD)
  • 13.
    😀 Bene fi ts ofUnit Testing • 💻 Automated way to test code - Other ways to automatically test code ‣ Behavioral (BDD): behat, phpspec ‣ Functional ‣ Acceptance: Selenium ‣ Others?
  • 14.
    😀 Bene fi ts ofUnit Testing • 🪲 Decrease bugs introduced with code - Decreased time to deployment - Better use of QA team time
  • 15.
    😀 Bene fi ts ofUnit Testing • 🪲 Decrease bugs introduced with code - High con fi dence in delivered code
  • 16.
    😀 Bene fi ts ofUnit Testing • 💯 Con fi dence when refactoring - Tests covering code being refactored - TDD ‣ Change tests ‣ Tests fail ‣ Change code ‣ Tests pass
  • 17.
  • 18.
    ⚙ PHPUnit Setup •Install via composer • Setup `phpunit.xml` for con fi guration (if needed) • Run unit tests
  • 19.
    ⚙ PHPUnit Setup •phpunit/phpunit • phpspec/prophecy-phpunit • mockery/mockery • fakerphp/faker
  • 20.
    ⚙ PHPUnit Setup composerrequire --dev phpunit/phpunit composer require --dev phpspec/prophecy-phpunit composer require --dev mockery/mockery composer require --dev fakerphp/faker
  • 21.
    ⚙ 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>
  • 22.
    ⚙ PHPUnit Setup •💻 Running PHPUnit vendor/bin/phpunit tests/
  • 23.
    ⚙ PHPUnit Setup •💻 Running PHPUnit vendor/bin/phpunit tests/ PHPUnit 9.5.25 by Sebastian Bergmann and contributors. Runtime: PHP 8.1.10 Con fi guration: /Users/mniebergall/projects/phpunit/phpunit.xml ........... 11 / 11 (100%) Time: 00:00.025, Memory: 8.00 MB OK (11 tests, 15 assertions)
  • 24.
    ⚙ PHPUnit Setup •💻 Running PHPUnit - Within PhpStorm - https://www.jetbrains.com/help/phpstorm/using- phpunit-framework.html
  • 25.
    ⚙ 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
  • 26.
  • 27.
    🧑💻 Writing UnitTests public function add(float ...$numbers): float { $return = 0; foreach ($numbers as $value) { $return = bcadd( (string) $return, (string) $value, 10 ); } return (float) $return; }
  • 28.
    🧑💻 Writing UnitTests 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); }
  • 29.
    🧑💻 Writing UnitTests public function testAdderThrowsExceptionWhenNotANumber() { $this->expectException(TypeError::class); $adder = new Adder(); $adder->add(7, 'Can't add this'); }
  • 30.
    🧑💻 Writing UnitTests 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); }
  • 31.
    🧑💻 Writing UnitTests /** * @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], ]; }
  • 32.
    🧑💻 Writing UnitTests /** * @dataProvider dataProviderNumbers */ public function testAdderAddsNumbers( float $expectedSum, ...$numbers ) { $adder = new Adder(); $sum = $adder->add(...$numbers); $this->assertSame($expectedSum, $sum); } public function dataProviderNumbers(): iterable { yield 'integer' => [2, 1, 1]; yield 'integers with decimals' => [2, 1.5, 0.5]; }
  • 33.
    🧑💻 Writing UnitTests • 📏 Test Coverage - Percent of code covered by tests - Not aiming for 100% - No need to test language constructs
  • 34.
    🧑💻 Writing UnitTests • ⛔ Self-contained - No actual database connections - No API calls should occur - No external code should be called ‣ Use testing framework
  • 35.
    🧑💻 Writing UnitTests • ✅ Assertions $this->assertInstanceOf(Response::class, $response); $this->assertEquals(200, $response->getStatusCode()); $this->assertSame(401, $responseActual->getStatusCode()); $this->assertTrue($dispatched); $this->assertFalse($sent);
  • 36.
    🧑💻 Writing UnitTests • ✅ Assertions $this->expectException(RuntimeException::class); $this->expectExceptionCode(403); $this->expectExceptionMessage(‘Configuration not found.');
  • 37.
    🧑💻 Writing UnitTests • Mocking - Built-in Mock Objects - Prophecy - Mockery
  • 38.
    🧑💻 Writing UnitTests • ⚠ Scratching the surface - Dive deep into each - See what you like, what works - Leverage a mix
  • 39.
    🧑💻 Writing UnitTests • Built-in Mock Objects - Only mock certain methods, let others run as-is - Pattern: ->method(…)->with($arg)->willReturn($value) use PHPUnitFrameworkTestCase; class RectangleTest extends TestCase
  • 40.
    🧑💻 Writing UnitTests • Built-in Mock Objects $adderMock = $this->getMockBuilder(Adder::class) ->onlyMethods(['add']) ->getMock(); $adderMock->method('add') ->with($length, $width) ->willReturn(10.34001); $multiplierMock = $this->getMockBuilder(Multiplier::class) ->onlyMethods(['multiply']) ->getMock(); $multiplierMock->method('multiply') ->with(2, 10.34001) ->willReturn(20.68002); $rectangle = new Rectangle( $length, $width, $adderMock, $multiplierMock );
  • 41.
    🧑💻 Writing UnitTests • Prophecy - Pattern: ->shouldBeCalled()->willReturn($value)
  • 42.
    🧑💻 Writing UnitTests • Prophecy - Mock objects - Expected method calls - Reveal object when injecting use PHPUnitFrameworkTestCase; use ProphecyPhpUnitProphecyTrait; class RectangleTest extends TestCase { use ProphecyTrait;
  • 43.
    🧑💻 Writing UnitTests • 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() );
  • 44.
    🧑💻 Writing UnitTests • Prophecy $dbMock->fetchRow(Argument::any()) ->shouldBeCalled() ->willReturn([]); $asyncBusMock ->dispatch( Argument::type(DoSomethingCmd::class), Argument::type('array') ) ->shouldBeCalled() ->willReturn((new Envelope(new stdClass())));
  • 45.
    🧑💻 Writing UnitTests • Mockery - Similar style to built-in
  • 46.
    🧑💻 Writing UnitTests • Mockery $adderMock = Mockery::mock(Adder::class); $multiplierMock = Mockery::mock(Multiplier::class); $adderMock->shouldReceive('add') ->with($length, $width) ->andReturn(10.34001); $multiplierMock->shouldReceive('multiply') ->with(2, 10.34001) ->andReturn(20.68002); $rectangle = new Rectangle( $length, $width, $adderMock, $multiplierMock );
  • 47.
  • 48.
    ⌨ Testing ExistingCode • ⚠ Problematic Patterns - Long and complex functions
  • 49.
    ⌨ Testing ExistingCode • ⚠ Problematic Patterns - Missing Dependency Injection (DI) ‣ `new Thing();` in functions to be tested
  • 50.
    ⌨ Testing ExistingCode • ⚠ Problematic Patterns - exit - die - print_r - var_dump - echo - other outputs in-line ‣ Hint: use expectOutputString
  • 51.
    ⌨ Testing ExistingCode • ⚠ Problematic Patterns - Time-sensitive - sleep
  • 52.
    ⌨ Testing ExistingCode • ⚠ Problematic Patterns - Database interactions - Resources
  • 53.
    ⌨ Testing ExistingCode • ⚠ Problematic Patterns - Out of class code execution - Functional code
  • 54.
    ⌨ Testing ExistingCode • ✅ Helpful Patterns - Unit testing promotes good code patterns
  • 55.
    ⌨ Testing ExistingCode • ✅ Helpful Patterns - Dependency Injection - Classes and functions focused on one thing - Abstraction - Interfaces - Clean code
  • 56.
    ⌨ Testing ExistingCode • ✅ Helpful Patterns - Code that is SOLID ‣ Single-responsibility: every class should have only one responsibility ‣ 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
  • 57.
    ⌨ Testing ExistingCode • ✅ Helpful Patterns - DDD
  • 58.
    ⌨ Testing ExistingCode • ✅ Helpful Patterns - KISS - DRY - YAGNI
  • 59.
    ⌨ Testing ExistingCode 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; } }
  • 60.
    ⌨ Testing ExistingCode 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; } }
  • 61.
    ⌨ Testing ExistingCode class ShapeService { 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); }
  • 62.
    ⌨ Testing ExistingCode 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); }
  • 63.
    ⌨ Testing ExistingCode 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 ); }
  • 64.
    💬 Discussion Items •Convincing Teammates • Convincing Management
  • 65.
    💬 Discussion Items •Does unit testing slow development down?
  • 66.
    💬 Discussion Items •“I don’t see the bene fi t of unit testing”
  • 67.
    💬 Discussion Items •Unit tests for legacy code
  • 68.
  • 69.
    📝 Review • Bene fi tsof Unit Testing • PHPUnit Setup • Writing Unit Tests • Testing Existing Code
  • 70.
    Unit Testing fromSetup to Deployment • 🙋 Questions? • https://joind.in/talk/906c6
  • 71.
    Mark Niebergall @mbniebergall •PHP since 2005 • Masters degree in MIS • Senior Software Engineer • Vulnerability Management project (security scans) • Utah PHP Co-Organizer • CSSLP, SSCP Certi fi ed and Exam Developer • Endurance sports, outdoors
  • 72.
    ✏ 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