Hexagonal BDD

Ffbc1edf730289e764d67474349a8021?s=47 scato
June 23, 2016

Hexagonal BDD

Ffbc1edf730289e764d67474349a8021?s=128

scato

June 23, 2016
Tweet

Transcript

  1. 4.

    Overview • User Stories • Head First: Behat • Hexagonal

    Architecture • Head First: PHPSpec • Test Driven Development • Test Doubles • Behaviour Driven Development • Test Terminology
  2. 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
  3. 7.

    User Stories “Software requirements is a communication problem” – Mike

    Cohn As a … I want … So that … role behavior business value
  4. 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
  5. 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
  6. 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
  7. 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
  8. 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
  9. 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
  10. 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
  11. 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
  12. 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)
  13. 20.

    Behat: Step Definitions /** * @Given I have not started

    a game yet */ public function iHaveNotStartedAGameYet() { throw new PendingException(); }
  14. 21.

    Behat: Step Definitions /** * @Given I have not started

    a game yet */ public function iHaveNotStartedAGameYet() { $this->gameRepository = new FakeGameRepository([]); }
  15. 22.

    Behat: Fakes // features/bootstrap/Fake/FakeGameRepository.php namespace Fake; class FakeGameRepository { private

    $games; public function __construct(array $games) { $this->games = $games; } }
  16. 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)
  17. 24.

    Behat: Step Definitions /** * @When I start a game

    as player :arg1 */ public function iStartAGameAsPlayer($arg1) { throw new PendingException(); }
  18. 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”.
  19. 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.
  20. 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
  21. 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.
  22. 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.
  23. 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)
  24. 38.

    PHPSpec: red-green-refactor /** * @When I start a game as

    player :arg1 */ public function iStartAGameAsPlayer($arg1) { throw new PendingException(); }
  25. 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); }
  26. 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
  27. 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); }
  28. 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); }
  29. 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)
  30. 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()); }
  31. 51.

    PHPSpec: red-green-refactor Application\GetGameQuery • it should have a game id

    Application\GetGameHandler • it should get a game
  32. 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
  33. 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); } }
  34. 57.

    PHPSpec: red-green-refactor interface GameRepository { /** * @param UuidInterface $id

    * @return Game * @throws RuntimeException if game was not found */ public function get(UuidInterface $id); }
  35. 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); } }
  36. 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)
  37. 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(); } }
  38. 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); } }
  39. 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)
  40. 69.

    Next Scenario • Scenario: Make a move • Test Driven

    Development • Test Doubles • Behaviour Driven Development
  41. 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
  42. 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.
  43. 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.
  44. 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
  45. 77.

    Test Driven Development Step 2: Make the test pass •

    Fake it • Triangulation • Obvious implementation
  46. 78.

    Test Driven Development: Fake It function testOneAndOneIsTwo() { $c =

    add(1, 1); $this->assertEquals(2, $c); } function add($a, $b) { return 2; }
  47. 79.

    Test Driven Development: Eliminate Duplication function testOneAndOneIsTwo() { $c =

    add(1, 1); $this->assertEquals(1 + 1, $c); } function add($a, $b) { return 1 + 1; }
  48. 80.

    Test Driven Development: Triangulation function testOneAndTwoIsThree() { $c = add(1,

    2); $this->assertEquals(3, $c); } function add($a, $b) { return 1 + $b; }
  49. 81.

    Test Driven Development Step 3: Refactor • Remove duplication •

    Improve naming • Improve readability • Preparatory refactoring
  50. 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
  51. 87.

    Test Doubles: Dummy function let(Authorizer $dummy) { $this->beConstructedWith($dummy); } function

    it_should_start_without_logins() { $this->loginCount()->shouldBe(0); }
  52. 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
  53. 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
  54. 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
  55. 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.
  56. 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?
  57. 99.

    PHPSpec emphasizes behaviour over testing • Specification • Example •

    Describe • Behaviour • Matcher • Expectation The word “test” is nowhere to be found.
  58. 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)
  59. 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
  60. 110.
  61. 114.
  62. 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.
  63. 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.
  64. 122.

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

    - Behat\MinkExtension\Context\MinkContext filters: tags: '@critical'
  65. 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
  66. 125.

    System Test /** * @BeforeScenario */ public function loadMinkExtension(BeforeScenarioScope $scope)

    { $environment = $scope->getEnvironment(); $this->minkContext = $environment->getContext(MinkContext::class); }
  67. 126.

    System Test /** * @Given I have started a game

    as player :playerName */ public function iHaveStartedAGameAsPlayer($playerName) { $this->minkContext->visit('/'); $this->minkContext->pressButton(''); }
  68. 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
  69. 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); }
  70. 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.
  71. 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
  72. 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()); }
  73. 138.

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

    // ... $this->assertInstanceOf(Game::class, $game); $this->assertInstanceOf(Uuid::class, $game->getId()); $this->assertInstanceOf(Board::class, $game->getBoard()); }
  74. 139.

    Integration Test // tests/AppBundle/Repository/DoctrineGameRepositoryTest.php class DoctrineGameRepositoryTest extends KernelTestCase { /**

    * @test */ public function shouldFailToGetNonExistentGame() { $this->expectException(RuntimeException::class); } }
  75. 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')); } }
  76. 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(); }
  77. 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); }
  78. 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
  79. 146.
  80. 147.

    Overview • User Stories • Hexagonal Architecture • Test Driven

    Development • Test Doubles • Behaviour Driven Development • Test Terminology
  81. 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
  82. 149.

    Hexagonal Architecture • The application core is about business value

    • Adapters are about getting things to work • Alternate between the business domain and Google
  83. 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