Pattern misuse and unit test gotchas
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
// --- Bad inheritance | |
abstract class Media | |
{ | |
abstract protected function tableSource() : string; | |
public function getByID(int $id) : array | |
{ | |
$tableSource = $this->tableSource(); | |
return $this->db->query("select * from $tableSource where id = ?", $id); | |
} | |
public function download(int $id) : string | |
{ | |
$provider = $this instanceof Music ? new S3Provider() : new ExternalProvider(); | |
return $provider->getDownloadURL($id); | |
} | |
} | |
class Music extends Media | |
{ | |
protected function tableSource() : string | |
{ | |
return 'music'; | |
} | |
} | |
class Video extends Media | |
{ | |
protected function tableSource() : string | |
{ | |
return 'videos'; | |
} | |
} | |
// --- composition | |
interface Media | |
{ | |
public function getByID(int $id) : array; | |
public function download(int $id) : string; | |
} | |
trait MediaDownloadTrait | |
{ | |
private $provider; | |
public function download(int $id) : string | |
{ | |
return $this->provider->getDownloadURL($id); | |
} | |
} | |
class Music implements Media | |
{ | |
use MediaDownloadTrait; | |
public function __construct(Provider $provider = null) | |
{ | |
$this->provider = $provider ?? new S3Provider(); | |
} | |
public function getByID(int $id) : array | |
{ | |
return $this->db->query("select * from music where id = ?", $id); | |
} | |
} | |
class Video implements Media | |
{ | |
use MediaDownloadTrait; | |
public function __construct(Provider $provider = null) | |
{ | |
$this->provider = $provider ?? new ExternalProvider(); | |
} | |
public function getByID(int $id) : array | |
{ | |
return $this->db->query("select * from videos where id = ?", $id); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
class Item | |
{ | |
public function __construct($name, $upc, $price) | |
{ | |
$this->name = $name; | |
$this->upc = $upc; | |
$this->price = $price; | |
} | |
} | |
class ItemCollection implements \Iterator | |
{ | |
private $items = []; | |
private $position = 0; | |
public function __construct(Item ...$items) | |
{ | |
$this->items = $items; | |
} | |
public function addItem(Item $item) | |
{ | |
$this->items[] = $item; | |
} | |
public function rewind() { | |
$this->position = 0; | |
} | |
public function current() { | |
return $this->items[$this->position]; | |
} | |
public function key() { | |
return $this->position; | |
} | |
public function next() { | |
++$this->position; | |
} | |
public function valid() { | |
return isset($this->items[$this->position]); | |
} | |
} | |
function display(ItemCollection $items) | |
{ | |
foreach ($items as $item) { | |
echo $item->name; | |
} | |
} | |
$item = new Item('A', '80090192308', 9.99); | |
$item2 = new Item('B', '80912980741', 10.99); | |
$itemCollection = new ItemCollection($item, $item2); | |
display($items); | |
function displayEasier(Item ...$items) | |
{ | |
foreach ($items as $item) { | |
echo $item->name; | |
} | |
} | |
displayEasier($item, $item2); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
class Email | |
{ | |
public $email; | |
public function __construct(string $email) | |
{ | |
$this->email = $email; | |
} | |
public function isValid() : boolean | |
{ | |
return SomeRFCRequirement::satisfies($this->email); | |
} | |
} | |
class Emailer | |
{ | |
public function sendEmail(Email $email, string $body) | |
{ | |
if (!$email->isValid()) { | |
throw \RuntimeException('Invalid email'); | |
} | |
} | |
} | |
class EmailRepo | |
{ | |
public function saveEmail(Email $email) : int | |
{ | |
return $this->db->execute('insert into emails (email) values (?)', $email->email); | |
} | |
public function getEmail($id) : Email | |
{ | |
return new Email($this->db->query('select email from emails where id = ?', $id)); | |
} | |
} | |
$email = new Email('somebademail'); | |
$emailRepo = new EmailRepo(); | |
$id = $emailRepo->saveEmail($email); | |
// Sometime later (possibly after user has left the site) | |
$email = $emailRepo->getEmail($id); | |
$emailer = new Emailer(); | |
$emailer->sendEmail($email, 'Some cool email text'); | |
// OH NO invalid email what do we do now? |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
// Observer pattern overuse | |
class ItemObserver implements SplObserver | |
{ | |
public function update(SplSubject $subject) { | |
foreach ($subject->items() as $item) { | |
if ($item['quantity'] < 0) { | |
// Now we have to know this observer's behavior in the subject in order to handle this? | |
throw new \RuntimeException('Expected positive quantity.'); | |
} | |
} | |
} | |
} | |
class Cart implements SplSubject | |
{ | |
private $items = []; | |
public function __construct() | |
{ | |
$this->bus = []; | |
} | |
public function attach(SplObserver $observer) : void | |
{ | |
$this->bus[] = $observer; | |
} | |
public function detach(SplObserver $observer) : void | |
{ | |
} | |
public function addItem(array $item) : void | |
{ | |
$this->items[] = $item; | |
$this->notify(); | |
} | |
public function notify() : void | |
{ | |
foreach ($this->bus as $observer) { | |
$observer->update($this); | |
} | |
} | |
public function items() : array | |
{ | |
return $items; | |
} | |
} | |
$cart = new Cart(); | |
$cart->addItem(['quantity' => 1, 'name' => 'some product']); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
// Provider misuse | |
interface SearchProvider | |
{ | |
public function search(string $term) : array; | |
} | |
class Provider | |
{ | |
private $providerList = [ | |
'products' => [ | |
'active' => 'ProductsElasticSearchProvider', | |
'fallback' => 'ProductsMySQLProvider', | |
], | |
'discounts' => [ | |
'active' => 'DiscountMySQLProvider', | |
'fallback' => 'DiscountMySQLProvider', | |
], | |
'coupons' => [ | |
'active' => 'CouponMySQLProvider', | |
'fallback' => 'CouponMySQLProvider', | |
], | |
'giftcard' => [ | |
'active' => 'GiftcardMySQLProvider', | |
'fallback' => 'GiftcardMySQLProvider', | |
], | |
]; | |
public static function getProvider(string $type) | |
{ | |
$classPath = static::$providerList[$type]['active']; | |
return new $classPath(); | |
} | |
} | |
class ProductMySQLProvider implements SearchProvider | |
{ | |
public function search(string $term) : array | |
{ | |
return $this->db->query('select * from products where name = ?', $term); | |
} | |
} | |
class ProductController | |
{ | |
public function __construct(Request $request, Response $response) | |
{ | |
$this->request = $request; | |
$this->response = $response; | |
} | |
public function search() | |
{ | |
$this->response->body($this->getProvider('product')->search($this->request->param('term'))); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
use PHPUnit\Framework\TestCase; | |
class ExpensiveOperation | |
{ | |
public function doIt() | |
{ | |
// This is just a simulation of something that takes a while | |
sleep(1); | |
return $this->getNow(); | |
} | |
protected function getNow() | |
{ | |
return date('c'); | |
} | |
} | |
class ExpensiveOperationTest extends TestCase | |
{ | |
// Brittle tests with dates | |
public function testDoIt() | |
{ | |
$now = date('c'); | |
$op = new ExpensiveOperation(); | |
$result = $op->doIt(); | |
$this->assertEquals($now, $result); | |
// This will fail periodically depending on the timing of the test | |
} | |
public function testDoItNiavelyBetter() | |
{ | |
$now = date('c'); | |
$mock = $this->getMock(ExpsensiveOperation::class, ['getNow']); | |
$mock->expects($this->any())->method('getNow') | |
->will($this->returnValue($now)); | |
$result = $mock->doIt(); | |
$this->assertEquals($now, $result); | |
// Passes, but we have a mock :( | |
} | |
public function testDoItBest() | |
{ | |
$now = new \DateTimeImmutable(); | |
$op = new ExpensiveOperationBetter($now); | |
$result = $op->doIt(); | |
$this->assertEquals($now->format('c'), $result); | |
// Passes, no mocks! | |
} | |
} | |
class ExpensiveOperationBetter extends ExpensiveOperation | |
{ | |
private $now; | |
public function __construct(\DateTimeImmutable $now = null) | |
{ | |
if ($now === null) { | |
$now = new \DateTimeImmutable(); | |
} | |
$this->now = $now; | |
} | |
protected function getNow() | |
{ | |
return $this->now->format('c'); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
use PHPUnit\Framework\TestCase; | |
use PHPUnit\DbUnit\TestCaseTrait; | |
class DataModel | |
{ | |
public function getEntries() : array | |
{ | |
return $this->db->query("select * from some_table"); | |
} | |
public function getFilteredEntries(string $name) : array | |
{ | |
return $this->db->query("select * from some_table where name = ?", $name); | |
} | |
} | |
class DataModelFixture | |
{ | |
public $fixture = [ | |
'some_table' => [ | |
['id' => 1, 'name' => 'a'], | |
['id' => 2, 'name' => 'b'], | |
['id' => 3, 'name' => 'b'], | |
] | |
]; | |
} | |
class DataModelTest extends TestCase | |
{ | |
use TestCaseTrait; | |
public function setUp() | |
{ | |
$this->fixture = DataModelFixture::$fixture; | |
} | |
public function testGetEntries() | |
{ | |
$model = new DataModel(); | |
$results = $model->getEntries(); | |
$this->assertCount(3, $results); | |
} | |
public function testGetFilteredEntries() | |
{ | |
$model = new DataModel(); | |
$results = $model->getFilteredEntries('b'); | |
$this->assertCount(2, $results); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
use PHPUnit\Framework\TestCase; | |
function numberGenerator($length) : \Generator | |
{ | |
foreach (range(1, $length) as $number) { | |
yield $number; | |
} | |
} | |
class NumberGeneratorTest extends TestCase | |
{ | |
public function testNumberGeneratorOops() | |
{ | |
$results = numberGenerator(3); | |
$this->assertEquals([1, 2, 3], $results); | |
// this will fail | |
} | |
public function testNumberGeneratorFix() | |
{ | |
$results = iterator_to_array(numberGenerator(3)); | |
// or | |
foreach (numberGenerator(3) as $number) { | |
$results[] = $number; | |
} | |
$this->assertEquals([1, 2, 3], $results); | |
// Passes | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment