PHP Québec

Presented by

Sponsors

Symfony Experts

Welcome

Steven Rosato, B. Ing

President & Founder @ Solutions Majisti

Steven Rosato

Small company near Montreal (Boisbriand) focussed on development with Symfony and React/Redux.

/solutions.majisti

Before we start...

1 year

8 meetups in 2016 every 6 weeks

- Docker & Symfony (16/11/2015)
- Symfony 3.0 (25/01/2016)

- Full Stack Testing with Symfony - Steven Rosato (15/03/2016)
- API with FOSRestBundle 
- Create a CMS with SonataAdminBundle
- How to Optimise Doctrine
- Symfony Caching System
- SyliusBundle

Before we start...

17h30 - 19h00 Networking 

19h00 - 20h00 Presentation

After... Pub Victoria

Today

Objectives

  • The Mentra of the Tester
  • TDD vs BDD
  • The Testing Pyramid (testing stack)
  • Symfony Testing
  • Frontend Testing (very brief)

The Mentra of the Tester

??

a Boob

a Guru

Don't be a boob

The Mentrality

  • Test first approach
  • Be prepared for serious conceptual design and discipline
  • Do not bite more than you can chew
  • Allow yourself some hack & slash or prototyping

Fixing a bug in production

You should test when...

You should not test when

  • You trash what you write in the short term
  • You design a prototype that does not become production
  • You are rich enough to clear your technical debt

Boob tip #1: You are poorer than you think

Your prototype will go in production

#no-bank

You should test when

Psst... Guru tip #1

  • Code maintainability is a concern
  • Client needs will evolve
  • Refactoring legacy to new structure (rewrite without testing is useless)

Your project will need maintenance

Guru tip #2

Your client's needs will evolve

TDD vs BDD

TDD: Test Driven Development

BDD: Behaviour Driven Development

  • Write test first
  • Red/Green
  • Refactor
  • Emerged from TDD
  • Focusses on Object Behaviour rather than input/output only

DDD: Domain Driven Design approach

Why??

The Benefits

  • Enforces S.O.L.I.D and G.R.A.S.P principles
    • Single responsability: A Class should have only one reason to change
  • Forces you to go back to the drawing boards
  • Regression bugs covered
  • Refactor confidence
  • Lower long term costs

TDD Bad Example

<?php

class Counter
{
    public function testTick()
    {
        $counter = new Counter();
        $count = $counter->tick();
        $this->assertEquals($count, 1);
    }
}
  • Don't reverse implement

What's wrong?

BDD Good Example

<?php

class Counter
{
    public function shouldIncrementCounterByOneByDefault()
    {
        $counter = new Counter();
        
        $expectedCount = $counter->current() + 1;
        $this->checkThat($expectedCount, equalTo($counter->tick()));
    }
    
    public function shouldIncrementCounterByDefinedStep()
    {
        $step = 2;
        $counter = new Counter($step);
        
        $expectedCount = $counter->current() + $step;
        $this->checkThat($expectedCount, equalTo($counter->tick()));
    }
}

Object behaviour

No reverse implementation

Real World Example

TDD Bad Example

class CustomerAccountTest extends UnitTest
{
    public function testUpdateReservationWillCancelPastReservation()
    {
        $currentReservation = m::mock(HotelReservation::class);
        $reservationToUpdate = m::mock(HotelReservation::class);
        
        $currentReservation ->shouldReceive('setStatus')
            ->once()
            ->with('CANCELLED');
        
        $reservationToUpdate->shouldReceive('setStatus')
            ->once()
            ->with('NEW');
        
        $this->uut->updateReservation($currentReservation );
        $this->uut->updateReservation($reservationToUpdate);
        
        $this->assertEquals('CANCELLED', $currentReservation ->getStatus());
        $this->assertEquals('NEW', $reservationToUpdate->getStatus());

        $this->assertEquals($currentReservation , $this->uut->getLastReservation());        
        $this->assertEquals($reservationToUpdate, $this->uut->getCurrentReservation());
    }
}

What's wrong?

BDD Example continued...

CustomerAccount has:

  • Too many responsibilities
    • It records information
    • It changes state for a reservation
    • It holds a history of cancelled reservations
  • Reverse implementation
    • Refactoring without changing the object's behaviour will break tests, when it should not

True story bro. Real world case. Even though I wrote tests, I ended up with this god object because I did not think about the Object's Behaviour.

BDD Example continued...

/**
 * @author Steven Rosato <steven.rosato@majisti.com>
 */
class CustomerAccountTest extends UnitTest
{
    public function shouldUpdateAReservation....????()
    {
        
    }
}

BDD Example continued...

Thinking object's behaviour forces us to come back to the drawing boards and come up with a better solution for the current problem

BDD Example continued...

/**
 * @author Steven Rosato <steven.rosato@majisti.com>
 * @property ReservationBooker $uut
 */
class ReservationBookerTest extends UnitTest
{
    /**
     * @var StateMachineFactory|m\MockInterface
     */
    private $stateMachineFactory;

    public function setUp()
    {
        $this->stateMachineFactory = m::mock(StateMachineFactory::class);
        $this->uut = new ReservationBooker($this->stateMachineFactory);
    }
}

