Ce diaporama a bien été signalé.
Nous utilisons votre profil LinkedIn et vos données d’activité pour vous proposer des publicités personnalisées et pertinentes. Vous pouvez changer vos préférences de publicités à tout moment.

Behat - Beyond the Basics (2016 - SunshinePHP)

3 552 vues

Publié le

Behat - Beyond the Basics (2016 - SunshinePHP)

Publié dans : Technologie
  • Identifiez-vous pour voir les commentaires

Behat - Beyond the Basics (2016 - SunshinePHP)

  1. 1. Behat: Beyond the Basics @jessicamauerhan @SunshinePHP | 2-5-16 | https://joind.in/talk/9f341
  2. 2. Introduction @jessicamauerhan jessicamauerhan@gmail.com Senior Test Engineer Grovo Learning, Inc.
  3. 3. What is Behavior Driven Development? Behaviour-driven development is an “outside-in” methodology. It starts at the outside by identifying business outcomes, and then drills down into the feature set that will achieve those outcomes. Each feature is captured as a “story”, which defines the scope of the feature along with its acceptance criteria. Dan North, “What’s in a Story” http://dannorth.net/whats-in-a-story It’s the idea that you start by writing human-readable sentences that describe a feature of your application and how it should work, and only then implement this behavior in software. Behat Documentation http://docs.behat.org/en/v2.5
  4. 4. BDD is not about... Well Designed Code Automated Testing Implementation UI Testing
  5. 5. Why Practice Behavior Driven Development? “You can turn an idea for a requirement into implemented, tested, production-ready code simply and effectively, as long as the requirement is specific enough that everyone knows what’s going on.” Dan North, “What’s in a Story” http://dannorth.net/whats-in-a-story
  6. 6. Gherkin Business Readable (Writable?), Domain Specific Language Living Documentation / User Manual Story: Feature File one Feature with a narrative one or more Scenarios Acceptance Criteria: Scenarios
  7. 7. Narrative As a [role] I want [feature] So that [benefit / business reason] Use “5 Whys” to determine narrative Feature: Account Holder Withdraws Cash Feature: Short, Descriptive, Action Acceptance Criteria: Scenarios Given: Exact Context When: Action/Event Then: Outcomes And/But: More of the same... Scenario: Account has sufficient funds Given the account balance is $100 And the card is valid And the machine contains enough money When I request $20 Then the ATM should dispense $20 And the account balance should be $80 And the card should be returned Scenario: Account has insufficient funds Given the account balance is $10 And the card is valid And the machine contains at least the amount of my balance When I request $20 Then the ATM should not dispense any money And the ATM should say there are insufficient funds And my account balance should be $10 And the card should be returned Scenario: Card has been disabled Given the card is disabled When I request $20 Then the ATM should retain the card And the ATM should say the card has been retained As an Account Holder I want to withdraw cash from an ATM So that I can get money when the bank is closed
  8. 8. Gherkin Keywords: Auto-Generated Steps You can implement step definitions for undefined steps with these snippets: /** * @When /^I select Texas from the states list$/ */ public function iSelectTexasFromTheStatesList() { throw new PendingException(); } /** * @Then /^I should see the list of services offered$/ */ public function iShouldSeeTheListOfServicesOffered() { throw new PendingException(); }
  9. 9. BDD is about... Communication Collaboration Documentation Preventing User-Facing Regressions
  10. 10. Writing Features
  11. 11. Feature: View Countdown before Broadcast As a user viewing a broadcast page before the broadcast starts I want to see a countdown timer So that I can know how long until the broadcast actually starts Scenario: View Countdown before Broadcast Given I view the “Future Broadcast” broadcast Then I should see "Future Broadcast" on the page And I should see "Future Broadcast Author" on the page And I should see "This broadcast begins at 6:00 pm EST" on the page And I should see a countdown timer Process Author describes Feature with implementation specific example Developer adds fixture data Issues Test only works before 6pm What is the intent? Confusion for business users
  12. 12. Feature: View Countdown before Broadcast As a user viewing a broadcast page before the broadcast starts I want to see a countdown timer So that I can know how long until the broadcast actually starts Scenario: View Countdown before Broadcast Given there is a broadcast scheduled for the future When I view that broadcast’s page Then I should see the broadcast title And I should see the broadcast author’s name And I should see "This broadcast begins at" followed by the start time in EST And I should see a countdown timer Changes Given now explains exact context Test is no longer time- dependent Intent - not implementation Why? Overall understanding Communication value Help identify poorly written code
  13. 13. Feature: Customer Views Product and Services Catalog As a customer I want to view the product catalog So that I can browse products and services Scenario: Customer views Product Catalog Given I view the catalog When I select "Texas" from the list of states Then I should see the list of products for sale Scenario: Customer views Local Services in Product Catalog Given I view the catalog When I select "Texas" from the list of states Then I should see the list of services offered Issues What is the intent? Is Texas relevant?
  14. 14. Feature: Customer Views Product and Services Catalog As a customer I want to view the product catalog So that I can browse products and services Scenario: Customer views Product Catalog Given I view the catalog When I select "Texas" from the list of states Then I should see the list of products for sale Scenario: Customer views Local Services in Product Catalog Given I view the catalog When I select "Texas" from the list of states Then I should see the list of services offered Issues What is the intent? Is Texas relevant? if not: implementation detail Scenario: Customer views Product Catalog Given I view the catalog When I select a state from the list of states Then I should see the list of products for sale Scenario: Display Local Services in Product Catalog Given I view the catalog When I select a state from the list of states Then I should see the list of services offered
  15. 15. Feature: Customer Views Product and Services Catalog As a customer I want to view the product catalog So that I can browse products and services Scenario: Customer views Product Catalog Given I view the catalog When I select "Texas" from the list of states Then I should see the list of products for sale Scenario: Customer views Local Services in Product Catalog Given I view the catalog When I select "Texas" from the list of states Then I should see the list of services offered Issues What is the intent? Is Texas relevant? if not: implementation detail if yes: uncover business value and context Scenario: Customer views Product Catalog Given I view the catalog When I select a state from the list of states Then I should see the list of products for sale Scenario: Display Local Services in Product Catalog Given the company has a regional office in a state When I view the catalog And I select that state from the list of states Then I should see the list of services offered
  16. 16. Feature: Customer Views Product and Services Catalog As a customer I want to view the product catalog So that I can browse products and services Scenario: Customer views Product Catalog Given I view the catalog When I select "Texas" from the list of states Then I should see the list of products for sale Scenario: Customer views Local Services in Product Catalog Given I view the catalog When I select "Texas" from the list of states Then I should see the list of services offered Narrative Use narrative to explain business value for feature Not processed in Behat 2.5 Behat 3.0+ role can be used to specify which context files are used Feature: Customer Views Texas-Specific Catalog As a customer in Texas I want to view the products and services for sale in Texas So that I can buy them Scenario: Display Local Services when viewing Texas Given I view the catalog And I select Texas from the list of states Then I should see the products for sale in Texas And I should see the list of services offered in Texas
  17. 17. Implementation When I select "Texas" from the states list /** * @When /^I select "([^"]*)" from the states list$/ */ public function iSelectFromTheStatesList($arg1){} Intent When I select Texas from the states list /** * @When /^I select Texas from the states list$/ */ public function iSelectTexasFromTheStatesList(){} Demonstrate Intent by Removing Variables Shows specific intent rather than an example of behavior Indicates there is an important business value to the state Impossible for anyone to plug in another state
  18. 18. Feature: Customer Views Product and Services Catalog As a customer I want to view the product catalog So that I can browse products and services Scenario: Customer views Product Catalog Given I view the catalog When I select a state from the list of states Then I should see the list of products for sale Scenario: Display Local Services in Product Catalog Given the company has a regional office in a state When I view the catalog And I select that state from the list of states Then I should see the list of services offered echo '<h1>Products</h1>'; foreach ($products AS $product) { echo '<p>' . $product->getName() . '</p>'; } echo '<h1>Services</h1>'; foreach ($services AS $service) { echo '<p>' . $service->getName() . '</p>'; } Scenario: Don’t Display Local Services in Product Catalog For States With No Regional Office Given the company does not have a regional office in a state When I view the catalog And I select that state from the list of states Then I should not see a list of services offered
  19. 19. Feature: Customer Views Product and Services Catalog As a customer I want to view the product catalog So that I can browse products and services Scenario: Customer views Product Catalog Given I view the catalog When I select a state from the list of states Then I should see the list of products for sale Scenario: Display Local Services in Product Catalog Given the company has a regional office in a state When I view the catalog And I select that state from the list of states Then I should see the list of services offered Scenario: Don’t Display Local Services in Product Catalog For States With No Regional Office Given the company does not have a regional office in a state When I view the catalog And I select that state from the list of states Then I should not see a list of services offered echo '<h1>Products</h1>'; foreach ($products AS $product) { echo '<p>' . $product->getName() . '</p>'; } if(count($services) > 0) { echo '<h1>Services</h1>'; foreach ($services AS $service) { echo '<p>' . $service->getName() . '</p>'; } }
  20. 20. Writing Great Features Exact Context Independent Scenarios & Features Intention, not Implementation Defined Narrative All Paths Explored Feature “Smells” Time Dependency Interdependency Multi-Scenario Scenarios Missing Scenarios Overuse of Variables Examples of Behavior (Implementation, not Intention)
  21. 21. Behat
  22. 22. Drivers http://mink.behat.org/en/latest Browser Emulator Faster HTTP Info: Response Header, Status Code Real Browser Driver execute JavaScript Tell if element is actually visible on the page
  23. 23. Hooks
  24. 24. Capturing Screenshot on Error /** @AfterScenario */ public function afterScenario($event) { if ($event->getResult() == EventStepEvent::FAILED) { $imageData = $this->getSession()->getDriver()->getScreenshot(); $imagePath = $this->getArtifactsDir() . time() . '.png'; file_put_contents($imagePath, $imageData); } }
  25. 25. Hooks & Tags /** @AfterScenario @javascript */ public function afterScenario($event) { if ($event->getResult() == EventStepEvent::FAILED) { $imageData = $this->getSession()->getDriver()->getScreenshot(); $imagePath = $this->getArtifactsDir() . time() . '.png'; file_put_contents($imagePath, $imageData); } }
  26. 26. Hooks & Tags (Multiple Tags) /** @AfterScenario @javascript,@screenshot */ public function afterScenario($event) { if ($event->getResult() == EventStepEvent::FAILED) { $imageData = $this->getSession()->getDriver()->getScreenshot(); $imagePath = $this->getArtifactsDir() . time() . '.png'; file_put_contents($imagePath, $imageData); } }
  27. 27. Hooks: Dealing with AJAX (jQuery & Angular) /** @BeforeStep @javascript */ public function beforeStep($event) { $waitTime = 5000; $jqDefined = "return (typeof jQuery != 'undefined')"; $active = '(0 === jQuery.active && 0 === jQuery(':animated').length)'; if ($this->getSession()->evaluateScript($jqDefined)) { $this->getSession()->wait($waitTime, $active); } } //Angular: https://gist.github.com/jmauerhan/f839926ea527ff5e74e7
  28. 28. Fixture Data namespace AcmeAppBundleDataFixtures; use DoctrineCommonPersistenceObjectManager; use DoctrineCommonDataFixturesFixtureInterface; class UserFixtureLoader implements FixtureInterface { public function load(ObjectManager $manager) { $user = new User(); $user->setUsername('admin'); $user->setPassword('password'); $manager->persist($user); $manager->flush(); } }
  29. 29. Load Fixture Data /** @BeforeFeature */ public function beforeFeatureReloadDatabase($event) { $loader = new Loader(); $directory = __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'DataFixtures'; $loader->loadFromDirectory($directory); $entityManager = $this->getEntityManager(); $purger = new ORMPurger(); $executor = new ORMExecutor($entityManager, $purger); $executor->execute($loader->getFixtures()); }
  30. 30. Steps
  31. 31. Multiple Regular Expressions /** * @Given /^I view the catalog$/ * @Given /^I am viewing the catalog$/ */ public function iViewTheCatalog(){ $this->getPage('Catalog')->open(); }
  32. 32. Case Insensitive - Flag Given I view the catalog Given I view the Catalog /** * @Given /^I am viewing the catalog$/i */ public function iViewTheCatalog(){ $this->getPage('Catalog')->open(); }
  33. 33. Given I view the catalog Given I view the Catalog /** * @Given /^I am viewing the (?i)catalog$/ */ public function iViewTheCatalog(){ $this->getPage('Catalog')->open(); } Case Insensitive - Inline
  34. 34. Quoted Variables Then I should see an "error" message /** * @Given /^I should see an "([^"])" message$/ */ public function iShouldSeeAnMessage($arg1){ }
  35. 35. Then I should see an error message /** * @Given /^I should see an (.*) message$/ */ public function iShouldSeeAnMessage($arg1){ } Unquoted Variables
  36. 36. Unquoted Variables with List of Options Then I should see an error message Then I should see a success message Then I should see a warning message /** * @Given /^I should see an? (error|success|warning) message$/ */ public function iShouldSeeAnMessage($messageType){ $class = '.alert-'.$messageType; $this->assertElementExists($class, 'css'); }
  37. 37. Optional Variables Then I should see an error message Then I should see an error message that says "Stop!" /** * @Given /^I should see an? (error|success|warning) message$/ * @Given /^I should see an? (error|success|warning) message that says "([^"]*)"$/ */ public function iShouldSeeAnMessageThatSays($messageType, $message = null) { $class = '.alert -' . $messageType; $this->assertElementExists($class, 'css'); if ($message !== null) { $this->assertElementContainsText($class, 'css', $message); } }
  38. 38. Non-Capturing Groups Then I view the catalog for "Texas" Then I am viewing the catalog for "Texas" /** * @Given /^I view the catalog for "([^"]*)"$/ * @Given /^I am viewing the catalog "([^"]*)"$/ */ public function iViewTheCatalogForState($stateName) { $args = ['stateName' => $stateName]; $this->getPage('Catalog')->open($args); }
  39. 39. Non-Capturing Groups Then I view the catalog for "Texas" Then I am viewing the catalog for "Texas" /** * @Given /^I (?:am viewing|view) the catalog for "([^"]*)"$/ */ public function iViewTheCatalogForState($stateName) { $args = ['stateName' => $stateName]; $this->getPage('Catalog')->open($args); }
  40. 40. Step Definition Changes in Behat 3.x Then I view the catalog for "Texas" Then I view the catalog for Texas /** * @Given I view the catalog for :stateName */ public function iViewTheCatalogForState($stateName) { $args = ['stateName' => $stateName]; $this->getPage('Catalog')->open($args); }
  41. 41. Contexts
  42. 42. SubContext (Behat 2.x) namespace AcmeAppBundleContext; use BehatMinkExtensionContextMinkContext; class FeatureContext extends MinkContext { public function __construct(array $parameters) { $this->parameters = $parameters; $this->useContext('MessageContext', new MessageContext()); } }
  43. 43. Several SubContexts (Behat 2.x) [...] public function __construct(array $parameters) { $this->parameters = $parameters; $this->useContext('AdminContext', new AdminContext()); $this->useContext('FormContext', new FormContext()); $this->useContext('EditUserContext', new EditUserContext()); $this->useContext('ApiContext', new ApiContext()); }
  44. 44. Alias All SubContexts Automatically (Behat 2.x) private function loadSubContexts() { $finder = new Finder(); $finder->name('*Context.php') ->notName('FeatureContext.php') ->notName('CoreContext.php'); $finder->files()->in(__DIR__); }
  45. 45. Alias All SubContexts Automatically (Behat 2.x) private function loadSubContexts() { $finder = new Finder(); $finder->name('*Context.php') ->notName('FeatureContext.php') ->notName('CoreContext.php'); $finder->files()->in(__DIR__); foreach ($finder as $file) { $className = $file->getBaseName('.php'); $namespace = __NAMESPACE__ . '' . $file->getRelativePath(); if (substr($namespace, -1) !== '') { $namespace .= ''; } $reflectionClass = new ReflectionClass($namespace . $className); $this->useContext($className, $reflectionClass->newInstance()); } }
  46. 46. <?php namespace AcmeAppBundleContext; class FeatureContext extends CoreContext { /** @Given /^I should see an? (error|success|warning) message that says "([^"])"$/ */ public function iShouldSeeAnMessageThatSays($messageType, $message = null) { $class = '.alert -' . $messageType; $element = $this->getPage()->find('css', $class); $actualMessage = $element->getText(); $this->assertEqual($actualMessage, $message); } } Message Context
  47. 47. Find Required Element Shortcut public function findRequiredElement($locator, $selector = 'xpath', $parent = null) { if (null === $parent) { $parent = $this->getPage(); } $element = $parent->find($selector, $locator); if (null === $element) { throw new ElementNotFoundException($this->getSession(), null, $selector, $locator); } return $element; }
  48. 48. Message Context namespace AcmeAppBundleContext; class FeatureContext extends CoreContext { /** @Given /^I should see an? (w*) message that says "([^"])"$/ */ public function iShouldSeeAnMessageThatSays($messageType, $message = null) { $class = '.alert -' . $messageType; $element = $this->findRequiredElement($class, 'css'); $actualMessage = $element->getText(); $this->assertEqual($actualMessage, $message); } }
  49. 49. CoreContext with Step Annotation causes Error [BehatBehatExceptionRedundantException] Step "/^I should be redirected to "([^"]*)"$/" is already defined in AcmeAppBundleContextFeatureContext::iShouldBeRedirectedTo() AcmeAppBundleContextFeatureContext::iShouldBeRedirectedTo() AcmeAppBundleContextMessageContext::iShouldBeRedirectedTo()
  50. 50. Reusing Multiple Steps
  51. 51. Reusing Multiple Steps Scenario: Upload a csv file Given I am viewing the csv import form When I attach a csv to "Import File" And I submit the form Then I should see a success message And I should see the file review screen Scenario: Review and Confirm the csv file Given I have uploaded a csv And I am viewing the file review screen When I select a property for each column And I submit the form Then I should see a success message
  52. 52. Meta-Steps use BehatBehatContextStep; class FeatureContext { /** @Given /^I have uploaded a csv$/ */ public function iHaveUploadedACsv() { return [ new StepGiven('I am viewing the csv import form'), new StepWhen('I attach a csv to "Import File"'), new StepWhen('I submit the form') ]; } }
  53. 53. Meta-Steps With Multi-line Arguments use BehatBehatContextStep; use BehatGherkinNodePyStringNode; class FeatureContext { /** @Given /^I should see the file review screen$/ */ public function iShouldSeeTheFileReviewScreen() { $content = 'Please review your file .' . PHP_EOL . 'Press Submit to continue'; $pyString = new PyStringNode($content); return new StepGiven('I should see', $pyString); } }
  54. 54. Direct Method Call - Same Context /** @Given /^I should see an error about the file type$/ */ public function iShouldSeeAnErrorAboutTheFileType() { $message = 'This file type is invalid'; $this->iShouldSeeAnMessageThatSays('error', $message); } /** @Given /^I should see an? (.*) message that says "([^"])"$/ */ public function iShouldSeeAnMessageThatSays($messageType, $message = null) { $class = '.alert -' . $messageType; $this->assertElementExists($class, 'css'); if ($message !== null) { $this->assertElementContainsText($class, 'css', $message); } }
  55. 55. Direct Method Call to Another Context (Behat 2.x) /** * @Given /^I should see an error about the file type$/ */ public function iShouldSeeAnErrorAboutTheFileType() { $message = "This file type is invalid"; $this->getMainContext() ->getSubContext('MessageContext') ->iShouldSeeAnMessageThatSays('error', $message); }
  56. 56. Direct Method Call to Another Context (Behat 2.x) /** * @return MessageContext */ public function getMessageContext() { return $this->getMainContext()->getSubContext('messageContext'); } /** * @Given /^I should see an error about the file type$/ */ public function iShouldSeeAnErrorAboutTheFileType() { $message = "This file type is invalid"; $this->getMessageContext()->iShouldSeeAnMessageThatSays('error', $message); }
  57. 57. Suite Contexts (Behat 3.x) default: suites: default: paths: [ %paths.base%/features/core ] contexts: [FeatureContext, MessageContext]
  58. 58. Store Other Contexts (Behat 3.x) use BehatBehatContextContext; use BehatBehatHookScopeBeforeScenarioScope; class FeatureContext implements Context { /** @var MessageContext */ private $messageContext; /** @BeforeScenario */ public function gatherContexts(BeforeScenarioScope $scope) { $environment = $scope->getEnvironment(); $this->messageContext = $environment->getContext('MessageContext'); } } // http://docs.behat.org/en/v3.0/cookbooks/context_communication.html
  59. 59. Direct Method Call to Another Context (Behat 3.x) use BehatBehatContextContext; use BehatBehatHookScopeBeforeScenarioScope; class FeatureContext implements Context { /** @var BehatMinkExtensionContextMinkContext */ private $minkContext; /** @BeforeScenario */ public function gatherContexts(BeforeScenarioScope $scope) { $environment = $scope->getEnvironment(); $this->minkContext = $environment->getContext('BehatMinkExtensionContextMinkContext'); } } // http://docs.behat.org/en/v3.0/cookbooks/context_communication.html /** * @Given /^I should see an error about the file type$/ */ public function iShouldSeeAnErrorAboutTheFileType() { $message = "This file type is invalid"; $this->messageContext->iShouldSeeAnMessageThatSays('error', $message); }
  60. 60. Direct Call ● Like any other method call ● Hooks do not fire (typically faster) ● Moving Step definitions might require refactor Meta-Steps Return a Step or array of Steps Hooks will fire (could be slow) Moving Step definitions does not break Removed in 3.0
  61. 61. Page Objects
  62. 62. Page Objects Extension #terminal php composer require "sensiolabs/behat-page-object-extension" #config.yml default: extensions: SensioLabsBehatPageObjectExtensionExtension: ~
  63. 63. Seminar Page Object Scenario: Visit Seminar Page before Broadcast Time Given there is a seminar scheduled for the future When I visit that seminar's page Then I should see the seminar's name And I should see the seminar's author’s name And I should see "This seminar begins at" And I should see the seminar’s start time in EST And I should see a countdown timer Scenario: Visit Seminar Page before Broadcast Time Given there is a seminar scheduled When I visit that seminar's page during the broadcast time Then I should see the seminar's name And I should see the seminar video And I should not see a countdown timer
  64. 64. Seminar Page Object namespace AcmeAppBundlePageObjects; use SensioLabsBehatPageObjectExtensionPageObjectPage; class Seminar extends Page { protected $path = '/seminar/{id}'; }
  65. 65. Seminar Page Object namespace AcmeAppBundlePageObjects; use SensioLabsBehatPageObjectExtensionPageObjectPage; class Seminar extends Page { protected $path = '/seminar/{id}'; protected $elements = [ 'Author Info' => ['css' => "#author"], 'Video' => ['xpath' => "//div[contains(@class, 'video')]"], 'Countdown Timer' => ['css' => ".timer"], ]; }
  66. 66. Element namespace AcmeAppBundlePageObjectsElements; use SensioLabsBehatPageObjectExtensionPageObjectElement; class AuthorInformation extends Element { protected $selector = ['css' => "#author"]; public function getAuthorName() { return $this->find('css', '.name'); } public function getAuthorPhoto() { return $this->find('xpath', '//img'); }
  67. 67. Interesting Interactions
  68. 68. CSV Report @javascript Scenario: View Summary Report Given a user in a group has registered for a seminar with a company And I am logged in as an admin And I am viewing the reports area When I download the "Summary" report Then I should see the following columns: | column | | Group | | Company | | Total | And I should see that user in the report
  69. 69. File Download Test /** * @When /^I download the "([^"]*)" report$/ */ public function iDownloadTheReport($reportName) { $xpath = "//a[normalize-space()='{$reportName}']"; $link = $this->findRequiredElement($xpath); $this->getSession()->visit('view-source:' . $link->getAttribute('href')); $content = $this->getSession()->getPage()->getContent(); $lines = explode(PHP_EOL, $content); $this->csvRows = []; foreach ($lines as $line) { if (strlen(trim($line))) { $this->csvRows[] = str_getcsv($line); } } }
  70. 70. Zip File Download @javascript Scenario: View Large Report Given I am viewing the reports area When I click "Export" for the "Extremely Large Report" report Then a zip file should be downloaded When I unzip the file and I open the extracted csv file Then I should see the following columns: | column | | Group | | Company | | Total |
  71. 71. File Download Test /** * @When /^I click "Export" for the "([^"]*)" report$/ */ public function iExportTheReport($reportName) { $this->file = $this->getArtifactsDir() . 'download-' . time() . '.zip'; file_put_contents($this->file, $this->getSession()->getDriver() > getContent()); } /** * @Then /^a zip file should be downloaded$/ */ public function aZipFileShouldBeDownloaded() { $header = $this->getSession()->getDriver()->getResponseHeaders(); $this->assertContains($header['Content-Type'][0], 'application/forced-download'); $this->assertContains($header['Content-Disposition'][0], "zip"); }
  72. 72. File Download Test /** * @When /^I unzip the file and I open the extracted csv file$/ */ public function iUnzipTheFileAndOpenCsvFile() { $zip = new ZipArchive; $unzipped = $zip->open($this->file); $csv = $zip->getNameIndex(1); $zip->extractTo($this->getArtifactsDir()); $zip->close(); $fileRef = fopen($this->getArtifactsDir() . $csv, 'r'); $this->csvContents = []; while (($data = fgetcsv($fileRef)) !== false) { $this->csvContents[] = $data; } fclose($fileRef); }
  73. 73. Confirm Text In PDF /** * @Given /^I should see a PDF with the order total$/ */ public function iShouldSeeAPdfWithTheOrderTotal() { $total = 'Order Total: ' . $this->orderTotal; $this->getMainContext()->assertPageContainsText($total); } @javascript Scenario: View PDF Receipt Given I am viewing my order history When I click "View Receipt" for an order Then I should see a PDF with the order total
  74. 74. Behat Without The Browser
  75. 75. Testing a Command Line Process with Behat Scenario: Test User Import With a Large Data Set Given the system already has 100000 users And there is a company with 10000 of the users assigned to it And an admin has uploaded a spreadsheet for the company with 10000 rows When the system has begun to process the spreadsheet And I have waited 1 minute Then the batch process status should be set to "Running" or "Completed" And I should see at least 100 new users in the company
  76. 76. The System Already Has 100000 Users /** @Given /^the system already has (d+) users$/ */ public function theSystemAlreadyHasUsers($numUsers) { $faker = $this->getFaker(); $userValues = []; for ($i = 0; $i < $numUsers; $i++) { $firstname = addslashes($faker->firstName); $lastname = addslashes($faker->lastName); $username = $faker->username . $i; //unique $userValues[] = "('{$firstname}', '{$lastname}', '{$username}')"; } $userQuery = "INSERT INTO `user`(firstname, lastname, username) VALUES" . implode(', ', $userValues); $this->getEntityManager()->getConnection()->exec($userQuery); }
  77. 77. There is a company with 10000 users assigned to it/** @Given /^there is a company with (d+) of the users assigned to it$/ */ public function thereIsACompanyWithOfTheUsersAssignedToIt($num) { $company = $this->generateCompany(); $conn = $this->getEntityManager()->getConnection(); $userCompanySQL = "INSERT INTO `user_company`(user_id, company_id) SELECT `user`.id, {$company->getId()} FROM `user` LIMIT {$num}"; $conn->exec($userCompanySQL); $this->getEntityManager()->refresh($company); $companyUsersCount = $company->getUserCompanies()->count(); $this->assertGreaterThanOrEqual($num, $companyUsersCount); $this->company = $company; $this->companyNumUsers = $companyUsersCount; }
  78. 78. An Admin Has Uploaded a Spreadsheet /** @Given /^an admin has uploaded a spreadsheet for the company with (d*) rows$/ */ public function adminHasUploadedSpreadsheetForTheCompanyWithRows($numRows) { $faker = $this->getFaker(); $this->filePath = $this->getUploadsDirectory() . 'import -' . $numRows . '.csv'; $fh = fopen($this->filePath, "w"); $rows = 'firstname, lastname, username' . PHP_EOL; for ($i = 0; $i < $numRows; $i++) { $firstname = addslashes($faker->firstName); $lastname = addslashes($faker->lastName); $username = $faker->username . $i; //add $i to force unique $rows .= "{$firstname}, {$lastname}, {$username}" . PHP_EOL; } fwrite($fh, $rows); fclose($fh); $repository = $this->getRepository('BatchProcess'); $this->batchProcess = $repository->create()->setFilename($this->filePath); $repository->save($this->batchProcess);
  79. 79. The System Has Begun To Process The Spreadsheet/** * @When /^the system has begun to process the spreadsheet$/i */ public function theSystemHasBegunToProcessTheSpreadsheet() { $command = 'php app' . DIRECTORY_SEPARATOR; $command .= 'console batch:process --batch_id='; $command .= $this->batchProcess->getId(); if (substr(php_uname(), 0, 7) == "Windows") { return pclose(popen("start /B " . $command, "r")); } return exec($command . " > /dev/null &"); }
  80. 80. I Have Waited 1 Minute /** * @When /^I have waited (d+) minutes?$/ */ public function iHaveWaitedSomeMinutes($num) { $seconds = 60; $outputEvery = 30; $cycles = ($num * $seconds) / $outputEvery; for ($i = 0; $i < $cycles; $i++) { sleep($outputEvery); echo '.'; } echo PHP_EOL; }
  81. 81. The Batch Process Status Should Be /** * @Given /^the batch process status should be set to "(.*)" or "(.*)"$/ */ public function theBatchProcessStatusShouldBeSetTo($statusA, $statusB) { $this->getEntityManager()->refresh($this->batchProcess); $statusName = $this->batchProcess->getStatus()->getName(); if ($statusName !== $statusA && $statusName !== $statusB) { throw new Exception("Status is currently: {$statusName}"); } }
  82. 82. I should see at least 100 new users /** * @Then /^I should see at least (d+) new users in the company$/ */ public function iShouldSeeAtLeastNewUsersInTheCompany($num) { $company = $this->company; $this->getEntityManager()->refresh($company); $companyNumUsersNow = $company->getUserCompanies()->count(); $originalNumUsers = $this->companyNumUsers; $difference = ($companyNumUsersNow - $originalNumUsers); $this->assertGreaterThanOrEqual($num, $difference); }
  83. 83. Thank You! @jessicamauerhan @SunshinePHP | 2-5-16 | https://joind.in/talk/9f341
  84. 84. Resources & Tools Drivers: http://mink.behat.org/en/latest/guides/drivers.html Angular AJAX check: https://gist.github.com/jmauerhan/f839926ea527ff5e74e7 Doctrine Data Fixtures: https://github.com/doctrine/data-fixtures Faker: https://github.com/fzaninotto/Faker Symfony Finder: http://symfony.com/doc/current/components/finder.html Page Object Extension: https://github.com/sensiolabs/BehatPageObjectExtension PHP Zip Archive: http://php.net/manual/en/class.ziparchive.php

×