Presented 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
??
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
Psst... Guru tip #3
You do not have time because skipping automated testing is slower
I ain't got no time for that
We must deliver the project quickly. (RUSH, RUSH, RUSH)
This is only permanently temporary
It is "just" this. It will be simple. (they say)
We have a QA.
If you are the QA, just must leave NOW.
What if you actually took all this time to implement tests?
Are you saying tests do not take more time? Yes. It is actually faster.
You get code that is maintainable.
Maintainable code = more features = client happy
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 GameTest extends UnitTest
{
public function testGamePlayerWins()
{
$deck = m::mock(Deck::class);
$deck->shouldReceive('draw')->once()->andReturn([new Card(10), new Card(7),
new Card(9), new Card(8), new Card(2)]);
$dealerHand = m::mock(Hand::class);
$playerHand = m::mock(Hand::class);
$player = m::mock(Player::class);
$dealer = m::mock(Dealer::class);
$dealerHand->shouldReceive('addCards')->once()->with([new Card(10), new Card(7)]);
$playerHand->shouldReceive('addCards')->once()->with([new Card(9), new Card(8)]);
$playerHand->shouldReceive('addCards')->once()->with([new Card(2)]);
$player->shouldReceive('setHand')->once()->with($playerHand);
$dealer->shouldReceive('setHand')->once()->with($dealerHand);
$game = new Game($dealerHand, $playerHand);
$game->start();
$game->hit($dealer, $deck, 2);
$game->hit($player, $deck, 3);
$this->assertSame($game->getWinner(), $player);
}
}
What's wrong?
Game has:
True story bro. Real world case. Even though I wrote tests, I ended up with god objects because I did not think about the Object's Behaviour.
class GameTest extends UnitTest
{
public function shouldStartGame....????()
{
}
public function shouldDetermineWinner...????()
{
}
}
Thinking object's behaviour forces us to come back to the drawing boards and come up with a better solution for the current problem
namespace Unit\Blackjack;
/**
* @method Game uut()
*
* @property Dealer|m\MockInterface dealer
* @property Player|m\MockInterface player
*/
class GameTest extends UnitTest
{
protected $uut;
protected function setUp()
{
$this->dealer = m::spy(Dealer::class);
$this->player = m::spy(Player::class);
parent::setUp();
}
protected function createUnitUnderTest()
{
$game = new Game();
$game->setDealer($this->dealer);
$game->setPlayer($this->player);
return $game;
}
<?php
namespace Unit\Blackjack;
class GameTest extends UnitTest
{
public function testTellsIfDealerWon()
{
$this->uut()->dealerWins();
$this->verifyThat($this->uut()->hasDealerWon(), is(true));
$this->verifyThat($this->uut()->hasPlayerWon(), is(false));
}
public function testTellsIfGameIsADraw()
{
$this->uut()->setIsDraw();
$this->verifyThat($this->uut()->isDraw(), is(true));
}
public function testTellsIfPlayerWon()
{
$this->uut()->playerWins();
$this->verifyThat($this->uut()->hasPlayerWon(), is(true));
$this->verifyThat($this->uut()->hasDealerWon(), is(false));
}
}
or Component
Mockery
Phake
AspectMock
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:
@navigation
Feature:
As a user
I should be able to navigate the different pages of this site
@homepage
Scenario: Navigate the homepage
Given I am on the homepage
Then I take a screenshot or save last response named "homepage"
@section
Scenario: Navigate to a section page
Given I visited the "Homepage"
When I navigate to the "Section 1" section within the menu
Then I should see a top stories widget
And I should see a 2x2 layout widget
And I should see a slideshow widget
And I should see a multi layout widget
And I should see a videos widget
Then I take a screenshot or save last response named "section"
class ConfigDeploymentCommandCest extends DeploymentCommandCest
{
const COMMAND_TO_RUN = 'ez:legacy:desktop-apps:config';
/**
* @param FunctionalTester $tester
*/
public function seeTheModifiedConfigFileWithNewValues(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()
);
}
}
@functional @blackjack
Feature:
As a player
I should be able to play a game of Blackjack through the Symfony2 CLI
Background:
Given I trick the deck
And I register the Blackjack command
Scenario: Dealer wins by outscoring player
Given the deck returns the following cards in a FILO order:
#dealer cards
| 6 |
| 6 |
#player cards
| 5 |
| 5 |
| 5 |
And I call "stand" when asked for my move
When I run the blackjack game command
Then the player score should be "10"
And the dealer score should be "17"
And the dealer should have won
class BlackJackGameTest extends ComponentTest
{
public function testDealerLosesIfHeBusts()
{
$this->initialDealerCards(new Card(10), new Card(3));
$this->initialPlayerCards(new Card(10), new Card(8));
$this->nextPlayerCards([new Card(3)]);
$this->nextDealerCards([new Card(9)]);
$this->playerWillHitTimes(1);
$game = $this->playEntireGame();
$this->verifyThat($game->getDealer()->hasBusted(), is(true));
$this->verifyThat($game->hasPlayerWon(), is(true));
}
public function testDealerShouldTryToStopAt17()
{
$this->initialDealerCards(new Card(10), new Card(3));
$this->initialPlayerCards(new Card(10), new Card(8));
$this->nextDealerCards([new Card(4), new Card(10)]);
$game = $this->playEntireGame();
$this->verifyThat($game->getDealer()->getBestScore(), is(equalTo(17)));
$this->verifyThat($game->hasPlayerWon(), is(true));
}
//...
}
class BlackJackGameTest extends ComponentTest
{
public function testGameDrawWhenBothPlayersHaveABlackjack()
{
$this->nextCardsFormABlackjack();
$this->nextCardsFormABlackjack();
$game = $this->coordinateGame();
$this->verifyThat($game->isDraw(), is(true));
}
public function testPlayerWillUseBestScoreInOrderToWin()
{
$this->initialDealerCards(new Card(10), new Card(8));
$this->initialDealerCards(new Card(5), new Card(Card::RANK_ACE));
$this->nextDealerCards([new Card(3)]);
$this->playerWillHitTimes(1);
$game = $this->playEntireGame();
$this->verifyThat($game->hasPlayerWon(), is(true));
}
}
<?php
namespace Unit\Blackjack;
/**
* @method Dealer uut()
*
* @property Deck|m\MockInterface deck
* @property Player|m\MockInterface player
*/
class DealerTest extends PlayerTest
{
protected function setUp()
{
$this->player = m::mock(Player::class);
$this->deck = m::mock(Deck::class);
$this->deck->shouldReceive('draw')->andReturn(new Card())->byDefault();
parent::setUp();
}
protected function createUnitUnderTest()
{
return new Dealer($this->deck);
}
}
class DealerTest extends PlayerTest
{
public function testCanGiveCardsToPlayer()
{
$player = m::mock(Player::class);
$player->shouldReceive('receiveCard')
->times(2)
->with(anInstanceOf(Card::class));
$this->uut()->hit($player, 2);
}
public function testDealerCannotDrawIfHeHasMoreThanSixteen()
{
$handCalculator = new HandCalculator();
$this->deck->shouldReceive('draw')->andReturn(new Card(1));
$this->uut()->receiveCards([
new Card(10),
new Card(6),
]);
$this->uut()->calculateHand($handCalculator);
$this->player->shouldReceive('getBestScore')->andReturn(18);
$this->uut()->play($handCalculator);
$this->verifyThat($this->uut()->getBestScore(), equalTo(17));
}
//todo: dealer can hit on a soft 17? Maybe add it as an optional rule (Rulebook object?)
}
Acceptance
Functional
Component
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
We all wait to long to update our vendors
Integration Layer
Functional Layer
Step in the right direction, but we are missing..
Stop manually testing using rest clients
Use database caching
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');
}
}
class Card
{
public static function createRandom(): Card
{
$types = self::SUITS;
$randomType = $types[rand(0, count($types) - 1)];
$randomValue = rand(1, static::CARDS_PER_TYPE_COUNT);
return new static($randomValue, $randomType);
}
}
class CardTest extends UnitTest
{
public function testCanCreateRandomCard()
{
$phpRandFunction = Test::func($this->getUutNamespace(),
'rand', Card::RANK_ACE);
$card = $this->uut()->createRandom();
$this->verifyThat($card, is(anInstanceOf(Card::class)));
$this->verifyThat($card->getRank(), is(equalTo(Card::RANK_ACE)));
$phpRandFunction->verifyInvoked([Card::RANK_ACE]);
}
}
* 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