BDD Example continued...

/**
 * @author Steven Rosato <steven.rosato@majisti.com>
 * @property ReservationBooker $uut
 */
class ReservationBookerTest extends UnitTest
{
    //setup...

    /**
     * @test
     */
    public function shouldUpdateAReservationForACustomerByCancellingHisPreviousOne()
    {
        $currentReservation = new HotelReservation();
        $reservationToUpdate = new HotelReservation();

        $account = m::spy(CustomerAccount::class);
        $account->setReservation($currentReservation);
        $account->shouldReceive('addReservationToHistory')->once()->with($currentReservation);

        $this->stateMachineFactory
            ->shouldReceive('get->apply')
            ->once()
            ->with(ReservationTransitions::CANCEL)
        ;

        $this->uut->updateReservation($account, $reservationToUpdate);
    }
}
winzou_state_machine:
    reservation:
        class: Majisti\Domain\Reservation\Reservation
        property_path: state
        graph: default
        states:
            - pending
            - confirmed
            - abandoned
            - cancelled

State Machine Pattern??

  • Uncouples the state transition responsibility from the object

You understood BDD when

You didn't understand BDD when

  • You test input/output
  • You test the "How"
    • You reverse implement
  • You write code before tests
  • You test object behaviour
  • You test objects interactions with the UUT
  • Feature driven
  • You write easy to understand scenarios
  • Each object has its own responsability

The Testing Pyramid

Available Tools in PHP

Mockery

Phake

Acceptance Testing

  • Selenium/PhantomJs Tests
  • Keep tests light
  • Can be used both on the Backend and Frontend (JavaScript)
  • Great for End-to-End testing
  • Great for legacy system testing (even Wordpress!)

Off load testing through the entire pyramid

Acceptance Testing

<?php
$I = new AcceptanceTester($scenario);
$I->wantTo('sign in');
$I->amOnPage('/login');
$I->fillField('username', 'Steven');
$I->fillField('password', 'qwerty');
$I->click('LOGIN');
$I->see('Welcome, Steven!');
?>

Exercise:

  • Page Object Pattern
  • Step Object Pattern

Functional Testing

  • Great for API testing
  • Command line testing
  • It's black-box testing, but at the technical level

Functional Testing

namespace Tests\Ez\Legacy\ConsoleCommands;

class DesktopAppsDeploymentCommandCest extends DeploymentCommandCest
{
    const COMMAND_TO_RUN = 'ez:legacy:desktop-apps:config';

    /**
     * @param FunctionalTester $tester
     */
    public function seeTheModifiedScannerAppConfigFileWithNewValues(FunctionalTester $tester)
    {
        $this->deleteTestFile($tester, DataConfiguration::scannerAppConfigOutput());

        $optionName = DesktopAppsDeploymentCommand::SCANNER_APP_OPTION_NAME;

        $tester->runCommand(static::COMMAND_TO_RUN, array('--' . $optionName => null));
        $tester->seeCommandOutput(sprintf(
            DesktopAppsDeploymentCommand::MESSAGE_DEPLOYMENT_SUCCESSFUL, 
            $optionName
        ));
        $tester->dontSeeAnException();
        $this->checkThatFileWasChanged(
            $tester, 
            DataConfiguration::scannerAppConfigInput(), 
            DataConfiguration::scannerAppConfigOutput()
        );
    }
}

Integration Testing

  • Great for component behaviour testing
  • Dependency Injection Container is your friend
  • Hamcrest

Integration Testing

namespace Tests\Integration;

/**
 * @author Guyllaume Cardinal <gy@majisti.com>
 * @group media.finding
 */
class MediaFindingTest extends IntegrationTest
{
    /**
     * @var MediaFinder
     */
    private $mediaFinder;

    public function _before()
    {
        /** @var Filesystem $fileSystem */
        $fileSystem = $this->getContainer()->get('oneup_flysystem.album_filesystem');
        $fileFilter = new FileFilter(new Blacklist(array(
            __DIR__ . '/path/to/fileA.jpg',
            __DIR__ . '/path/to/directoryA',
        )));
        $this->mediaFinder = new MediaFinder($fileSystem, $fileFilter);
    }
}

Integration Testing Continued...

namespace Tests\Integration;

/**
 * @author Guyllaume Cardinal <gy@majisti.com>
 * @group media.finding
 */
class MediaFindingTest extends IntegrationTest
{
    //setup...

    /**
     * @test
     * @group media.finding.blacklist
     */
    public function shouldNotReturnBlacklistedFiles()
    {
        $this->checkThat(
            'MediaFinder did not properly filter files',
            $this->hasProcessedBlacklistedFiles($this->mediaFinder->getMediaSources()),
            is(equalTo(false))
        );
    }
}

Unit Testing

  • Testing object interaction and behaviour
  • Dependency Injection is your friend
  • Mocking frameworks are your friends
  • Hamcrest

Unit Testing

namespace Edm\Test\Unit\Album\Finder;

use Mockery as m;

/**
 * @author Guyllaume Cardinal <gy@majisti.com>
 */
