Crafting Quality PHP Applications (PHP Benelux 2018)
-
Upload
james-titcumb -
Category
Technology
-
view
109 -
download
0
Transcript of Crafting Quality PHP Applications (PHP Benelux 2018)
@asgrim
"The best code is no code at all”-- Jeff Atwood
source: https://blog.codinghorror.com/the-best-code-is-no-code-at-all/
@asgrim
“A freelancer at $25 an hour for 100 hours still costs more than a freelancer at $150 an hour that
takes 10 hours to do the same task”-- Brandon Savage
source: http://www.brandonsavage.net/earning-more-money-as-a-php-freelancer/
@asgrim
This talk...
● Planning● Development● Testing● Continuous integration● Code reviews● Deployments
@asgrim
“There are only two hard things in Computer Science: cache invalidation and naming things.”
-- Phil Karlton
source: https://martinfowler.com/bliki/TwoHardThings.html
@asgrim
“Give awkward names to awkward concepts”-- Eric Evans
source: https://skillsmatter.com/conferences/8231-ddd-exchange-2017
@asgrim
“Code for your use-case,not for your re-use-case”
-- Marco Pivetta
source: https://ocramius.github.io/extremely-defensive-php/#/39
@asgrim
A public method is like a child:once you've written it,
you are going to maintain itfor the rest of its life!
-- Stefan Priebsch
@asgrim
Immutable value objects
declare(strict_types=1);use Assert\Assertion; // beberlei/assert library !final class PostalCode{ private const VALIDATION_EXPRESSION = '(GIR 0AA)|((([A-Z-[QVX]][0-9][0-... /** @var string */ private $value; public function __construct(string $value) { $this->assertValidPostalCode($value); $this->value = $value; } private function assertValidPostalCode(string $value) : string { Assertion::regex($value, self::VALIDATION_EXPRESSION); } public function __toString() : string { return $this->value; }}
@asgrim
Value objects are valid!
declare(strict_types=1);
final class Thing { public function assignPostalCode(PostalCode $postalCode) : void { // ... we can trust $postalCode is a valid postal code }}
// 12345 is not valid - EXCEPTION!$myThing->assignPostalCode(new PostalCode('12345'));
// assignPostalCode is happy$myThing->assignPostalCode(new PostalCode('PO1 1AA'));
// With STRICT types, this will also FAIL$myThing->assignPostalCode(new PostalCode(12345));
@asgrim
Reduce complexity
public function process(Stuff a, string b, int c) : MoreStuff{ // lots of complicated code // lots of complicated code // lots of complicated code // lots of complicated code // lots of complicated code // lots of complicated code // lots of complicated code // lots of complicated code // lots of complicated code // lots of complicated code // lots of complicated code // lots of complicated code // lots of complicated code // lots of complicated code
@asgrim
Reduce complexity
public function meaningfulThing(Stuff a, string b, int c) : More{ // call nicer, meaningful methods below}
private function throwStuffIntoFire(Stuff a) : Fire{ // smaller, meaningful chunk of code}
private function combineStringWithFire(Fire a, string b) : string{ // simple, lovely code!}
private function postFlowersToSpain(int c) : void
@asgrim
Line coverage
<?phpfunction foo(bool $a, bool $b) { if ($a) { echo "A"; } if ($b) { echo "B"; }}
// generate coverage using calls:foo(true, false);foo(false, true);
@asgrim
Line coverage
<?phpfunction foo(bool $a, bool $b) { if ($a) { echo "A"; } if ($b) { echo "B"; }}
// generate coverage using calls:foo(true, false);foo(false, true);
@asgrim
Branch coverage
<?phpfunction foo(bool $a, bool $b) { if ($a) { echo "A"; } if ($b) { echo "B"; }}
// generate coverage using calls:foo(true, false);foo(false, true);
@asgrim
Branch coverage
<?phpfunction foo(bool $a, bool $b) { if ($a) { echo "A"; } if ($b) { echo "B"; }}
// generate coverage using calls:foo(true, false);foo(false, true);
@asgrim
Branch coverage
<?phpfunction foo(bool $a, bool $b) { if ($a) { echo "A"; } if ($b) { echo "B"; }}
// generate coverage using calls:foo(true, false);foo(false, true);foo(false, false); // NEW TEST!!!
@asgrim
Branch coverage
<?phpfunction foo(bool $a, bool $b) { if ($a) { echo "A"; } if ($b) { echo "B"; }}
// generate coverage using calls:foo(true, false);foo(false, true);foo(false, false); // NEW TEST!!!foo(true, true); // NEW TEST!!!
@asgrim
Prevent coverage leaking
<?php
namespace Foo;
/** * @covers \Foo\Bar */final class BarTest extends TestCase{ // write some tests!}
@asgrim
Example of a test not testing…
public function testPurchaseTickets(){ $event = Event::create(uniq('event', true) $customer = Customer::create(uniq('name', true)); $shop->purchaseTicket(1, $event, [$customer]);}
@asgrim
@asgrim
Assert you’re asserting
public function testPurchaseTickets(){ $event = Event::create(uniq('event', true) $customer = Customer::create(uniq('name', true)); $receipt = $shop->purchaseTicket(1, $event, [$customer]);
self::assertSame($event, $receipt->event()); $customersInReceipt = $receipt->customers(); self::assertCount(1, $customersInReceipt); self::assertContains($customer, $customersInReceipt);}
@asgrim
@asgrim
Mutation testing
function add(int $a, int $b) : int {
return $a + $b;
}
function testAdd() {
$result = add(2, 3);
// self::assertSame(5, $result);
}
@asgrim
Mutation testing
function add(int $a, int $b) : int {
return $a - $b;
}
function testAdd() {
$result = add(2, 3);
// self::assertSame(5, $result);
}
@asgrim
Mutation testing
function add(int $a, int $b) : int {
return $a - $b;
}
function testAdd() {
$result = add(2, 3);
self::assertSame(5, $result);
// /\/\/\ test will now fail with mutation
}
@asgrim
BAD! Do not do this.
Feature: Ability to print my boarding pass
Scenario: A checked in passenger can print their boarding pass Given I have a flight booked And I have checked in When I visit the home page And I click the ".manage-booking" button And I enter "CJM23L" in the ".bref-ipt-fld" field And I click the "Find Booking" button Then I should see the ".booking-ref.print" button When I click the ".booking-ref.print" button Then I should see the print dialogue
@asgrim
Better Behaviour test
Feature: Ability to print my boarding pass
Policies: - Boarding passes are only available when already checked in - If customer cannot print boarding pass, they can collect at The airport - more business rules etc...
Scenario: A checked in passenger can print their boarding pass Given I have a flight booked for LHR-OTP on 24th May And I have previously checked in for the flight When I display my booking reference "CJM23L" Then I should be able to print my boarding pass And the boarding pass should display flight details correctly
@asgrim
Automated tests
Application (and UI, API, etc.)
Domain / Business Logic
Infrastructure (DBAL, APIs, etc.)
@asgrim
Automated tests
Application (and UI, API, etc.)
Domain / Business Logic
Infrastructure (DBAL, APIs, etc.)
@asgrim
Testing at domain layer
Application (UI, API, etc.)
Domain / Business Logic
Infrastructure (DB, APIs, etc.)
// Testing via the UIpublic function iDisplayMyBookingReference(string $reference){ $page = $this->getSession()->getPage(); $page->click(".manage-booking"); $page->findField(".bref-ipt-fld")->setValue($reference); $page->click("Find Booking");}
// Using the domain layer directly in testspublic function iDisplayMyBookingReference(string $reference){ $this->booking = $this->retrieveBooking($reference);}
@asgrim
Some UI testing is okay!!!
Feature: Ability to print my boarding pass
Policies: - Boarding passes are only available when already checked in - If customer cannot print boarding pass, they can collect at The airport - more business rules etc...
@ui Scenario: A checked in passenger can print their boarding pass Given I have a flight booked for LHR-OTP on 24th May And I have previously checked in for the flight When I display my booking reference "CJM23L" Then I should be able to print my boarding pass And the boarding pass should display flight details correctly