Barry O Sullivan - 2020
@barryosull
@barryosull
Barry O Sullivan
Test are granular behavioural specifications
that you can also run
Testing an interface gives you immediate feedback
Behaviour focussed
Module driven design
Only external interfaces need to be tested
DB/Network/Filesystem
is fine (if it is fast)
Mock appropriately
Classes extracted from a class do not need tests
Method focussed
Class driven design
Every class needs its own test cases
No DB/Network/FileSystem
access at all
Mock every dependency
Classes extracted from a class needs tests
Common Misconceptions
Describe what you want it to do;
Not how you're going to do it.
Module
Sub Module
Sub Module
Library Module
Potential Boundary for a
Stub/Mock/Fake
Easy/Fast
Complicated/Slow
Migrate to these
(Most important)
Start with
these
Avoid these
Write Test
Write Code
Refactor Test
Refactor
Code
You won't get it right first time
HTTP RequestIN
HTTP Response Out
Database Operations
Message Queue
Behavioural tests for the application
<?php
namespace OSSTests\Auth\Acceptance;
class UserLoginTest extends \PHPUnit\Framework\TestCase
{
/** @var GuzzleHttp\Client */
private $client;
public function test_a_user_can_login()
{
$this->givenThereIsAUser();
$request = $this->makeLoginUserRequest();
$response = $this->client->send($request);
$this->assertUserHasLoginCookie($response);
}
private function givenThereIsAUser(): void
{
// Insert user into database
}
private function makeLoginUserRequest(): RequestInterface
{
// Make a POST request to login a user
}
private function assertUserHasLoginCookie(ResponseInterface $response)
{
// Check session to see if the user is there
}
}
Testing
Staging
Production
HTTP
Adapters
Application
Repositories
External Services
<?php
namespace OSSTests\Auth\Unit\Http\Controllers;
Use OSS\Auth\Controllers\UserController;
use OSSTest\Auth\Support\UserRequestFactory;
use OSSTest\Auth\Support\UserCommandFactory;
use OSSTest\Auth\App\CommandHandlerFake;
class UserRegisterTest extends \PHPUnit\Framework\TestCase
{
public function test_creates_register_user_command()
{
$httpRequest = UserRequestFactory::makeRegisterUser();
$commandHandler = new CommandHandlerFake();
$adapter = new UserController($commandHandler);
$adapter->handle($request);
$expectedCommand = UserCommandFactory::makeRegisterUserCommand();
$actualCommand = $commandHandler->handledCommand();
$this->assertEquals($expectedCommand, $actualCommand, "Commands do not match");
}
}
Issue Commands to your application
Application
Command
Success /
Failure
Repositories
External
Services
Stubs/Mocks/Fakes
Stubs/Mocks/Fakes
Encapsulate value guarding
<?php
namespace OSS\Auth\App\ValueObjects;
final class Email
{
private $value;
public function __construct(string $email)
{
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new DomainException("'{$email}' is not a valid email address");
}
$this->value = $email;
}
public function __toString(): string
{
return $this->value;
}
}
<?php
namespace OSSTests\Auth\App\ValueObjects;
Use OSS\Auth\ValueObjects\Email;
Use OSS\Auth\ValueObjects\ValueError;
class EmailTest extends \PHPUnit\Framework\TestCase
{
public function test_accepts_valid_emails()
{
$validEmails = ['email@email.com', 'fgsfg@fgfd2234.com', 'foo+bar@baz.com'];
foreach($validEmails as $validEmail) {
$email = new Email($validEmail);
}
}
/**
* @dataProvider invalidEmails
*/
public function test_rejects_invalid_emails($invalidEmail)
{
$this->expectException(ValueError::class);
$email = new Email($invalidEmail);
}
private function invalidEmails(): array
{
return [['qux.baz.com'], ['foo@bar@baz.com'], ['.foo@baz.com']];
}
}
<?php
namespace OSS\Auth\App\Commands;
final class ChangeEmailCommand
{
private $userId;
private $email;
public function __construct(UserId $userId, Email $email)
{
$this->userId = $userId;
$this->email = $email;
}
public function getUserId(): UserId
{
return $this->userId;
}
public function getEmail(): Email
{
return $this->email;
}
}
Compose ValueObjects to represent concepts
(e.g. Commands)
Encapsulate object persistence
<?php
namespace OSSTests\Auth\Infra\Repositories;
Use OSS\Auth\App\Repositories\UserRepository;
use OSSTests\Auth\Support\UserFactory;
abstract class UserRepostioryTest extends \PHPUnit\Framework\TestCase
{
public function test_can_fetch_a_stored_user()
{
$user = UserFactory::make();
$repo = $this->makeRepo();
$repo->store($user);
$actual = $repo->fetch($user->getId());
$this->assertEquals($user, $actual);
}
abstract protected function makeRepo(): UserRepository;
}
Concrete integration test would extend this test and make the `makeRepo()` method return a real instance
Service
Interface
App
HTTP
Client
Testing Difficult APIs:
Dont control, can't test
Do control,
can fake
<?php
....
protected function setUp()
{
$this->engineFactory = Phake::mock('OSS\CacheEngine\CacheDriverFactory');
$this->xcacheMock = Phake::mock('OSS\CacheEngine\XcacheEngine');
$this->memcachedMock = Phake::mock('OSS\CacheEngine\MemcachedEngine');
$this->inMemoryMock = Phake::mock('OSS\CacheEngine\InMemoryEngine');
Phake::when($this->engineFactory)->getCacheDriver(new CacheType(Cacher::SHARED_CACHE))
->thenReturn($this->memcachedMock);
Phake::when($this->engineFactory)->getCacheDriver(new CacheType(Cacher::SESSION_CACHE))
->thenReturn($this->memcachedMock);
Phake::when($this->engineFactory)->getCacheDriver(new CacheType(Cacher::LOCAL_CACHE))
->thenReturn($this->xcacheMock);
Phake::when($this->engineFactory)->getCacheDriver(new CacheType(Cacher::IN_MEMORY_CACHE))
->thenReturn($this->inMemoryMock);
}
Encapsulate object creation
When factories aren't enough
<?php
namespace OSSTests\Auth\Support;
class UserBuilder
{
private $lastLogin = null;
private $userName;
public function __construct()
{
$this->username = new UserName("testuser");
// Details omitted
}
public function withLastLogin(DateTime $lastLogin): self
{
$builder = clone $this;
$builder->lastLogin = $lastLogin;
return builder;
}
public function withUserName(UserName $userName): self
{
$builder = clone $this;
$builder->userName = $userName;
return builder;
}
public function build(): User
{
return new User($this->username, ..., ..., $this->lastLogin);
}
}
Tests are runnable documentation
therefore
Focus on describing behaviour
Barry O Sullivan - 2020
@barryosull