Mocking Drupal: Unit Testing In Drupal 8 - Softpixel

1y ago
11 Views
2 Downloads
630.20 KB
65 Pages
Last View : 1m ago
Last Download : 3m ago
Upload by : Wade Mabry
Transcription

Mocking Drupal:Unit Testing inDrupal 8Matthew Radcliffemradcliffe@mattkineme

Spoilers Quality Assurance PHPUnit Mocking Drupal things

Quality Assurance Prevent defects from making it to the customer: Adopt standards and specifications Review code Manage releases Test code

Some Types of Testing User Acceptance Test: Test according to specification orrequirement. Functional Test: Test one function (or feature) expected output. Unit Test: Test the smallest “unit” of testable code (in isolation). Integration Test: Test the interaction of “units” in 1 or more systems. Behavioral Test: Automated UAT or black box testing. Stress Test: Test the product under heavy load/stress.

Value of Testing Fundamental problems of software programming “I didn’t think anyone would do THAT!” “Why did it break?” Increase reliability and quality of software.Discover regressions in software.Improve confidence in our code. “I think this will work.”

Common ExcusesDroofus saysDropant saysWriting tests takes too long.Start small. 100% coverage isn’t going tocome in a day, and every bit helps.I don’t have source control / versioncontrol.Do not pass go. Do not write any morecode. Go directly to a Git repository.I don’t have any testing infrastructureRun locally, enforce social contract. Orsetup TravisCI/Jenkins.I don’t know what I’m going to writeuntil I write it.Not everyone needs to adopt Test-DrivenDevelopment, but it is “best practice”.My code is heavily integrated with state I test only what I need to and mock the(database or web services).rest.

Unit Tests A unit is the smallest testable piece of code, which is often afunction, class or method. Plug a set of inputs into that code, and confirm the expectedoutput (like behavioral and integration tests). Units should act in memory and not depend on other systems. Should be fast. Do not run Drupal installation. Run all unit tests after code change.

Drupal & Unit Tests Modules have complex dependencies and setup necessary todo what they do. Simpletest module is still a test runner for both SimpleTestand PHPUnit tests, but You may use phpunit directly instead, and core/scripts/run-tests.shis a hot mess.

PHPUnit: Getting Started phpunit.xml configuration bootstrap Test class in tests/src/Unit instead of src/Tests. Annotations Assertions Data Providers Test Doubles

?xml version "1.0" encoding "UTF-8"? phpunit php ini name "error reporting" value "32767"/ ini name "memory limit" value "-1"/ /php testsuites testsuite name "My Module Unit Test Suite" directory tests /directory exclude ./vendor /exclude exclude ./drush/tests /exclude /testsuite /testsuites !-- Filter for coverage reports. -- filter whitelist directory src /directory exclude directory src/Tests /directory /exclude /whitelist /filter /phpunit

Use Core’s Bootstrap? Add more namespaces to autoloader than are necessary i.e.increased memory. Necessary because contrib namespaces are not loaded byComposer autoloader. Can re-use core abstract test class with mockeddependencies easier.Relative path may conflict with where phpunit can be runfrom.

Abstract Test Classes Drupal\Tests\UnitTestCase Basic unit test class with some useful test doubles. sts\KernelTestBase Adds database and filesystem state, but without aninstallation at the cost of performance. Not as slow asSimpleTest functional tests.

Class Annotations Class Annotations @group Required for drupal.org testing infrastructure.

Assertions assertEquals assertArrayHasKey assertEmpty assertArrayHasSubset assertSame assertCount assertInstanceOf assertFileExists assertXmlStringEqualsXmlFile assertNullPHPUnit Manual. sertions.html

Data Providers A data provider is a method that returns an array ofparameters to pass into a test method. A test method may test several inputs.Important to note that data provider methods are run beforeany setup so this cannot depend on any test doubles.

function sequenceProvider() {return [[0, 0],[5, 8],[10, 55],[100, 354224848179261915075],[-5, 5],[-6, 8]];}/*** Test class with data provider.** @dataProvider sequenceProvider*/function testFibonacciWithProvider( number, output) { this- assertEquals( output, fibonacci( number));}

