Tactical Patterns for testable applications

Barry O Sullivan - 2018

@barryosull

Goal of this talk

Who am i?

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

THe landscape of testing

THe types of tests

  • Unit
  • Integration
  • Acceptance 
  • End to End
  • Manual

Easy/Fast

Complicated/Slow

the Tools of testing

JAVA

PHP

JAVASCRIPT

JEST

CODECEPTION

UNIT

UI

BDD

What makes a good test

behaviour focussed

Focus on what you you want you to do

Not how you're going to do it

effective Testing

  • Stable Software
  • SOLID code
  • Easy to change
  • Higher quality
  • Reduced bugs
  • Confidence!!!

Ineffective testing 

  • Tests take forever to write
  • Tests are brittle
  • More test code than actual code
  • Way too many objects
  • Don't know where to start
  • Anxiety "Why is it all RED?!!?"

Unit Tests

Common misconceptions

They are

  1. Behaviour focussed
  2. Module driven design
  3. Only external interface needs to be tested
  4. DB/Network/Filesystem is fine if its fast
  5. Use mocks/fakes sparingly
  6. See point 3

They are noT

  1. Method focussed
  2. Class driven design
  3. Every class needs to be tested
  4. No DB/Network /FileSystem access
  5. Always mock dependencies
  6. Extracted classes need tests

Unit Tests

HOW DO WE WRITE EFFECTIVE TESTS

MODULES

Test modules, not classes

Module

Sub Module

Sub Module

Library Module

CQS

COMMAND QUERY SEPARATION

<?php

namespace OSS\Shop\Services;

use OSS\Shop\ {Payment, PaymentId, PaymentCollection};

interface UserPaymentService 
{
    /****************
        Command changes state, return nothing
     ***************/
    public function processPayment(Payment $payment): void;

    public function reversePayment(PaymentId $paymentId): void;

    /****************
        Queries return state, change nothing
     ***************/
    public function wasPaymentSuccessful(): bool;

    public function getAllFailedPayments(): PaymentCollection;
}

The most basic separation of concerns

PATTERNS FOR TESTING BEHAVIOUR

AcceptancE tests

HTTP RequestIN

HTTP Response Out

Database Operations 

Message Queue 

Behavioural tests for the entire application

GIVEN/WHEN/THEN

<?php

namespace OSSTests\Auth\Acceptance;

class UserLoginTest extends \PHPUnit\Framework\TestCase
{
	const USER_ID = 22;
	const USERNAME = 'username';
	const PASSWORD = 'password';

	public function test_a_user_can_login()
	{
		$this->givenThereIsAUser();
		$this->whenTheUSerLogsIn();
		$this->thenTheyAreLoggedIn();
	}

	private function givenThereIsAUser()
	{
		// Insert user into database
	}

	private function whenTheUSerLogsIn()
	{
		// Make POST call to login endpoint
	}

	private function thenTheyAreLoggedIn()
	{
		// Check session to see if the user is there
	}
}

GIVEN/WHEN/THEN

Test case is descriptive

Details can go here

Extract into test support code (eventually)

Consts for shared values

When tests get messy

The curse of too many behavioural combinations

ports and adapters

HTTP

Adapter

Application

 

Repositories

External Services

Domain

Value Object

 

Encapsulate value guarding

 

<?php

namespace OSS\Auth\ValueObjects;

final class Email 
{
    private $value;

    public function __construct(string $email)
    {
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            throw new ValueError("'$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

<?php

namespace OSSTests\Auth\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']];
    }
}    

Composite Vos

<?php

namespace OSS\Auth\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

HTTP adapter

<?php

namespace OSSTests\Auth\ValueObjects;

Use OSS\Auth\Controllers\UserController;
use OSSTest\Auth\Support\UserRequestFactory;
use OSSTest\Auth\Support\UserCommandFactory;
use OSSTest\Auth\App\FakeCommandHandler;

class UserRegisterTest extends \PHPUnit\Framework\TestCase
{
	public function test_creates_register_user_command()
	{
		$httpRequest = UserRequestFactory::makeRegisterUser();

		$commandHandler = new FakeCommandHandler();

		$adapter = new UserController($commandHandler);

		$adapter->handle($request);

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

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

Command handler

Issue Commands to a CommandHandler

Application

 

Command

Success /

Failure

repositories

ENCAPSULTATE OBJECT PERSISTENCE

repository test

<?php

namespace OSSTests\Auth\ValueObjects;

Use OSS\Auth\Repository\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;
}    

API INTEGRATIONS

Service

Interface

App

HTTP

Client

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

COmposition tests

  • Usually an anti-pattern
  • Implementation specific (brittle)
  • Rely on mocking libraries
  • Bring less value than behavioural tests
  • Most of the time they're useless
<?php
....
    protected function setUp()
    {
        $this->engineFactory = Phake::mock('OSS\CacheEngine\DaftCacheDriverFactory');
        $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);
    }

Dependency injection

test support code

Factories

Encapsulate object creation

Builders

When factories aren't enough

<?php

namespace OSSTests\Auth\Support;

use OSS\Auth\Repositories\ {UserRepository, UserRepositoryFactory};
use OSS\Auth\Services\ {MessageQueueFactory, MessageQueue};
use OSS\Auth\App\CommandHandler;

class CommandHandlerBuilder
{
	private $userRepository;
	private $messageQueue;

	public function __construct()
	{
		$this->userRepository = new UserRepositoryFactory::database();
		$this->messageQueue = new MessageQueueFactory::sqs();
	}

	public function withUserRepository(UserRepository $userRepository)
	{
		$this->userRepository = $userRepository;
	}

	public function withMessageQueue(MessageQueue $messageQueue)
	{
		$this->messageQueue = $messageQueue;
	}

	public function build(): CommandHandler
	{
		return new CommandHandler($this->userRepository, $this->messageQueue);
	}
}

Breather slide

Let's all take a nice deep breath

<?php

namespace OSSTests\Auth\App;

class UserLoginTest extends \PHPUnit\Framework\TestCase
{
	public function test_a_user_can_login()
	{
		$this->givenThereIsAUser();
		$this->whenTheUSerLogsIn();
		$this->thenTheyAreLoggedIn();
	}

	private function givenThereIsAUser()
	{
		$user = UserFactory::make();
                $this->userRepository->store($user);
	}

	private function whenTheUSerLogsIn()
	{
		$request = UserRequestFactory::makeLoginRequest();
                $this->httpAdapter->handle($request);
	}

	private function thenTheyAreLoggedIn()
	{
                $user = UserFactory::make();
		$isLoggedIn = $this->userSession->isUserActive($user->getId());

                $this->assertTrue($isLoggedIn, "User should be logged in");
	}
}

GIVEN/WHEN/THEN revisted

Key points

Tests are runnable documentation

therefore

Focus on describing behaviour

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

More info

Q & A

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.

  • 77
Loading comments...

More from Barry O' Sullivan