Skip to content

Instantly share code, notes, and snippets.

@cjsaylor
Last active September 20, 2018 23:59
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cjsaylor/c30c1e09f0f1a50b3a847397054d2277 to your computer and use it in GitHub Desktop.
Save cjsaylor/c30c1e09f0f1a50b3a847397054d2277 to your computer and use it in GitHub Desktop.
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