Presented by
Space allocated by
Symfony Experts
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
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
17h30 - 19h00 Networking
19h00 - 20h00 Presentation
After... Pub Victoria
??
a Boob
a Guru
Don't be a boob
Boob tip #1: You are poorer than you think
Your prototype will go in production
#no-bank
Psst... Guru tip #1
Your project will need maintenance
Guru tip #2
Your client's needs will evolve
TDD: Test Driven Development
BDD: Behaviour Driven Development
DDD: Domain Driven Design approach
<?php
class Counter
{
public function testTick()
{
$counter = new Counter();
$count = $counter->tick();
$this->assertEquals($count, 1);
}
}
What's wrong?
<?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
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?
CustomerAccount has:
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.
/**
* @author Steven Rosato <steven.rosato@majisti.com>
*/
class CustomerAccountTest extends UnitTest
{
public function shouldUpdateAReservation....????()
{
}
}
Thinking object's behaviour forces us to come back to the drawing boards and come up with a better solution for the current problem
/**
* @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);
}
}
/**
* @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
Mockery
Phake
Off load testing through the entire pyramid
<?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:
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()
);
}
}
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);
}
}
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))
);
}
}
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
);
}
}
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();
}
}
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
Nightly builds
On commit build
phamtomjs + xfvb or SauceLabs ($$$) for Acceptance testing
Integration Layer
Functional Layer
Step in the right direction, but we are missing..
Stop manually testing using rest clients
Stop manually refreshing your browser
Use Symfony's TypeTestCase
/**
* @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');
}
}
* Come talk to me for more info. This subject could cover an entire presentation by itself
Stuff to still dig in...
Wow this is a lot!
Read us ranting
Come blog with us
Symfony Experts