Pro Yearly is on sale from $80 to $50! »

Hexagonal BDD

Ffbc1edf730289e764d67474349a8021?s=47 scato
June 23, 2016

Hexagonal BDD

Ffbc1edf730289e764d67474349a8021?s=128

scato

June 23, 2016
Tweet

Transcript

  1. Hexagonal BDD Scato Eggen @scataco

  2. Hexagonal BDD Let’s get started!

  3. Setup Instructions Clone https://github.com/scato/hexagonal-bdd.git and follow the instructions in README.md

  4. Overview • User Stories • Head First: Behat • Hexagonal

    Architecture • Head First: PHPSpec • Test Driven Development • Test Doubles • Behaviour Driven Development • Test Terminology
  5. User Stories

  6. User Stories Feature: Start a new game In order to

    practice my tic-tac-toe skills As a player I need to be able to start a new game Scenario: Choose to go first Given I have not started a game yet When I start a game as player "X" Then I should see an empty board
  7. User Stories “Software requirements is a communication problem” – Mike

    Cohn As a … I want … So that … role behavior business value
  8. User Stories: Example As a player I want to start

    a new game of tic-tac-toe So that I can practice my tic-tac-toe skills
  9. User Stories: Roles As a player • Identify the roles

    used to interact with the system • Look at features from the user’s point of view
  10. User Stories: Behavior I want to start a new game

    of tic-tac-toe • Focus on the intent of the user • Determine conditions of satisfaction
  11. User Stories: Business Value So that I can practice my

    tic-tac-toe skills • Provide background for making informed guesses • Focus on adding value
  12. User Stories: Conditions of Satisfaction • Verify that a game

    has been started. • Verify that the board is empty at the start of the game. • Also known as: acceptance criteria
  13. Behaviour Driven Design “Acceptance criteria should be executable” – Dan

    North Given some initial context (the givens) When an event occurs Then ensure some outcomes
  14. Head First: Behat

  15. Head First: Behat

  16. Behat: Getting Started $ vendor/bin/behat --init # features/start-game.feature Feature: Start

    a new game In order to practice my tic-tac-toe skills As a player I need to be able to start a new game
  17. Behat: Scenarios # features/start-game.feature Feature: Start a new game In

    order to practice my tic-tac-toe skills As a player I need to be able to start a new game Scenario: Choose to go first Given I have not started a game yet When I start a game as player "X" Then I should see an empty board
  18. Behat: Step Definitions class FeatureContext implements Context, SnippetAcceptingContext { //

    ... } $ vendor/bin/behat --append-snippets
  19. Behat: Step Definitions Scenario: Choose to go first Given I

    have not started a game yet TODO: write pending definition When I start a game as player "X" Then I should see an empty board 1 scenario (1 pending) 3 steps (1 pending, 2 skipped) 0m0.03s (10.87Mb)
  20. Behat: Step Definitions /** * @Given I have not started

    a game yet */ public function iHaveNotStartedAGameYet() { throw new PendingException(); }
  21. Behat: Step Definitions /** * @Given I have not started

    a game yet */ public function iHaveNotStartedAGameYet() { $this->gameRepository = new FakeGameRepository([]); }
  22. Behat: Fakes // features/bootstrap/Fake/FakeGameRepository.php namespace Fake; class FakeGameRepository { private

    $games; public function __construct(array $games) { $this->games = $games; } }
  23. Behat: Step Definitions Scenario: Choose to go first Given I

    have not started a game yet When I start a game as player "X" TODO: write pending definition Then I should see an empty board 1 scenario (1 pending) 3 steps (1 passed, 1 pending, 1 skipped) 0m0.03s (10.87Mb)
  24. Behat: Step Definitions /** * @When I start a game

    as player :arg1 */ public function iStartAGameAsPlayer($arg1) { throw new PendingException(); }
  25. Hexagonal Architecture

  26. Hexagonal Architecture “Allow an application to equally be driven by

    users, programs, automated test or batch scripts, and to be developed and tested in isolation from its eventual run-time devices and databases.” – Alistair Cockburn • UI tests are slow and brittle. • Business logic always creeps into the user interface code. • Also known as “ports & adapters”.
  27. Hexagonal Architecture up and down don’t mean anything

  28. Hexagonal Architecture getting rid of up and down

  29. Hexagonal Architecture inside and outside actually mean something

  30. Growing Object-Oriented Software “Acceptance tests are customer-facing tests that capture

    the domain logic the system must perform and demonstrates that it performs them.” – Nat Pryce • Use Ports & Adapters to run acceptance-tests directly against the application domain model. • The domain model is cleanly decoupled from the technical infrastructure that connects it to the outside world.
  31. Growing Object-Oriented Software

  32. “A project needs a common language that is more robust

    than the lowest common denominator.” – Eric Evans • Domain experts should object to terms or structures that are awkward or inadequate to convey domain understanding • Developers should watch for ambiguity or inconsistency that will trip up design. Ubiquitous Language
  33. Modelling by Example “By embedding Ubiquitous Language in your scenarios,

    your scenarios naturally become your domain model, which you can use to develop the most important part of your application - a core domain.” – Konstantin Kudryashov • Push for Ubiquitous Language in your scenarios. • Do Domain-Driven Design while you're doing the red- green-refactor cycle.
  34. Hexagonal Architecture 1. The application core is where the business

    value is at. 2. The user interface and persistence should be an afterthought. 3. If it’s easy to test, it’s probably hexagonal. 4. Don’t test for things that are not in the Ubiquitous Language.
  35. Head First: PHPSpec

  36. Head First: PHPSpec

  37. PHPSpec: red-green-refactor Scenario: Choose to go first Given I have

    not started a game yet When I start a game as player "X" TODO: write pending definition Then I should see an empty board 1 scenario (1 pending) 3 steps (1 passed, 1 pending, 1 skipped) 0m0.03s (10.87Mb)
  38. PHPSpec: red-green-refactor /** * @When I start a game as

    player :arg1 */ public function iStartAGameAsPlayer($arg1) { throw new PendingException(); }
  39. PHPSpec: red-green-refactor /** * @When I start a game as

    player :arg1 */ public function iStartAGameAsPlayer($arg1) { $command = new StartGameCommand(); $command->gameId = self::GAME_ID; $command->playerName = $arg1; $handler = new StartGameHandler(); $handler->handle($command); }
  40. PHPSpec: red-green-refactor $ vendor/bin/phpspec run 1 specs 1 example (1

    broken) 18ms Do you want me to create `StartGameCommand` for you? [Y/n] $ vendor/bin/phpspec desc Application/StartGameCommand
  41. PHPSpec: red-green-refactor class StartGameCommandSpec extends ObjectBehavior { function it_should_have_a_game_id() {

    $this->gameId->shouldBeNull(); } }
  42. PHPSpec: red-green-refactor class StartGameCommand { /** * @var string */

    public $gameId; }
  43. PHPSpec: red-green-refactor class StartGameCommandSpec extends ObjectBehavior { function it_should_have_a_player_name() {

    $this->playerName->shouldBeNull(); } }
  44. PHPSpec: red-green-refactor class StartGameCommand { /** * @var string */

    public $playerName; }
  45. PHPSpec: red-green-refactor $ vendor/bin/phpspec desc Application/StartGameHandler

  46. PHPSpec: red-green-refactor class StartGameHandlerSpec extends ObjectBehavior { function it_should_start_a_game() {

    $command = new StartGameCommand(); $this->handle($command); } }
  47. PHPSpec: red-green-refactor /** * @When I start a game as

    player :arg1 */ public function iStartAGameAsPlayer($arg1) { $command = new StartGameCommand(); $command->gameId = self::GAME_ID; $command->playerName = $arg1; $handler = new StartGameHandler(); $handler->handle($command); }
  48. PHPSpec: red-green-refactor /** * @When I start a game as

    player :playerName */ public function iStartAGameAsPlayer($playerName) { $command = new StartGameCommand(); $command->gameId = self::GAME_ID; $command->playerName = $playerName; $handler = new StartGameHandler(); $handler->handle($command); }
  49. PHPSpec: red-green-refactor Scenario: Choose to go first Given I have

    not started a game yet When I start a game as player "X" Then I should see an empty board TODO: write pending definition 1 scenario (1 pending) 3 steps (2 passed, 1 pending) 0m0.03s (10.87Mb)
  50. PHPSpec: red-green-refactor /** * @Then I should see an empty

    board */ public function iShouldSeeAnEmptyBoard() { $query = new GetGameQuery(); $query->gameId = self::GAME_ID; $handler = new GetGameHandler(); $result = $handler->handle($query); PHPUnit_Framework_Assert::assertTrue($result->getBoard()->isEmpty()); }
  51. PHPSpec: red-green-refactor Application\GetGameQuery • it should have a game id

    Application\GetGameHandler • it should get a game
  52. PHPSpec: red-green-refactor class GetGameHandlerSpec extends ObjectBehavior { function it_should_get_a_game() {

    } }
  53. PHPSpec: red-green-refactor Application\Game\Board • it should start out empty

  54. PHPSpec: red-green-refactor class Board { public function isEmpty() { return

    true; } }
  55. PHPSpec: red-green-refactor Application\Game\Game • it should have an id •

    it should have a board Application\Game\GameRepository • get(UuidInterface $id): Game
  56. PHPSpec: red-green-refactor class GameSpec extends ObjectBehavior { function let(UuidInterface $id)

    { $this->beConstructedWith($id); } function it_should_have_an_id(UuidInterface $id) { $this->getId()->shouldBe($id); } }
  57. PHPSpec: red-green-refactor interface GameRepository { /** * @param UuidInterface $id

    * @return Game * @throws RuntimeException if game was not found */ public function get(UuidInterface $id); }
  58. PHPSpec: red-green-refactor Application\GetGameHandler • it should get a game

  59. PHPSpec: red-green-refactor class GetGameHandlerSpec extends ObjectBehavior { function let( GameRepository

    $gameRepository, UuidFactoryInterface $uuidFactory, UuidInterface $gameId, Game $game ) { $this->beConstructedWith($gameRepository, $uuidFactory); $uuidFactory->fromString('1234')->willReturn($gameId); $gameRepository->get($gameId)->willReturn($game); } }
  60. PHPSpec: red-green-refactor class GameRepositoryStub implements GameRepository { public function get(UuidInterface

    $id) { // TODO: Implement get() method. } }
  61. PHPSpec: red-green-refactor Scenario: Choose to go first Given I have

    not started a game yet When I start a game as player "X" Then I should see an empty board Game with ID xxxxxxxx not found (RuntimeException) 1 scenario (1 failed) 3 steps (2 passed, 1 failed) 0m0.03s (10.87Mb)
  62. Are We There Yet?

  63. Behaviour Driven Development

  64. PHPSpec: red-green-refactor class StartGameHandlerSpec extends ObjectBehavior { function it_should_start_a_game( GameRepository

    $gameRepository, Game $game ) { $command = new StartGameCommand(); $this->handle($command); $gameRepository->add($game)->shouldHaveBeenCalled(); } }
  65. PHPSpec: red-green-refactor class StartGameHandlerSpec extends ObjectBehavior { function let( GameRepository

    $gameRepository, GameFactory $gameFactory, UuidInterface $gameId, Game $game ) { $this->beConstructedWith($gameRepository, $gameFactory); $gameFactory->create($gameId)->willReturn($game); } }
  66. PHPSpec: red-green-refactor Scenario: Choose to go first Given I have

    not started a game yet When I start a game as player "X" Then I should see an empty board 1 scenario (1 pending) 3 steps (3 passed) 0m0.03s (10.87Mb)
  67. PHPSpec: red-green-refactor Use cases: • StartGameCommand • GetGameQuery Ports: •

    GameRepository Adapters: • FakeGameRepository
  68. Next Scenario

  69. Next Scenario • Scenario: Make a move • Test Driven

    Development • Test Doubles • Behaviour Driven Development
  70. Scenario: Make a move # features/start-game.feature Scenario: Make a move

    Given I have started a game as player "X" When I make a move Then I should see a board with one symbol on it
  71. Scenario: Make a move

  72. Test Driven Development

  73. Test Driven Development “Test-driven development is a way of managing

    fear during programming.” – Kent Beck • Write new code only if you first have a failing automated test. • Eliminate duplication.
  74. Test Driven Development Red-Green-Refactor 1. Write a failing test 2.

    Make the test pass 3. Refactor
  75. Test Driven Development Step 1: Write a failing test •

    Arrange, Act, Assert • Listen for design feedback • Make it compile* *) make it run up to the first assertion.
  76. Test Driven Development: Design Feedback • Keep It Simple, Stupid

    (KISS) • You Ain’t Gonna Need It (YAGNI) • Don’t Repeat Yourself (DRY) • Low Coupling
  77. Test Driven Development Step 2: Make the test pass •

    Fake it • Triangulation • Obvious implementation
  78. Test Driven Development: Fake It function testOneAndOneIsTwo() { $c =

    add(1, 1); $this->assertEquals(2, $c); } function add($a, $b) { return 2; }
  79. Test Driven Development: Eliminate Duplication function testOneAndOneIsTwo() { $c =

    add(1, 1); $this->assertEquals(1 + 1, $c); } function add($a, $b) { return 1 + 1; }
  80. Test Driven Development: Triangulation function testOneAndTwoIsThree() { $c = add(1,

    2); $this->assertEquals(3, $c); } function add($a, $b) { return 1 + $b; }
  81. Test Driven Development Step 3: Refactor • Remove duplication •

    Improve naming • Improve readability • Preparatory refactoring
  82. Scenario: Make a move

  83. Test Doubles

  84. Test Doubles “The word "mock" is sometimes used in an

    informal way to refer to the whole family of objects that are used in tests. They are called Test Doubles.” – Robert C. Martin • Dummy • Stub • Fake • Spy • Mock
  85. Test Doubles: Prophecy $prophet = new Prophet(); $testDouble = $prophet->prophesize(Authorizer::class);

    // set up test double $authorizer = $testDouble->reveal();
  86. Test Doubles: PHPSpec function let(Authorizer $testDouble) { $this->beConstructedWith($testDouble); } function

    it_should_(Authorizer $testDouble) { // use test double }
  87. Test Doubles: Dummy function let(Authorizer $dummy) { $this->beConstructedWith($dummy); } function

    it_should_start_without_logins() { $this->loginCount()->shouldBe(0); }
  88. Test Doubles: Stub function it_should_count_logins(Authorizer $stub) { $stub->authorize(Argument::any(), Argument::any()) ->willReturn(true);

    $this->login('user', 'pass'); $this->loginCount()->shouldBe(1); }
  89. Test Doubles: Fake function it_should_count_logins(Authorizer $fake) { $fake->authorize('Bob', 'bobspassword') ->willReturn(true);

    $this->login('Bob', 'bobspassword'); $this->loginCount()->shouldBe(1); }
  90. Test Doubles: Spy function it_should_authorize_users(Authorizer $spy) { $this->login('user', 'pass'); $spy->authorize('user',

    'pass') ->shouldHaveBeenCalled(); }
  91. Test Doubles: Mock function it_should_authorize_users(Authorizer $mock) { $mock->authorize('user', 'pass') ->willReturn(true)

    ->shouldBeCalled(); $this->login('user', 'pass'); }
  92. Scenario: Make a move

  93. Behaviour Driven Development

  94. Behaviour Driven Development • Test method names should be sentences

    • A simple sentence template keeps test methods focused • An expressive test name is helpful when a test fails • “Behaviour” is a more useful word than “test” • JBehave PHPSpec emphasizes behaviour over testing • Determine the next most important behaviour • Requirements are behaviour, too • BDD provides a “ubiquitous language” for analysis • Acceptance criteria should be executable from Introducing BDD by Dan North
  95. Test method names should be sentences phpunit --testdox BankAccountTest PHPUnit

    5.3.0 by Sebastian Bergmann and contributors. BankAccount [x] Balance is initially zero [x] Balance cannot become negative
  96. A simple sentence template keeps test methods focused phpunit --testdox

    BankAccountTest PHPUnit 5.3.0 by Sebastian Bergmann and contributors. BankAccount [x] Should initially have a balance of zero [x] Should never have a negative balance
  97. An expressive test name is helpful when a test fails

    • I had introduced a bug. Bad me. Solution: Fix the bug. • The intended behaviour was still relevant but had moved elsewhere. Solution: Move the test and maybe change it. • The behaviour was no longer correct – the premise of the system had changed. Solution: Delete the test.
  98. “Behaviour” is a more useful word than “test” Helps to

    answer questions about TDD: • What should you call your test? • How much should you cover in one test? • When a test fails, what should you do?
  99. PHPSpec emphasizes behaviour over testing • Specification • Example •

    Describe • Behaviour • Matcher • Expectation The word “test” is nowhere to be found.
  100. Determine the next most important behaviour “The customer doesn’t want

    your code, they want the value of the software.” – Marcello Duarte • Determine what objects should do before you decide how they should do it. (See also: CRC cards.) • Objects expose behaviour, not data, so you have to tell them what to do with the data. (Tell, Don’t Ask)
  101. CRC cards Class Responsibilities Collaborators

  102. Requirements are behaviour, too Requirements are responsibilities at the application

    level
  103. BDD provides a “ubiquitous language” for analysis Feature: • As

    a • I want • So that Class: • Arrange • Act • Assert Scenario: • Given • When • Then Story BDD Spec BDD
  104. Acceptance criteria should be executable Gherkin

  105. Scenario: Make a move

  106. Are We There Yet?

  107. Behaviour Driven Development

  108. Test Terminology

  109. Test Terminology

  110. Unit Tests

  111. Integration Tests

  112. Acceptance Tests

  113. System Tests

  114. Component Tests Limit the scope of the exercised software to

    a portion of the system under test
  115. Functional Tests Verify correct that the correct behaviour is performed

  116. Non-Functional Tests Check for requirements like performance and security

  117. Smoke Tests You plug in a new board and turn

    on the power. If you see smoke coming from the board, turn off the power.
  118. Learning Tests Instead of just using a new method or

    a new class, we write a little test that verifies that the API works as expected.
  119. Regression Tests Verify that software still performs correctly after a

    change.
  120. System Test

  121. System Test # behat.yml default: suites: acceptance: contexts: - FeatureContext

  122. System Test # behat.yml default: suites: system: contexts: - WebFeatureContext

    - Behat\MinkExtension\Context\MinkContext filters: tags: '@critical'
  123. System Test # behat.yml default: extensions: Behat\MinkExtension: base_url: http://192.168.33.99/app_dev.php sessions:

    default: goutte: ~
  124. System Test # features/start-game.feature @critical Scenario: Make a move Given

    I have started a game as player "X" When I make a move Then I should see a board with one symbol on it
  125. System Test /** * @BeforeScenario */ public function loadMinkExtension(BeforeScenarioScope $scope)

    { $environment = $scope->getEnvironment(); $this->minkContext = $environment->getContext(MinkContext::class); }
  126. System Test /** * @Given I have started a game

    as player :playerName */ public function iHaveStartedAGameAsPlayer($playerName) { $this->minkContext->visit('/'); $this->minkContext->pressButton(''); }
  127. Integration Test

  128. Integration Test • Overloaded term • Test an abstraction that

    we own but have implemented with some third-party package • Don’t mock what you don’t own
  129. Integration Test // tests/AppBundle/Repository/DoctrineGameRepositoryTest.php class DoctrineGameRepositoryTest extends KernelTestCase { /**

    * @test */ public function shouldGetGames() { } }
  130. Integration Test /** * @test */ public function shouldGetGames() {

    static::bootKernel(); $entityManager = static::$kernel->getContainer() ->get('doctrine.orm.default_entity_manager'); $repository = $entityManager->getRepository(Game::class); $gameId = Uuid::uuid4(); $game = $repository->get($gameId); $this->assertInstanceOf(Game::class, $game); }
  131. Integration Test /vagrant$ vendor/bin/phpunit PHPUnit 5.3.2 by Sebastian Bergmann and

    contributors. .E 2 / 2 (100%) Time: 1.35 seconds, Memory: 18.00Mb There was 1 error: 1) AppBundle\Repository\DoctrineGameRepositoryTest::shouldGetGames Doctrine\Common\Persistence\Mapping\MappingException: The class 'Application\Game\Game' was not found in the chain configured namespaces FAILURES! Tests: 2, Assertions: 2, Errors: 1.
  132. Integration Test doctrine: orm: auto_generate_proxy_classes: "%kernel.debug%" naming_strategy: doctrine.orm.naming_strategy.underscore mappings: app:

    mapping: true type: yml dir: "%kernel.root_dir%/../src/AppBundle/Resources" prefix: "Application" is_bundle: false
  133. Integration Test # src/AppBundle/Resources/Game.Game.orm.yml Application\Game\Game: type: entity id: id: type:

    string
  134. Integration Test # src/AppBundle/Resources/Game.Game.orm.yml Application\Game\Game: type: entity repositoryClass: \AppBundle\Repository\DoctrineGameRepository id:

    id: type: string
  135. Integration Test public function setUp() { static::bootKernel(); $application = new

    Application(static::$kernel); $application->setAutoExit(false); $input = new StringInput('doctrine:schema:create'); $output = new BufferedOutput(); $exitCode = $application->run($input, $output); $this->assertEquals(0, $exitCode, $output->fetch()); }
  136. Integration Test # app/config/config_test.yml doctrine: dbal: driver: pdo_sqlite

  137. Integration Test doctrine: dbal: types: uuid_binary: class: Ramsey\Uuid\Doctrine\UuidBinaryType

  138. Integration Test /** * @test */ public function shouldGetGames() {

    // ... $this->assertInstanceOf(Game::class, $game); $this->assertInstanceOf(Uuid::class, $game->getId()); $this->assertInstanceOf(Board::class, $game->getBoard()); }
  139. Integration Test // tests/AppBundle/Repository/DoctrineGameRepositoryTest.php class DoctrineGameRepositoryTest extends KernelTestCase { /**

    * @test */ public function shouldFailToGetNonExistentGame() { $this->expectException(RuntimeException::class); } }
  140. Integration Test // tests/AppBundle/Repository/DoctrineGameRepositoryTest.php class DoctrineGameRepositoryTest extends KernelTestCase { /**

    * @test */ public function shouldAddGames() { } }
  141. Integration Test // tests/AppBundle/Controller/DefaultControllerTest.php class DefaultControllerTest extends WebTestCase { public

    function testIndex() { $client = static::createClient(); $crawler = $client->request('GET', '/'); $this->assertEquals(200, $client->getResponse()->getStatusCode()); $this->assertCount(1, $crawler->filter('form[action$="/start"]')); $this->assertCount(1, $crawler->filter('form[action$="/start"] button')); } }
  142. Integration Test public function shouldStartAGame() { $prophet = new Prophet();

    $startGameHandler = $prophet->prophesize(StartGameHandler::class); $client = static::createClient(); $client->getKernel()->getContainer() ->set('app.start_game_handler', $startGameHandler->reveal()); $client->request('POST', '/start'); $startGameHandler->handle(Argument::type(StartGameCommand::class)) ->shouldHaveBeenCalled(); }
  143. Integration Test // tests/AppBundle/AppBundleTest public function shouldExposeStartGameHandler() { static::bootKernel(); $startGameHandler

    = static::$kernel->getContainer()->get('app.start_game_handler'); $this->assertInstanceOf(StartGameHandler::class, $startGameHandler); }
  144. Next Feature

  145. Behaviour Driven Development # features/play-against-computer.feature Feature: Play against the computer

    In order to practice by myself As a player I want to play against the computer Scenario: Choose to go second Given I have not started a game yet When I start a game as player "O" Then I should see a board with one symbol on it Scenario: Computer makes the second move Given I have started a game as player "X" When I make a move Then I should see a board with two symbols on it
  146. Round Up

  147. Overview • User Stories • Hexagonal Architecture • Test Driven

    Development • Test Doubles • Behaviour Driven Development • Test Terminology
  148. Test Driven Development • Writing tests is a divergent process

    • Making a test pass is a convergent process • Alternate between formulating a problem and finding a solution
  149. Hexagonal Architecture • The application core is about business value

    • Adapters are about getting things to work • Alternate between the business domain and Google
  150. Behaviour Driven Development • Unit tests provide inner quality •

    Acceptance tests provide outer quality • Alternate between what the system is and what the system does
  151. thnx @scataco