setUp The setUp method is executed for every test method in aclass. Configure fixtures or setting up known state such asdatabase or test files. Configure test doubles or mocks, which are dependencies ofa unit that you do not need to test in that test class. Advice: Add you own abstract test classes to create fixturesor test double commonly used across your test classes.

Test Doubles Test doubles (or mock objects) allow to focus a unit test onthe code that needs to be tested without bootstrappingdependencies. Example: I don’t want to load Drupal 8’s Entity system whenI test my unrelated code. Instead PHPUnit allows to create amock via Reflection so that I get an object that looks like anEntity. Reflection or introspection allows a program to knowabout and modify itself at runtime.

Test Doubles getMockBuilder() disableOriginalConstructor()method() with() callback() withConsecutive() will() onConsecutive() returnValueMap()

Prophecy Drupal’s dev requirements include the prophecy test doublelibrary and integrated into the UnitTestBase class. Prophecy provides more readable test code But it is “opinionated” in that it will never be able to mockthings such as magic methods or method chaining.

Prophecy A prophecy returned by prophesize() is an object thatyou can call the same methods as the Class or Interface youare mocking. Once the prophecy object has its methods and return valuesmocked, the reveal() method will return the mockedobject. This reduces the complexity of mocking somewhat.

Test Double Risks Assume too much about the framework. Tests can pass when the framework changes. Could assume one thing leading to breaking actual codeusage. Ouch. :(Having to mock a lot is a sign of architecture issues.

Mocking Drupal“You’re crazy.”–webçick (June, 2015)

Mocking Drupal Entity Plugin Database Form Other services

Mocking Entities Mock low-level classes that entities use: Config Entity Config: The immutable config is useful to mock forconfig forms.

Xero ModuleDrupal\Tests\Unit\xero\Form\SettingsFormTest

protected function setUp() {parent::setUp();// Mock config object. this- config this- g')- disableOriginalConstructor()- getMock();// Mock ConfigFactory service. configFactory this- - disableOriginalConstructor()- getMock(); configFactory- expects( this- any())- method('getEditable')- with('xero.settings')- will( this- returnValue( this- config));

public function testNoPrivateKey() { key this- createToken(); secret this- createToken();// Mock the getter. this- config- expects( this- any())- method('get')- withConsecutive([‘oauth.consumer key'],[‘oauth.consumer secret'],['oauth.key path'])- will( this- onConsecutiveCalls( key, secret, ''));

public function testNoPrivateKey() { key this- createToken(); secret this- createToken(); values ['oauth.consumer key' key,'oauth.consumer secret' secret,'oauth.key path' '',]; configProphecy this- prophesize('\Drupal\Core\Config\ImmutableConfig'); configProphecy- get(Argument::type('string'))- will(function( args) use( values) {return values[ args[0]];}); this- configFactory- expects( this- any())- method('getEditable')- with('xero.settings')- will( this- returnValue( configProphecy- reveal()));

Xero ModuleDrupal\xero\Form\SettingsForm

public function buildForm(array form, FormStateInterface form state) { config self::config('xero.settings');// . form['oauth']['consumer key'] array('#type' 'textfield','#title' this- t('Xero Consumer Key'),'#default value' config- get('oauth.consumer key'),); form['oauth']['consumer secret'] array('#type' 'textfield','#title' this- t('Xero Consumer Secret'),'#default value' config- get('oauth.consumer secret'),); form['oauth']['key path'] array('#type' 'textfield','#title' this- t('Xero Key Path'),'#default value' config- get('oauth.key path'),'#element validate' array(array( this, 'validateFileExists')),);return parent::buildForm( form, form state);}

Mocking Entities Mock low-level classes that entities use: Content Entity Entity Manager: getDefinition, getStorage,getFieldDefinitions Entity Storage: loadMultiple, load configuration entities would mock ConfigStorageNode: getTitle, get

class MockingDrupalFormTest extends FormTestBase {protected function setUp() {parent::setUp(); this- node title this- getRandomGenerator()- word(10); this- node this- getMockBuilder('Drupal\node\Entity\Node')- disableOriginalConstructor()- getMock(); this- node- expects( this- any())- method('getTitle')- will( this- returnValue( this- node title)); this- nodeStorage this- getMockBuilder('Drupal\node\NodeStorage')- disableOriginalConstructor()- getMock(); this- nodeStorage- expects( this- any())- method('load')- will( this- returnValueMap([[1, this- node],[500, NULL],])); entityManager this- terface')- disableOriginalConstructor()- getMock(); entityManager- expects( this- any())- method('getStorage')- with('node')- willReturn( this- nodeStorage);

Mocking Plugins? No mock necessary: instantiate the plugin class depends onplugin type: Create via initialize ( construct) with definition array andany additional settings for the plugin type.Mock the plugin manager for the plugin type, if necessary Typed Data Manager requires getDefinitions to beconsecutively mocked if dealing with composite datatypes.

Key ModuleDrupal\Tests\Unit\key\KeyRepositoryTest

public function defaultKeyContentProvider() { defaults ['key value' this- createToken()]; definition ['id' 'config','class' 'title' 'Configuration',]; KeyProvider new ConfigKeyProvider( defaults, 'config', definition);return [[ defaults, KeyProvider]];}

Xero ase

public function setUp() {// Typed Data Manager setup. this- typedDataManager this- nager')- disableOriginalConstructor()- getMock(); this- typedDataManager- expects( this- any())- method('getDefinition')- with(static::XERO TYPE, TRUE)- will( this- returnValue(['id' ‘xero employee', 'definition class' nition’]));// Snip. . . . . . . . . . . .// Mock the container. container new ContainerBuilder(); container- set('typed data manager', this- typedDataManager);\Drupal::setContainer( container);// Create data definition definition class static::XERO DEFINITION CLASS; this- dataDefinition definition class::create(static::XERO TYPE);}

Typed Widgets ModuleDrupal\Tests\Unit\typed widget\TypedElementTestBase

protected function getEntityTypeManagerMock() { prophecy this- nterface');return prophecy- reveal();}protected function getTypedDataMock(DataDefinitionInterface definition, array constraints []){ typedDataProphecy this- rInterface'); typedDataProphecy- createDataDefinition( definition- getDataType())- willReturn( definition); typedDataProphecy- getDefaultConstraints( definition)- willReturn( constraints); typedDataProphecy- getDefinition( definition- getDataType())- willReturn( definition); typedDataProphecy- getDefinitions()- willReturn([ definition- getDataType() definition]);if ( definition instanceof ComplexDataDefinitionInterface) {foreach ( definition- getPropertyDefinitions() as name child definition) { typedDataProphecy- createDataDefinition( child definition- getDataType())- willReturn( child definition); typedDataProphecy- getDefaultConstraints( child definition)- willReturn([]); typedDataProphecy- getDefinition( child definition- getDataType())- willReturn( child definition);}}elseif ( definition instanceof ListDataDefinitionInterface) { typedDataProphecy- createDataDefinition('string')- willReturn( definition- getItemDefinition()); typedDataProphecy- getDefaultConstraints( definition- getItemDefinition())- willReturn([]); typedDataProphecy- getDefinition('string')- willReturn( definition- getItemDefinition());}return typedDataProphecy- reveal();}

Mocking Database Mock Drupal\Tests\Core\Database\Stub\StubPDO Then use Drupal\Core\Database\Connection Use Prophecy to mock the Connection class and pass it to theclasses you’re testing that need it. Or use KernelTestBase and add the database service to teston an actual -doubles.html#test-doubles.prophecy

Drupal esqlConnectionTest

protected function setUp() {parent::setUp(); this- mockPdo this- ;}/*** @covers ::escapeTable* @dataProvider providerEscapeTables*/public function testEscapeTable( expected, name) { pgsql connection new Connection( this- mockPdo, []); this- assertEquals( expected, pgsql connection- escapeTable( name));}/*** @covers ::escapeAlias* @dataProvider providerEscapeAlias*/public function testEscapeAlias( expected, name) { pgsql connection new Connection( this- mockPdo, []); this- assertEquals( expected, pgsql connection- escapeAlias( name));}

Drupal CoreDrupal\Tests\Core\Database\ConditionTest

public function testSimpleCondition() { connection this- prophesize(Connection::class); connection- escapeField('name')- will(function ( args) {return preg replace('/[ A-Za-z0-9 .] /', '', args[0]);}); connection- mapConditionOperator(' ')- willReturn(['operator' ' ']); connection connection- reveal(); query placeholder this- prophesize(PlaceholderInterface::class); counter 0; query placeholder- nextPlaceholder()- will(function() use (& counter) {return counter ;}); query placeholder- uniqueIdentifier()- willReturn(4); query placeholder query placeholder- reveal(); condition new Condition('AND'); condition- condition('name', ['value']); condition- compile( connection, query placeholder); this- assertEquals(' (name :db condition placeholder 0) ', condition- toString()); this- assertEquals([':db condition placeholder 0' 'value'], condition- arguments());

Mocking Forms FormTestBase is pretty useless, but it is there. How useful is testing a form in phpunit? FormBuilder requires complex Request mocking, and it isnot possible to simply pass in FormState with values set. This means that a form needs to be very careful to followAPI in all that it does and the expectation is that the formknows everything about Drupal form builder input.

form['node id'] ['#type' 'number','#title' this- t('Node id'),'#description' this- t('Provide a node id.'),'#min' 1,'#required' TRUE,]; form['actions'] ['#type' 'actions']; form['actions']['submit'] ['#type' 'submit','#value' this- t('Display'),];if ( form state- getValue('node id', 0)) {try { node this- entityManager- getStorage('node')- load( form state- getValue('node id',0));if (!isset( node)) {throw new \Exception;} form['node'] ['#type' 'label','#label' node- getTitle(),];}catch (\Exception e) { this- logger- error('Could not load node id: %id', ['%id' form state getValue('node id', 0)]);}

protected function setUp() {// Set the container into the Drupal object so that Drupal can call the// mocked services. container new ContainerBuilder(); container- set('entity.manager', entityManager); container- set('logger.factory', loggerFactory); container- set('string translation', this- stringTranslation);\Drupal::setContainer( container);// Instantiatie the form class. this- form MockingDrupalForm::create( container);}

public function testBuildForm() { form this- formBuilder- getForm( this- form); this- assertEquals('mockingdrupal form', form['#form id']); state new FormState(); state- setValue('node id', 1);// Fresh build of form with no form state for a value that exists. form this- formBuilder- buildForm( this- form, state); this- assertEquals( this- node title, form['node']['#label']);// Build the form with a mocked form state that has value for node id that// does not exist i.e. exception testing. state new FormState(); state- setValue('node id', 500); form this- formBuilder- buildForm( this- form, state); this- assertArrayNotHasKey('node', form);}

public function testFormValidation() { form this- formBuilder- getForm( this- form); input ['op' 'Display','form id' this- form- getFormId(),'form build id' form['#build id'],'values' ['node id' 500, 'op' 'Display'],]; state new FormState(); state- setUserInput( input)- setValues( input['values'])- setFormObject( this- form)- setSubmitted(TRUE)- setProgrammed(TRUE); this- form- validateForm( form, state); errors state- getErrors(); this- assertArrayHasKey('node id', errors); this- assertEquals('Node does not exist.',\PHPUnit Framework Assert::readAttribute( errors['node id'], 'string')); input['values']['node id'] 1; state new FormState(); state- setUserInput( input)- setValues( input['values'])- setFormObject( this- form)- setSubmitted(TRUE)- setProgrammed(TRUE); this- form- validateForm( form, state); this- assertEmpty( state- getErrors());}

Mocking Other Things Guzzle Provides MockHandler that can be added to the handlerstack before initiating a connection. Applications need to be able to pass the client objectaround.

Tests\XeroClientTest

public function testGetRequest() { mock new MockHandler(array(new Response(200, array('Content-Length' 0)))); this- options['handler'] HandlerStack::create( mock); this- options['private key'] this- pemFile; client new XeroClient( this- options);try{ client- get('Accounts');}catch (RequestException e){ this- assertNotEquals('401', e- getCode());} this- assertEquals('/api.xro/2.0/Accounts', mock- getLastRequest()- getUri()- getPath());}

Test Automation TravisCI DrupalTI Can setup build matrices with all PHP versions.Can setup Drupal on TravisCI for simpletest, behat, and phpunittests.DrupalCI Official infrastructure has supported databases and PHP versions,but harder to get dependencies via composer.

TravisCI Configuration Requires Drupal source Requires module to have a phpunit.xml configuration. Move test directory inside of Drupal’s module directory.

language: phpphp:- 5.6- 7.0env:- DRUPAL 8.1.x-devsudo: falseinstall:- TESTDIR (pwd)- export PATH " HOME/.composer/vendor/bin: PATH"- composer self-update- composer global require drush/drush: 8.1- cd .- drush dl drupal- {DRUPAL}before script:# Deploy the Drupal module into the Drupal modules directory.- rsync -rtlDPvc --exclude .git/ " {TESTDIR}" drupal- {DRUPAL}/modules/- cd drupal- {DRUPAL}script:- ./vendor/bin/phpunit -c modules/my module/phpunit.xml.dist

language: phpphp:- 5.6env:- DRUPAL 8.1.xsudo: falseinstall:- composer self-update# Download Drupal and dependencies.- cd .- git clone --depth 1 --branch {DRUPAL} http://git.drupal.org/project/drupal.gitdrupal- cd drupalbefore script:# rsync the module directory into modules/xero- rsync -rtlDPvc --exclude .git/ TESTDIR modules/# Change back to the root Drupal directory.- composer config repositories.drupal composer https://packagist.drupal-composer.org- composer require mile23/drupal-merge-pluginscript:- ./vendor/bin/phpunit -c modules/my module/phpunit.xml.dist

DrupalTI Configuration drupalti is a suite of bash scripts for setting up a Drupalenvironment on TravisCI. Always installs Drupal instance and runs through simpletesttest runner. Supports stand alone behat tests as well as phpunit,simpletest and mink tests.

env:global:- PATH " PATH: HOME/.composer/vendor/bin"- DRUPAL TI MODULE NAME "key"- DRUPAL TI SIMPLETEST GROUP "key"- DRUPAL TI DB "drupal travis "- DRUPAL TI DB URL "mysql://root@127.0.0.1/ DRUPAL TI DB"- DRUPAL TI WEBSERVER URL "http://127.0.0.1"- DRUPAL TI WEBSERVER PORT "8080"- DRUPAL TI SIMPLETEST ARGS "--verbose --color --url DRUPAL TI WEBSERVER URL: DRUPAL TI WEBSERVER PORT"- DRUPAL TI PHPUNIT CORE SRC DIRECTORY "./tests/src"- DRUPAL TI ENVIRONMENT "drupal-8"matrix:- DRUPAL TI RUNNERS "simpletest phpunit-core"before install:- composer self-update- composer global require "lionsad/drupal ti:1.*"- drupal-ti before installinstall:- drupal-ti installbefore script:- drupal-ti before script- DRUPAL TI PHPUNIT ARGS "-c DRUPAL TI DRUPAL DIR/modules/key/phpunit.xml --coverage-text"script:- drupal-ti scriptafter script:- drupal-ti after script

DrupalCI Configuration drupalci is the new testing infrastructure on drupal.org which leverages aDocker to create all the build environments we want to test. Docker project: drupalci testbot Can run in provided vagrant vm, but actual hardware recommended. Supports Drupal 7, 8. Does not support modules with composer dependencies ondrupal.org, but there is a working patch for testing locally.

DrupalCI Configuration Start docker drupalci init:*Set job properties (ENV) drupalci config:set drupalci run

DCI CoreBranch 7.xDCI PHPVersion 5.6DCI DBVersion mysql-5.5DCI TestItem directory:sites/all/modules/micropayment fieldDCI JobType simpletestlegacy7DCI AdditionalRepositories ules/contrib/micropayment field,7.x-1.x,sites/all/modules/micropayment field,1DCI TestGroups Micropayment FieldDCI Concurrency 1DCI CoreRepository git://git.drupal.org/project/drupal.gitDCI GitCheckoutDepth 1DCI RunOptions verbose;xml;keep-results

DCI DBVersion pgsql-9.4DCI PHPVersion 5.6DCI TestItem directory:modules/xeroDCI CoreBranch 8.1.xDCI JobType simpletestDCI AdditionalRepositories modules/xero,1DCI TestGroups XeroDCI ComposerInstall TrueDCI RunScript /var/www/html/core/scripts/run-tests.shDCI DBUrl estbotDCI Concurrency 1DCI CoreRepository git://git.drupal.org/project/drupal.gitDCI GitCheckoutDepth 1DCI RunOptions sqlite /var/www/html/results/simpletest.sqlite

Applying patches DCI Fetch: -transaction-isolation-level-12.patch,. DCI Patch: ,.

Summary Overview of unit testing PHPUnit and Drupal 8 PHPUnit mocks, Prophecy Complex mockingUnit Test Automation with Drupal

Some Types of Testing User Acceptance Test: Test according to specification or requirement. Functional Test: Test one function (or feature) expected output. Unit Test: Test the smallest "unit" of testable code (in isolation). Integration Test: Test the interaction of "units" in 1 or more systems. Behavioral Test: Automated UAT or black box testing.

Related Documents:

How to create custom content to store in your Drupal database using CCK Implementing seo in drupal website Drupal custom theme development (Html to drupal theme development) Drupal 8.0 content management system syllabus 1. Drupal's requirements and how it works: drupal architecture Drupal 8 Basics o How Drupal began o What is Drupal 8

guided migration from Drupal 6 or 7 to Drupal 8. Assisted upgrades to Drupal 8 can now be done, much more easily than they used to be able to earlier. Three modules were added in order to facilitate the custom migrations as well as the Drupal 6 or Drupal 7 to Drupal 8 migrations: Migrate Migrate Drupal Migrate Drupal UI Chapter 2

Chapter 1: Developing for Drupal 8. 7. Introducing Drupal (for developers) 8. Developing for Drupal 8. 8. Technologies that drive Drupal. 9 PHP 10 Databases and MySQL 10 The web server 11 HTML, CSS, and JavaScript 11. Drupal architecture. 11 Drupal core, modules, and themes 11 Hooks, plugins, and events 12 Services and the dependency injection .

Customer Identity and Access Management in a global Drupal setup Drupal Business Days, Frankfurt, 19.05.17. . Sponsor of multiple Drupal camps and European Drupal Business Days Active community work through contributions . Document and assign all tasks. Solutions. Be bold. Solutions. Get into the lead.

This is a free introductory course for people who are curious about Drupal, and want to find out more. Your Drupal guide will help you get up to speed with Drupal more quickly than if you tried on your own. First youʼll find out about your Drupal Guide delivering the Hello Drupal tour, and also learn about the other people in the room with you.

serez invité à choisir la version de Drupal à télécharger. Je recommande de sélectionner le dernier. Ainsi, lorsque Drupal est téléchargé, vous devez l'installer. drupal site:install Après quelques étapes simples, votre site Drupal sera prêt. Avec cette méthodologie, une nouvelle installation de Drupal nous prend entre 5 et 7 .

AJAX Framework & drag-and-drop systems in Drupal 6! File & Image modules in Drupal 7! Dialog system & CKEditor in Drupal 8 First core patch included in 7.14 release! Drupal core usability team member, D7 & D8! Twig initiative lead, Drupal 8 (2011 - 2013) Core Contributors who are we to judge? Nate Jen

Symfony Framework . CMS for Easy to Use CMS for Easy to Maintain II. Our Drupal 6 Platform Overview . Infrastructure Geneseo Website . . Ajax backup_migrate block_manager brilliant_gallery . Drupal 7 Yes Yes Yes No Yes Yes Yes Drupal 8 In plan No No Embedded? Under development