Behat+Mink+PhantomJS = Test ALL THE THINGS!

Behat+Mink+PhantomJS = Test ALL THE THINGS!

Sometimes simple unit testing is not enough. There is more to it than that when you want to test JavaScript and the rendering of your page. In this talk I will show you how you can use Behat+Mink+PhantomJS to accomplish screenshot comparison, test your JavaScript implementations and your PHP code using the same human-readable language: Behat’s Gherkin.

01da6d807a29ad6d49801c0157518148?s=128

Michelle Sanver

April 18, 2015
Tweet

Transcript

  1. @michellesanver Behat+Mink+PhantomJS = TEST ALL THE THINGS* * Maybe not

    all the things
  2. @michellesanver

  3. @michellesanver WIIIIIIIE \o/ “Learn the most by sharing your knowledge

    with others” - @coderabbi
  4. @michellesanver Audience Do you test your application? (With automatic testing

    not just clicking in a browser)
  5. @michellesanver I would have a picture of grumpy programmer here,

    but I didn’t want to pay the royalties.
  6. @michellesanver “My code is so awesome, I don’t need tests.”

    - Conference attendee
  7. @michellesanver Audience Do you use PHPUnit?

  8. @michellesanver Audience TDD? BDD?

  9. @michellesanver This talk is entry-level.

  10. @michellesanver

  11. @michellesanver

  12. @michellesanver Michelle Sanver

  13. @michellesanver Michelle Sanver Co-President of PHPWomen

  14. @michellesanver PHPWomen 30 dollars

  15. @michellesanver Michelle Sanver Accent(s)!?

  16. @michellesanver Michelle Sanver

  17. @michellesanver I’m a backend developer.

  18. @michellesanver This talk is Open Source

  19. @michellesanver What is Behat anyway?

  20. @michellesanver Behat!? Behat is a Behaviour Driven Development framework

  21. @michellesanver Erm, what is BDD, anyway?

  22. @michellesanver BDD BDD is clearly defined behaviour and English spoken

    tests :D
  23. @michellesanver BDD BDD = TDD + Spices

  24. @michellesanver Why we chose Behat

  25. @michellesanver Gherkin Defining your behaviour

  26. @michellesanver Gherkin Feature: List
 In order to use the shoppinglist


    As a website user
 I need to be able to see the shoppinglist and use its elements
 
 @anonymous
 Scenario: Visit shoppinglist page as anonymous
 Given I am not logged in
 And I am on profile “shopping lists"
 Then I should see “My shopping lists"
 And I should see “I don’t have an M-connect account yet“
 
 @loggedin
 Scenario: See the shoppinglist
 Given I am logged in
 And I am on profile “shopping lists"
 Then I should see "My shopping lists"
 And I should see "I don’t have an M-connect account yet"
  27. @michellesanver Gherkin: Feature Feature: List
 In order to use the

    shoppinglist
 As a website user
 I need to be able to see the shoppinglist and use its elements
 
 @anonymous
 Scenario: Visit shoppinglist page as anonymous
 Given I am not logged in
 And I am on profile “shopping lists"
 Then I should see “My shopping lists"
 And I should see “I don’t have an M-connect account yet“
 
 @loggedin
 Scenario: See the shoppinglist
 Given I am logged in
 And I am on profile “shopping lists"
 Then I should see “My shopping lists"
 And I should see “Active lists"
  28. @michellesanver Gherkin: Scenario Feature: List
 In order to use the

    shoppinglist
 As a website user
 I need to be able to see the shoppinglist and use its elements
 
 Scenario: Larry garfield at a conference
 Given I am Larry Garfield
 And I am at a conference
 Then I should wear “Blue shirt”
 And I should wear “Leather vest"
 
 @loggedin
 Scenario: See the shoppinglist
 Given I am logged in
 And I am on profile “shopping lists"
 Then I should see “My shopping lists"
 And I should see “Active lists"
  29. @michellesanver Gherkin Given, Whens, Thens Scenario: Larry garfield at a

    conference
 Given I am Larry Garfield
 And I am at a conference
 Then I should wear “Blue shirt”
 And I should wear “Leather vest"
  30. @michellesanver Gherkin And + But Scenario: Larry garfield at a

    conference
 Given I am Larry Garfield
 And I am at a conference
 Then I should wear “Blue shirt”
 And I should wear “Leather vest"
  31. @michellesanver Gherkin: Background Feature: Multiple site support
 
 Background:
 Given

    a global administrator named "Greg"
 And a blog named "Greg's anti-tax rants"
 And a customer named "Wilson"
 And a blog named "Expensive Therapy" owned by "Wilson"
 
 Scenario: Wilson posts to his own blog
 Given I am logged in as Wilson
 When I try to post to "Expensive Therapy"
 Then I should see "Your article was published."
 
 Scenario: Greg posts to a client's blog
 Given I am logged in as Greg
 When I try to post to "Expensive Therapy"
 Then I should see "Your article was published."
  32. @michellesanver Gherkin: Scenario Outlines Scenario: Drink 5 out of 12


    Given there are 12 beers
 When I drink 5 beers
 Then I should have 7 beers
 
 Scenario: Drink 5 out of 20
 Given there are 20 beers
 When I drink 5 beers
 Then I should have 15 beers
  33. @michellesanver Gherkin: Scenario Outlines Scenario Outline: Lonestar social
 Given there

    are <start> beers
 When I drink <drink> beers
 Then I should have <left> beers
 
 Examples:
 | start | drink | left |
 | 12 | 5 | 7 |
 | 20 | 5 | 15 | Keep it DRY
  34. @michellesanver Gherkin Tagged Hooks (@loggedin etc that we’ve seen)

  35. @michellesanver Reusable Actions: Steps

  36. @michellesanver Steps Each keyword (given, when etc.) is a step

  37. @michellesanver Steps Automatically add steps by accepting context!

  38. @michellesanver Steps use Behat\Behat\Context\SnippetAcceptingContext;
 use Behat\Gherkin\Node\PyStringNode;
 use Behat\Gherkin\Node\TableNode;
 
 class

    FeatureContext implements SnippetAcceptingContext
 {
 /**
 * Initializes context.
 */
 public function __construct()
 {
 }
 }
  39. @michellesanver Steps: Results Pending

  40. @michellesanver Steps: Results Failed

  41. @michellesanver Steps: Results Skipped

  42. @michellesanver Steps: Results Ambigious

  43. @michellesanver Steps: Results Reduntant

  44. @michellesanver Steps: Results Undefined

  45. @michellesanver SUCCESSFUL \o/ :D

  46. @michellesanver Hooked on a feeling *bamdabadam* Hooks

  47. @michellesanver Hooks Suite Hooks

  48. @michellesanver Hooks Feature Hooks

  49. @michellesanver Hooks Scenario Hooks

  50. @michellesanver Hooks Step Hooks

  51. @michellesanver Hooks /** 
 * @BeforeScenario
 */
 public function before(BeforeScenarioScope

    $scope)
 {
 if($this->getMink()->getDefaultSessionName() == 'selenium2') {
 $this->getSession()->resizeWindow(1200, 768);
 } else {
 self::$client = $this->getSession(‘symfony2') ->getDriver()->getClient();
 self::$container = self::$client->getContainer();
 self::$guzzle = self::$container->get(‘guzzle.api.client’);
 }
 }
  52. @michellesanver Tagged hooks /**
 * @BeforeScenario @database,@orm
 */
 public function

    cleanDatabase()
 {
 // clean database before
 // @database OR @orm scenarios
 }
  53. @michellesanver Tagged hooks: && /**
 * @BeforeScenario @database&&@fixtures
 */
 public

    function cleanDatabase()
 {
 // clean database before
 // @database OR @orm scenarios
 }
  54. @michellesanver Context

  55. @michellesanver Context Feature How your application behaves Context How to

    test your application
  56. @michellesanver Context FeatureContext Class

  57. @michellesanver Context /**
 * @When I do something with :argument


    */
 public function iDoSomethingWith($argument)
 {
 // do something with $argument
 }
  58. @michellesanver Context Contact class requirements

  59. @michellesanver Context: Class requirements 1. The context class should implement

    the Behat\Behat\Context \Context interface.
  60. @michellesanver Context: Class requirements 2. The context class should be

    called FeatureContext.
  61. @michellesanver Context: Class requirements 3. The context class should be

    discoverable and loadable by Behat.
  62. @michellesanver Context Contexts lifetime

  63. @michellesanver Context lifetime Your context class is initialized before each

    scenario is run
  64. @michellesanver Context lifetime 1. Every scenario is isolated from each

    other scenario’s context.
  65. @michellesanver Context lifetime 2. Every step in a single scenario

    is executed inside a common context instance.
  66. @michellesanver Context Multiple contexts

  67. @michellesanver Multiple contexts # behat.yml default: suites: default: contexts: -

    FeatureContext - SecondContext - ThirdContext
  68. @michellesanver Writing TESTS

  69. @michellesanver Writing Tests A test only fails if an exception

    is thrown.
  70. @michellesanver Writing Tests Write simple tests using PHPUnits assertions.

  71. @michellesanver Writing Tests But essentially test ANYTHING YOU’D LIKE, just

    throw exceptions.
  72. @michellesanver Mink

  73. @michellesanver Mink Simulate browser behaviour with Mink

  74. @michellesanver Mink Clicking links!

  75. @michellesanver Mink // wait for animation
 $session->wait(500); 
 // get

    loginlink
 $loginLinkSelector = '.hidden-phone a.login';
 $loginLink = $page->find('css', $loginLinkSelector); 
 // click loginlink
 $loginLink->click();
  76. @michellesanver Mink Drag and drop

  77. @michellesanver Fixtures and Mocking

  78. @michellesanver Fixtures and Mocking

  79. @michellesanver Fixtures and Mocking Any time API responses changed… We

    cried. :’(
  80. @michellesanver Fixtures and Mocking PHP VCR Record HTTP interactions!

  81. @michellesanver PhantomJS Testing JavaScript

  82. @michellesanver PhantomJS Headless browser

  83. @michellesanver Selenium Driver PhantomJS is a ghost!

  84. @michellesanver PhantomJS @javascript
 Scenario: Searching for "tomaten"
 Given I am

    on "/sortiment/groceries"
 When I search for "tomatoes"
 Then I should see “Your search for tomatoes yielded"

  85. @michellesanver PhantomJS /**
 * @When I search for :searchterm
 */


    public function iSearchFor($searchterm)
 {
 
 }
  86. @michellesanver PhantomJS $session = $this->getSession();
 $page = $session->getPage();
 
 $searchField

    = $page->find('css', '.hidden-phone .search-input');
 $searchField->click();
 $searchField->setValue($searchterm);
 
 $searchButton = $page->find('css', '.hidden-phone .search-btn');
 $searchButton->click();
 
 // Wait until we have a search-title with a length longer than 0.
 $session->wait(
 0,
 "$('.search-title').length > 0"
 );
 
 $this->saveScreenshot('debugscreenshot.png', self::DEBUGPATH);

  87. @michellesanver PhantomJS $session = $this->getSession();
 $page = $session->getPage();
 
 $searchField

    = $page->find('css', '.hidden-phone .search-input');
 $searchField->click();
 $searchField->setValue($searchterm);
 
 $searchButton = $page->find('css', '.hidden-phone .search-btn');
 $searchButton->click();
 
 // Wait until we have a search-title with a length longer than 0.
 $session->wait(
 0,
 "$('.search-title').length > 0"
 );
 
 $this->saveScreenshot('debugscreenshot.png', self::DEBUGPATH);

  88. @michellesanver PhantomJS $session = $this->getSession();
 $page = $session->getPage();
 
 $searchField

    = $page->find('css', '.hidden-phone .search-input');
 $searchField->click();
 $searchField->setValue($searchterm);
 
 $searchButton = $page->find('css', '.hidden-phone .search-btn');
 $searchButton->click();
 
 // Wait until we have a search-title with a length longer than 0.
 $session->wait(
 0,
 "$('.search-title').length > 0"
 );
 
 $this->saveScreenshot('debugscreenshot.png', self::DEBUGPATH);

  89. @michellesanver PhantomJS $session = $this->getSession();
 $page = $session->getPage();
 
 $searchField

    = $page->find('css', '.hidden-phone .search-input');
 $searchField->click();
 $searchField->setValue($searchterm);
 
 $searchButton = $page->find('css', '.hidden-phone .search-btn');
 $searchButton->click();
 
 // Wait until we have a search-title with a length longer than 0.
 $session->wait(
 0,
 "$('.search-title').length > 0"
 );
 
 $this->saveScreenshot('debugscreenshot.png', self::DEBUGPATH);

  90. @michellesanver PhantomJS $session = $this->getSession();
 $page = $session->getPage();
 
 $searchField

    = $page->find('css', '.hidden-phone .search-input');
 $searchField->click();
 $searchField->setValue($searchterm);
 
 $searchButton = $page->find('css', '.hidden-phone .search-btn');
 $searchButton->click();
 
 // Wait until we have a search-title with a length longer than 0.
 $session->wait(
 0,
 "$('.search-title').length > 0"
 );
 
 $this->saveScreenshot('debugscreenshot.png', self::DEBUGPATH);

  91. @michellesanver Screenshot comparison Compare image code

  92. @michellesanver Wrapup

  93. @michellesanver Behat and Gherkin A BDD Framework with clearly defined

    behaviours
  94. @michellesanver Contexts and Steps Steps are your tests living in

    a context, which contains your logic for testing
  95. @michellesanver Testing JavaScript Use Mink with a driver, for instance

    PhantomJS to test dynamic parts of your website.
  96. @michellesanver Still not mature for CSS and image comparison.

  97. @michellesanver Behat is Open Source

  98. @michellesanver Got a bit excited about one Testing Framework for

    ALL THE THINGS? As developers in an open source community the decision is ours.
  99. @michellesanver Contribute to my talk!

  100. @michellesanver Thank you!