Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Using Behat to test ExpressionEngine add-ons

Using Behat to test ExpressionEngine add-ons

EE CONF 2015 presentation

BoldMinded

October 14, 2015
Tweet

More Decks by BoldMinded

Other Decks in Programming

Transcript

  1. 1

  2. Who am I? Brian Litzinger — Principle Software Engineer at

    The Nerdery — Created Publisher and other BoldMinded add-ons 4
  3. 6

  4. Why now? — EE2 => EE3 — 10,843 lines of

    code in Publisher — Write tests, then refactor 8
  5. Unit tests vs Functional tests — Unit tests are small

    and usually cover a specific function, or unit of work. All dependencies are mocked. — Functional tests cover a large spectrum of functionality and may interact with a database or other dependencies. — User needs to get from point A to point B. 9
  6. So what is Behat? — It is a behavior driven

    development framework for PHP. — A combination of acceptance and unit testing. — You describe features in human readable sentences. 10
  7. Feature: Some terse yet descriptive text of what is desired

    Textual description of the business value of this feature Business rules that govern the scope of the feature Any additional information that will make the feature easier to understand Scenario: Some determinable business situation Given some precondition And some other precondition When some action by the actor And some other action And yet another action Then some testable outcome is achieved And something else we can check happens too Scenario: A different situation ... 11
  8. Hurdles — Need access to ee() — Need access to

    database — Need to generate fixtures 12
  9. 13

  10. $project_url = 'http://ee300.dev/'; $project_base = realpath('/Users/blitzing/Dropbox/ee/sandboxes/dev300').'/'; define('SYSPATH', $project_base.'system/'); define('BASEPATH', SYSPATH.'ee/legacy/');

    define('APPPATH', BASEPATH); ... ee()->di = $di; require_once(BASEPATH.'database/DB.php'); $files = ['ee/legacy/core/Config.php', 'ee/legacy/database/DB_forge.php', 'ee/legacy/database/drivers/mysqli/mysqli_forge.php', 'ee/legacy/core/URI.php', 'ee/legacy/core/Input.php', 'ee/legacy/core/Loader.php', 'ee/legacy/libraries/Extensions.php']; foreach ($files as $file) { require_once(SYSPATH.$file); } define('UTF8_ENABLED', true); define('URL_THIRD_THEMES', $project_url.'themes/user/third_party/'); define('PATH_THIRD', SYSPATH.'user/addons/'); ee()->db = DB([ 'hostname' => 'localhost', 'username' => 'root', 'password' => 'root', 'database' => 'dev300', 'dbdriver' => 'mysqli', 'dbprefix' => 'exp_', 'pconnect' => FALSE, 'port' => '3306' ]); ee()->config = new EE_Config(); ee()->config->set_item('site_id', 1); ee()->uri = new EE_URI(); ee()->input = new EE_Input(); ee()->dbforge = new CI_DB_mysqli_forge(); ee()->extensions = new EE_Extensions(); ee()->load = new EE_Loader(); ee()->load->helper('string'); $app->addProvider(SYSPATH.'user/addons/publisher'); 14
  11. Features & Contexts — Features describe what you are testing.

    — Contexts handle the specifics of the test. 15
  12. user/ addons/ publisher/ behat.yml bin/ behat selenium Model/ Service/ Test/

    Features/ 00_settings.feature 01_languages.feature 02_fields.features Contexts/ ee.php FieldContext.php LanguageContext.php SettingContext.php SuiteContext.php 16
  13. Fixtures Feature: Fields Background: Given I have fields: | field_id

    | site_id | group_id | field_name | field_label | field_instructions | field_type | | 1 | 1 | 1 | page_body | Body | | text | | 2 | 1 | 1 | page_header | Header | | text | | 3 | 1 | 1 | page_checkboxes | Checkboxes | | checkboxes | | 4 | 1 | 3 | page_grid | Grid Field | | grid | Given I have field groups: | group_id | site_id | group_name | | 1 | 1 | Pages | 17
  14. use Behat\Behat\Context\Context; use Behat\Behat\Context\SnippetAcceptingContext; use Behat\Gherkin\Node\TableNode; class SuiteContext implements Context,

    SnippetAcceptingContext { /** * @BeforeSuite */ public static function beforeSuite() { exec("/Applications/MAMP/Library/bin/mysql --host=localhost -uroot -proot -e 'DROP DATABASE IF EXISTS ee300_clean'"); exec("/Applications/MAMP/Library/bin/mysql --host=localhost -uroot -proot -e 'CREATE DATABASE ee300_clean'"); exec("/Applications/MAMP/Library/bin/mysql --host=localhost -uroot -proot ee300_clean < ".PUBLISHER_PATH."/Test/ee300_clean.sql"); } /** * @AfterSuite */ public static function afterSuite() { } } 18
  15. class FieldContext implements Context { private $type; public function __construct()

    { // Bootstrap EE() require_once 'ee.php'; } /** * @Given /^I have fields:$/ */ public function iHaveFields(TableNode $table) { foreach ($table->getColumnsHash() as $field) { ee()->db->insert('channel_fields', $field); } } /** * @Given /^I have field groups:$/ */ public function iHaveFieldGroups(TableNode $table) { foreach ($table->getColumnsHash() as $group) { ee()->db->insert('field_groups', $group); } } 19
  16. Feature: Fields Background: Given I have fields: | field_id |

    site_id | group_id | ... Given I have field groups: | group_id | site_id | group_name | ... Scenario: I need fields by type When default language is "en" Then I want "text" fields Then I should get "text" fields as array And I should have 2 "text" fields Then I want to see if "field_id_3" is ignored And I want to see if "field_id_2" is not ignored Then I want know what type of field "field_id_4" is and I should get "grid" And I want to know what type of field "field_id_1" is and I should not get "text" Then I expect the field name for "field_id_2" to be "page_header" Then I want a list of fields as select menu options Then I expect to get the field label "Grid Field" for custom field by column name "field_id_4" And I expect to get the field label "Checkboxes" for custom field by short name "page_checkboxes" And I expect to get the short name "page_header" for custom field by column name "field_id_2" Then I want to see if "title" is a custom field Then I want to see if "field_id_4" is a custom field Then I want to see if "page_header" is a custom field Then I want to see if "foobar" is not a custom field 20
  17. /** * @When /^default language is "([^"]*)"$/ */ public function

    defaultLanguageIs($shortName) { /** @var Language $languageModel */ $languageModel = ee('Model')->make(Language::NAME); $defaultLangauge = $languageModel->findLanguageByCode($shortName); /** @var Request $requestService */ $requestService = ee(Request::NAME); $requestService ->setDefaultLanguage($defaultLangauge) ->setSiteId(1); } /** * @Then /^I want "([^"]*)" fields$/ */ public function iWantFields($type) { $this->type = $type; } /** * @Then /^I should get "([^"]*)" fields as array$/ */ public function iShouldGetFieldsAsArray($type) { /** @var Field $fieldService */ $fieldService = ee(Field::NAME); $fields = $fieldService->getFieldsByType($this->type); PHPUnit_Framework_Assert::assertInternalType('array', $fields); } 21
  18. /** * @Then /^I want a list of fields as

    select menu options$/ */ public function iWantAListOfFieldsAsSelectMenuOptions() { /** @var Field $fieldService */ $fieldService = ee(Field::NAME); $fields = $fieldService->getFieldsAsOptions(); $expected = [ 1 => "Pages &raquo; Body", 2 => "Pages &raquo; Header", 3 => "Pages &raquo; Checkboxes", 4 => "Pages &raquo; Grid Field", ]; PHPUnit_Framework_Assert::assertEquals($expected, $fields); } /** * @Then /^I want to see if "([^"]*)" is a custom field$/ */ public function iWantToSeeIfIsACustomField($fieldName) { /** @var Field $fieldService */ $fieldService = ee(Field::NAME); $isCustomField = $fieldService->isCustomField($fieldName); PHPUnit_Framework_Assert::assertTrue($isCustomField); } 22
  19. public function getCustomField($name, $return = 'field_label', $flipped = false) {

    if (! isset(ee()->session->cache['publisher']['cfields'][$this->siteId])) { $query = ee()->db->get('channel_fields'); foreach ($query->result_array() as $row) { if (! isset(ee()->session->cache['publisher']['cfields'][$row['site_id']])) { ee()->session->cache['publisher']['cfields'][$row['site_id']] = array(); } ee()->session->cache['publisher']['cfields'][$row['site_id']]['field_id_'.$row['field_id']] = $row; } } if (isset(ee()->session->cache['publisher']['cfields'][$this->siteId][$name][$return])) { return ee()->session->cache['publisher']['cfields'][$this->siteId][$name][$return]; } else{ return null; } } 24
  20. public function getCustomField($name, $return = 'field_label', $flipped = false) {

    $cache = array(); if (! isset($cache['publisher']['cfields'][$this->siteId])) { $query = ee()->db->get('channel_fields'); foreach ($query->result_array() as $row) { if (! isset($cache['publisher']['cfields'][$row['site_id']])) { $cache['publisher']['cfields'][$row['site_id']] = array(); } $cache['publisher']['cfields'][$row['site_id']]['field_id_'.$row['field_id']] = $row; } } if (isset($cache['publisher']['cfields'][$this->siteId][$name][$return])) { return $cache['publisher']['cfields'][$this->siteId][$name][$return]; } else { return null; } } 25
  21. private function getCustomField($name, $return = 'field_label', $flipped = false) {

    $mapping = $flipped ? 'nameToField' : 'columnToField'; $cacheKeyPrefix = 'customFields/'. $this->siteId; if ($field = $this->requestCache->get($cacheKeyPrefix .'/'. $mapping .'/'. $name)) { return $field[$return]; } $customFields = array(); /** @var \CI_DB_result $query */ $query = ee()->db->get('channel_fields'); foreach ($query->result_array() as $row) { $customFields[$cacheKeyPrefix .'/columnToField/field_id_'. $row['field_id']] = $row; $customFields[$cacheKeyPrefix .'/nameToField/'. $row['field_name']] = $row; $this->requestCache->set($cacheKeyPrefix .'/columnToField/field_id_'. $row['field_id'], $row); $this->requestCache->set($cacheKeyPrefix .'/nameToField/'. $row['field_name'], $row); } if (isset($customFields[$cacheKeyPrefix .'/'. $mapping .'/'. $name][$return])) { return $customFields[$cacheKeyPrefix .'/'. $mapping .'/'. $name][$return]; } return null; } 26
  22. /** * @return null|Language */ public function findDefaultLanguage() { /**

    @var RequestCache $requestCache */ $requestCache = ee(RequestCache::NAME); if ($defaultLanguage = $requestCache->get('defaultLanguage')) { return $defaultLanguage; } /** @var Builder $queryBuilder */ $queryBuilder = $this->getQueryBuilder(self::NAME); /** @var Language $language */ $language = $queryBuilder ->filter('is_default', 'y') ->first(); $requestCache->set('defaultLanguage', $language); return $language; } 27
  23. /** * @return null|Language */ public function findDefaultLanguage() { $languages

    = $this->findAllEnabledLanguages(); /** @var Language $language */ foreach($languages as $language) { if ($language->is_default === 'y') { return $language; } } return null; } 28
  24. Feature: Browser Test @javascript Scenario: I need to login to

    EE's control panel Given I am on "/admin.php" When I fill in "Username" with "admin" When I fill in "Password" with "password" And I press "submit" Then I should see "Create" When I follow "Create" When I follow "Pages" Given I am on "/admin.php?/cp/publish/create/1" Then I wait for ".grid-input-form a.btn.action" When I fill in "title" with "Automated Testing FTW" When I follow "add new row" When I fill in "field_id_1[rows][new_row_1][col_id_1]" with "This is content in a Grid row" Then I press "Publish" 30
  25. use Behat\MinkExtension\Context\MinkContext; class BrowserContext extends MinkContext { /** * @Then

    /^I wait for "([^"]*)"$/ */ public function iWaitFor($selector) { $this->spin(function($context) use ($selector) { /** @var $context BrowserContext */ return $context->getSession()->getPage()->find('css', $selector)->isVisible(); }); } /** * See http://docs.behat.org/en/v2.5/cookbook/using_spin_functions.html * * @param $lambda * @return bool */ public function spin($lambda) { while (true) { try { if ($lambda($this)) { return true; } } catch (Exception $e) { // do nothing } sleep(1); } return false; } } 31
  26. How to win using Behat — Can make upgrading add-ons

    to EE3 easier — Adding new tests for edge cases reported by customers — Constantly regression test those edge cases 34