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