Tactical Patterns for testable applications

Barry O Sullivan - 2020


Who am i?

  • Web Developer
  • Contractor/Architect/Consultant
  • Specialise in Legacy Web Apps
  • Insanely good at Mario Kart
  • PHP Dublin Organiser
  • DDD IE Organiser


Barry O Sullivan

Talk Structure

  • Why Test (just in case)
  • Writing "Good" Tests
  • Strategic Patterns
  • Tactical Patterns
  • Conclusion (shocker!)

My history with testing

A tumultuous relationship

Why test?

Its saves time & MOney

living Documentation

Test are granular behavioural specifications

that you can also run

better design

Testing an interface gives you immediate feedback

Writing "good" tests

effective Testing

  • Iterative
  • Easy to change
  • Easy to maintain
  • Reduced bugs
  • Fast tests
  • Confidence!!!

Ineffective testing 

  • Tests take forever to write
  • More tests than code
  • Brittle tests and code
  • Way too many objects . . . 
  • Slow tests
  • Anxiety (Why is it all RED?!!?)

They are

Behaviour focussed

Module driven design

Only external interfaces need to be tested

is fine (if it is fast)

Mock appropriately

Classes extracted from a class do not need tests

They are noT

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

Unit tests


for testing

Describe what you want it to do;

Not how you're going to do it.

Focus on Behaviour


UNIT Test modules, not classes


Sub Module

Sub Module

Library Module

Potential Boundary for a


types of tests

  1. Unit
  2. Integration
  3. Acceptance 
  4. End to End
  5. Manual



Migrate to these

(Most important)

Start with


Avoid these

Write Test

Write Code

Refactor Test




You won't get it right first time


for testing

AcceptancE tests

HTTP RequestIN

HTTP Response Out

Database Operations 

Message Queue 

Behavioural tests for the application


namespace OSSTests\Auth\Acceptance;

class UserLoginTest extends \PHPUnit\Framework\TestCase
	/** @var GuzzleHttp\Client */
	private $client;

	public function test_a_user_can_login()
		$request = $this->makeLoginUserRequest();
		$response = $this->client->send($request);

	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

Acceptance test

Dependency injection




ports and adapters






External Services

HTTP adapter Test


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);


		$expectedCommand = UserCommandFactory::makeRegisterUserCommand();
		$actualCommand = $commandHandler->handledCommand();

		$this->assertEquals($expectedCommand, $actualCommand, "Commands do not match");

Command handler (App)

Issue Commands to your application



Success /







Value Object


Encapsulate value guarding



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;
  • Represent values
  • Simple classes
  • Cannot instantiate an invalid value
  • Immutable
  • Value constraints now easy to test

Value Object Tests


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)
        $email = new Email($invalidEmail);

    private function invalidEmails(): array
        return [['qux.baz.com'], ['foo@bar@baz.com'], ['.foo@baz.com']];

Composite Vos


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

repository test


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();


    	$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







Testing Difficult APIs:

  • Manually test API
  • Store sample Requests/Responses
  • Use as fixtures in Fake HTTP Client
  • Inject Fake into Service Interface during tests

Dont control, can't test

Do control,

can fake


Bad COmposition tests

  • Easy to misuse
  • Implementation specific (brittle)
  • Rely heavily mocking libraries
  • Behaviour isn't tested
  • Most of the time they're useless
    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))
        Phake::when($this->engineFactory)->getCacheDriver(new CacheType(Cacher::SESSION_CACHE))
        Phake::when($this->engineFactory)->getCacheDriver(new CacheType(Cacher::LOCAL_CACHE))
        Phake::when($this->engineFactory)->getCacheDriver(new CacheType(Cacher::IN_MEMORY_CACHE))

test support code


Encapsulate object creation

  • Start with static methods
  • Use "make" keyword
  • Wait for duplication
  • Extract into classes
  • Name key states
  • Use as test glue


When factories aren't enough

namespace OSSTests\Auth\Support;

class UserBuilder
	private $lastLogin = null;
	private $userName;

	public function __construct()
		$this->username = new UserName("testuser");

	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);


Key points

Tests are runnable documentation


Focus on describing behaviour

  • Test modules, not individual classes
  • Keep it simple
  • Break up modules when testing gets difficult
  • Choose the right pattern for the job

More info

Q & A

Barry O Sullivan - 2020


Tactical Patterns for Testable Applications

By Barry O' Sullivan

Tactical Patterns for Testable Applications

Testing is core to being a professional developer, it's one of main tools we use to design systems and prove they work. The thing is though, it's tricky to do. Tests are code and code must be maintained. So how do you test your code in a way that doesn't hamper change? That's the focus of this talk, a series of tactical design patterns that can applied to write testable applications.

  • 793
Loading comments...

More from Barry O' Sullivan