class MediaFinderTest extends UnitTest
{
    public function setup()
    {
        //other mocks...

        $this->fileFilterMock = m::spy(FileFilter::class);
        $this->mediaFinder = new MediaFinder(
            $this->fileSystemMock, 
            $this->fileFilterMock, 
            $this->factoryMock
        );
    }
}

Unit Testing

namespace Edm\Test\Unit\Album\Finder;

use Mockery as m;

/**
 * @author Guyllaume Cardinal <gy@majisti.com>
 */
class MediaFinderTest extends UnitTest
{
    //setup...

    public function testShouldIgnoreCertainFilesUsingAFileFilter()
    {
        $fakeDirectory = $this->createFakeDirectory();
        $this->fileFilterMock
            ->shouldReceive('validate')
            ->atLeast()->once()
            ->andReturn(true)
        ;

        $this->mediaFinder->getMediaSources();
    }
}

How to choose which layer a test goes in?

  • Conceptualize top to bottom, but implement bottom to top
  • Behaviour Driven!
  • Domain Driven Design

Acceptance

Functional

Integration

Unit

Virtual Browser

Blackbox

Technical
Command line
API

Set of classes

Repository

Database

Framework

Atomic
One per class

Objects Interactions

Real browser

UI

End-to-End

Third party libraries

The integration server counts in the entire stack

  • build process
  • deployment process
  • metrics thresholds
  • run the entire testing stack

Nightly builds

  • installation process
  • composer update
  • npm update

On commit build

phamtomjs + xfvb or SauceLabs ($$$) for Acceptance testing

Symfony Testing

Integration Layer

  • The WebTestCase
  • Testing Controllers
  • Testing Forms
  • Testing Translations
  • Testing Configuration
  • Testing Mailing
  • Doctrine2/Database Testing

Functional Layer

  • Command line testing
  • API Testing

The WebTestCase from Symfony

Step in the right direction, but we are missing..

  • Command line testing
  • Database fixtures loading
  • Api testing

Command Line Testing

  • Do not create fat commands
  • Use a Command Helper
  • A command is essentially a controller!

Why?

  • A break in Symfony will break all commands
  • Can plug-in transactional database testing

What to Test?

  • Exceptions
  • The output
  • The end result

API Testing

Stop manually testing using rest clients

Why?

  • Verify information you send to consumers
    • Response codes
  • Confident in refactoring (API versionning)
  • Easy to document breaking changes

What to Test?

  • A lot like a command line test
  • Response codes
  • Test different view formats (JSON, XML, HTML)
  • The end result
  • Idempotency

Testing Forms

Stop manually refreshing your browser

Why?

What to Test?

  • Form type
  • Added constraints
  • Translation keys
  • Do not need to manually test forms
    • validation
    • translation
    • data types

Use Symfony's TypeTestCase

Testing Controllers

  • Nothing to cover but the UI rendering
  • Other tests were made to ease out the testing of the controller
  • Covered by light acceptance testing or functional tests

Testing Translations

  • JMSTranslationBundle
  • Command line test for extraction
    • ran only by the CI
  • Integration test for translated keys

Testing Configuration

When?

What to Test?

  • Custom constraints
  • Exception handling
  • Configuration choices
  • Business logic in your configuration

Testing Mailing

  • SwiftMailer Collector
/**
 * @author Steven Rosato <steven.rosato@majisti.com>
 * @group mail
 */
class MailSendingTest extends IntegrationTest
{
    /**
     * @test
     * @group mail.messageCount
     */
    public function itShouldSendAContactEmailToUserAndAdmin()
    {
        $this->fireMailer();
        $collector = $this->getCollector();

        $this->checkThat($collector, is(notNullValue()));
        $this->checkThat($collector->getMessageCount(), is(equalTo(2)));
    }

    /**
     * @return MessageDataCollector
     */
    private function getCollector()
    {
        return $this->getClient()->getProfile()->getCollector('swiftmailer');
    }
}

Database Testing

What to do? *

  • Use Doctrine2
  • Database caching with SQLite
    • Schema only loading
    • Full fixtures loading
  • Database transactions for every test case
  • Database snapshots

* Come talk to me for more info. This subject could cover an entire presentation by itself

Last but not least... Frontend Testing

  • Same approach (Acceptance, Functional, Integration, Unit)
    • Off load throughout the testing stack
    • Reactive pattern let's you test behavior of single components
    • Integration testing let's you test a full slider, for exemple
    • Protractor / chimp.js for acceptance. PhantomJs again!

Conclusion

  • Parallel testing
  • Automatic production deployment and rollback
  • Test performance with Vagrant and VMWare (VirtualBox is slow, even with NFS)

Stuff to still dig in...

Wow this is a lot!

  • Still fits for small and medium projects

Conclusion

Read us ranting

  • Symfony application development
  • Consultation with video recording
  • Friendly expert advice

Come blog with us

Symfony Experts

PHP Québec 07/04/2016. Full Stack Testing with Symfony.

By Solutions Majisti

PHP Québec 07/04/2016. Full Stack Testing with Symfony.

Full Stack Testing with Symfony

  • 1,107