Pattern misuse and unit test gotchas
<?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); | |
} | |
} |
<?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); |
<?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? |
<?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']); |
<?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'))); | |
} | |
} |
<?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'); | |
} | |
} |
<?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); | |
} | |
} |
<?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