diff --git a/src/images/drupal-association-logo.png b/src/images/drupal-association-logo.png new file mode 100644 index 0000000..6c8b425 Binary files /dev/null and b/src/images/drupal-association-logo.png differ diff --git a/src/test-driven-drupal/images/broadbean-website.png b/src/test-driven-drupal/images/broadbean-website.png new file mode 100644 index 0000000..37d7bf1 Binary files /dev/null and b/src/test-driven-drupal/images/broadbean-website.png differ diff --git a/src/test-driven-drupal/images/override-node-options-2012-1.png b/src/test-driven-drupal/images/override-node-options-2012-1.png new file mode 100644 index 0000000..4e44b42 Binary files /dev/null and b/src/test-driven-drupal/images/override-node-options-2012-1.png differ diff --git a/src/test-driven-drupal/images/override-node-options-2012-2.png b/src/test-driven-drupal/images/override-node-options-2012-2.png new file mode 100644 index 0000000..0c2d53b Binary files /dev/null and b/src/test-driven-drupal/images/override-node-options-2012-2.png differ diff --git a/src/test-driven-drupal/images/override-node-options-2012-3.png b/src/test-driven-drupal/images/override-node-options-2012-3.png new file mode 100644 index 0000000..700cb86 Binary files /dev/null and b/src/test-driven-drupal/images/override-node-options-2012-3.png differ diff --git a/src/test-driven-drupal/images/override-node-options-2012-4.png b/src/test-driven-drupal/images/override-node-options-2012-4.png new file mode 100644 index 0000000..ac53a5d Binary files /dev/null and b/src/test-driven-drupal/images/override-node-options-2012-4.png differ diff --git a/src/test-driven-drupal/images/override-node-options-2020-1.png b/src/test-driven-drupal/images/override-node-options-2020-1.png new file mode 100644 index 0000000..aff4836 Binary files /dev/null and b/src/test-driven-drupal/images/override-node-options-2020-1.png differ diff --git a/src/test-driven-drupal/images/override-node-options-2020-2.png b/src/test-driven-drupal/images/override-node-options-2020-2.png new file mode 100644 index 0000000..c6000d8 Binary files /dev/null and b/src/test-driven-drupal/images/override-node-options-2020-2.png differ diff --git a/src/test-driven-drupal/slides.md b/src/test-driven-drupal/slides.md index 036209d..aae6c50 100644 --- a/src/test-driven-drupal/slides.md +++ b/src/test-driven-drupal/slides.md @@ -1,110 +1,67 @@ -theme: poster, 8 autoscale: true build-lists: true header-emphasis: #53B0EB -header: alignment(left) text: alignment(left) text-emphasis: #53B0EB code: -Monaco, #6699FF, #999999, #6666FF, #66FF66, #66FF66, line-height(1.3) +theme: Simple, 8 +autoscale: true +build-lists: true +header: alignment(left), line-height(1.1), text-scale(1.3) +text: alignment(left) +text-emphasis: #53B0EB -[.header: alignment(center)] ![](images/title.png) -# _TDD:_ Test
Driven Drupal - ---- - [.header: alignment(center)] -## [fit] opdavi.es/_tdd-test-driven-drupal_ - -^ View on the website or click through to Speakerdeck - ---- - -[.build-lists: false] - -- Module and theme developers -- Want to know more about automated testing -- Looking to start writing your first tests -- Drupal 8 -- PHPUnit +# **TDD: Test
Driven Drupal** --- - Why write tests, and what to test - Types of tests - How to run tests -- Real life example -- Building a new module with TDD +- Example +- Building a new module with test driven development --- -[.background-color: #FFFFFF][.build-lists: false] [.header: -#111111][.text: #111111, alignment(left)] +[.background-color: #FFFFFF] +[.build-lists: false] +[.header: #111111] +[.text: #111111, alignment(left)] ![right 800%](../images/me-phpnw.png) - Full Stack Web Developer & System Administrator -- Senior Developer at Microserve -- Part-time freelancer -- Acquia certified Drupal 8 Grand Master -- Drupal 7 & 8 core contributor -- Drupal, Symfony, Laravel, Sculpin +- Senior Software Engineer at Inviqa +- PHP South Wales organiser - @opdavies - www.oliverdavies.uk -^ Work at Microserve. Maintain Drupal modules, PHP CLI tools and libraries Blog -on my website - ---- - -[.build-lists: false] - -- opdavi.es/_talks_ -- opdavi.es/_twitter_ -- opdavi.es/_drupal_ -- opdavi.es/_github_ - -^ Example code on GitHub - ---- - -[.background-color: #FFFFFF][.text: #111111, alignment(left)] - -![right 100%](../images/microserve-light.png) - -- https://microserve.io -- https://www.drupal.org/microserve -- https://github.com/microserve-io -- https://twitter.com/microserveltd -- https://www.linkedin.com/company/microserve-ltd +^ - Work at Inviqa. +- Organiser of the PHP South Wales user group. Formerly PHP South West, Drupal Bristol, SWDUG +- Maintain Drupal modules, PHP CLI tools and libraries +- Blog on my website --- [.header: alignment(center)] -## test*driven_drupal*.com\_ - -^ Book on automated testing in Drupal 8 Building a conference website - ---- - -![80%](images/tdd-drupal-tasks.png) +## Write **custom modules
and themes** for clients --- [.header: alignment(center)] -## Write custom modules and themes _for clients_ +## Contributor to **Drupal core** + +^ - Drupal 7 and 8 core contributor +- Most recent patch submitted on a live stream --- [.header: alignment(center)] -## Occassionally
contribute _to core_ +## Maintain and contribute
to **contrib projects** ---- - -[.header: alignment(center)] - -## Maintain and contribute to _contrib projects_ +^ Approaching this from a few different angles --- @@ -114,14 +71,11 @@ on my website --- -## _Override Node Options_ +## Override Node Options - Become maintainer in 2012 +- **#232** most used module on Drupal.org (May 2020) - Had some existing tests -- Used on _11,046 sites_ in October 2012 (_84_ D5, _7,094_ D6, _3,868_ D7) -- Used on _30,572 sites_ in March 2019 (_10_ D5, _1,180_ D6, _24,057_ D7, - _5,335_ D8) -- _#230_ most used module on Drupal.org - Crucial to preventing regressions ^ Preventing regressions when adding new features or fixing bugs, but also user @@ -129,64 +83,70 @@ submitted patches First module I ported to Drupal 8, aided by tests --- -[.header: alignment(center)] - -## _Why_ write tests? +![](images/override-node-options-2012-1.png) --- -## _Why write tests?_ +![](images/override-node-options-2012-4.png) + +--- + +![](images/override-node-options-2020-2.png) + +^ - D5 and D6 gone down, D7 up and a few different D8 versions including D9 compatibility. +- Around 30,000 sites + +--- + +[.header: alignment(center)] + +## **Why write tests?** + +--- + +## Why write tests? - Catch bugs earlier - Peace of mind - Prevent regressions - Write less code - Documentation -- Drupal core requirement - __ -- More important with regular D8 releases +- Drupal core requirement +- More important with regular D8/D9 releases and supporting multiple versions -^ Dave Liddament talk - better and cheaper to catch bugs earlier (e.g. whilst -developing rather than after it's been released) Refer to tests when writing -implementation code ONO merge conflict +^ Dave Liddament talk - better and cheaper to catch bugs earlier (e.g. whilst developing rather than after it's been released) +Refer to tests when writing implementation code ONO merge conflict --- -## _Core Testing Gate_ +## Core Testing Gate -- New features should be accompanied by automated tests. -- If the feature does not have an implementation, provide a test implementation. -- Bug fixes should be accompanied by changes to a test (either modifying an - existing test case or adding a new one) that demonstrate the bug. +| Description | When | +| --- | --- | +| Check test coverage and ensure all tests pass | When changing/refactoring existing code | +| Add new tests | When adding new features | +| Upload a test case that fails | When fixing bugs in PHP code | +| Add JavaScript test | When making JS changes | +| Manually test in browsers and provide screenshots or screencasts | When making markup or CSS changes | +| Provide an example module | When a new feature is not implemented by Drupal core | [.footer: https://opdavi.es/drupal-core-testing-gate] --- -[.background-color: #FFFFFF] +## Testing in Drupal -![fit inline](images/afilina-tweet.png) +- **Drupal 7** - SimpleTest (testing) module provided as part of core +- **Drupal 8** - PHPUnit added as a core dependency, later became the default via the PHPUnit initiative +- **Drupal 9** - SimpleTest removed, moved to contrib --- -## _Testing in Drupal_ - -- _Drupal 7_ - SimpleTest (testing) module provided as part of core -- _Drupal 8_ - PHPUnit added as a core dependency -- _PHPUnit Initiative_ - SimpleTest to be deprecated and removed in Drupal 9 - ---- - -![fit](images/simpletest-countdown.png) - ---- - -[.header: #53B0EB] - ## Writing Tests (Drupal 8) -- PHP class with _.php_ extension -- _tests/src_ directory within each module -- Within the _Drupal\Tests\module_name_ namespace +- PHP class with **.php** extension +- **tests/src** directory within each module +- Within the **Drupal\Tests\module_name** namespace - Class name must match the filename - Namespace must match the directory structure - One test class per feature @@ -197,25 +157,35 @@ implementation code ONO merge conflict [.header: alignment(center)] -## _1._ Arrange +## **Arrange** -## _2._ Act +## **Act** -## _3._ Assert +## **Assert** --- [.header: alignment(center)] -## _3._ Assert +## **Given** -## _2._ Act +## **When** -## _1._ Arrange +## **Then** --- -```php +[.header: alignment(center)] + +## **Given** the About page exists + +## **When** I go to the page + +## **Then** I should see "About Me" + +--- + +```php, [.highlight: 1-3] // modules/example/tests/src/Functional/ExampleTest.php namespace Drupal\Tests\example\Functional; @@ -225,18 +195,73 @@ use Drupal\Tests\BrowserTestBase; class ExampleTest extends BrowserTestBase { public function testSomething() { - // Arrange - - // Act - - // Assert + // Given... + // When... + // Then... } } ``` -^ PHP class Filename matches class name Namespace matches directory structure -Extend BrowserTestBase Add test method +--- + +```php, [.highlight: 5-7] +// modules/example/tests/src/Functional/ExampleTest.php + +namespace Drupal\Tests\example\Functional; + +use Drupal\Tests\BrowserTestBase; + +class ExampleTest extends BrowserTestBase { + + public function testSomething() { + // Given... + // When... + // Then... + } + +} +``` + +--- + +```php, [.highlight: 9,13] +// modules/example/tests/src/Functional/ExampleTest.php + +namespace Drupal\Tests\example\Functional; + +use Drupal\Tests\BrowserTestBase; + +class ExampleTest extends BrowserTestBase { + + public function testSomething() { + // Given... + // When... + // Then... + } + +} +``` + +--- + +```php, [.highlight: 10-12] +// modules/example/tests/src/Functional/ExampleTest.php + +namespace Drupal\Tests\example\Functional; + +use Drupal\Tests\BrowserTestBase; + +class ExampleTest extends BrowserTestBase { + + public function testSomething() { + // Given... + // When... + // Then... + } + +} +``` --- @@ -251,8 +276,6 @@ public function it_does_something() {} --- -[.header: #53B0EB] - ## What to test? - Creating nodes with data from an API @@ -265,348 +288,30 @@ public function it_does_something() {} - Closed support tickets are re-opened when comments are added - Custom form validation rules ---- - -[.header: #53B0EB] - -## What to test first? - -- What is the core piece of functionality? -- What provides the most value to the client? -- What would you not like to be fixing on a Friday afternoon or after hours? - -^ Payments! Anything related to money. What would provide the largest negative -impact to the client if it were to fail? +^ Examples of some things that I tested on a previous project. --- [.background-color: #FFFFFF] -![inline 150%](images/matt-stauffer-tweet.png) +![inline 125%](images/matt-stauffer-tweet.png) --- -[.header: #53B0EB] +[.header: alignment(center) ] -## What to test first? - -- Write a _new test_ when adding any _new functionality_ -- Write a _regression test_ when _fixing a bug_ - -^ Use tests to replicate the bug Could be a new test, or adding to an existing -test Test passes when the bug is fixed That issue cannot be re-added without the -test failing again +## **Types of Tests** --- -[.header: #53B0EB] +## Types of Tests -## Types of tests - -- Unit -- Kernel _(integration)_ -- Functional / FunctionalJavascript _(web, browser, feature)_ +- **Functional** / **FunctionalJavascript** (web, browser, feature) +- **Kernel** (integration) +- **Unit** --- -[.header: #53B0EB] - -## Unit tests - -- Tests PHP logic -- No database interaction -- Fast to run -- Need to mock dependencies -- Can become tightly coupled -- Can be hard to refactor - ---- - -```php -// tests/src/Unit/JobTest.php - -namespace Drupal\Tests\advancedqueue\Unit; - -use Drupal\advancedqueue\Job; -use Drupal\Tests\UnitTestCase; - -class JobTest extends UnitTestCase { - - public function testCreate() { - $job = Job::create('test', ['my' => 'data']); - - $this->assertEquals('test', $job->getType()); - $this->assertEquals(['my' => 'data'], $job->getPayload()); - $this->assertEquals(Job::STATE_QUEUED, $job->getState()); - } - -} -``` - -^ Within a Unit directory and namespace Called JobTest because it's testing the -Job class Called testCreate because it's testing the create method - ---- - -```php, [.highlight: 11] -// tests/src/Unit/JobTest.php - -namespace Drupal\Tests\advancedqueue\Unit; - -use Drupal\advancedqueue\Job; -use Drupal\Tests\UnitTestCase; - -class JobTest extends UnitTestCase { - - public function testCreate() { - $job = Job::create('test', ['my' => 'data']); - - $this->assertEquals('test', $job->getType()); - $this->assertEquals(['my' => 'data'], $job->getPayload()); - $this->assertEquals(Job::STATE_QUEUED, $job->getState()); - } - -} -``` - -^ Create a job with the create method - ---- - -```php, [.highlight: 13-15] -// tests/src/Unit/JobTest.php - -namespace Drupal\Tests\advancedqueue\Unit; - -use Drupal\advancedqueue\Job; -use Drupal\Tests\UnitTestCase; - -class JobTest extends UnitTestCase { - - public function testCreate() { - $job = Job::create('test', ['my' => 'data']); - - $this->assertEquals('test', $job->getType()); - $this->assertEquals(['my' => 'data'], $job->getPayload()); - $this->assertEquals(Job::STATE_QUEUED, $job->getState()); - } - -} -``` - -^ Retrieve data from the object with getters Asssert that the data is correct. - ---- - -[.header: #53B0EB] - -## Kernel Tests - -- Integration tests -- Can install modules, interact with services, container, database -- Minimal Drupal bootstrap -- Faster than functional tests -- More setup required - ---- - -```php -// tests/src/Kernel/ProcessorTest.php - -namespace Drupal\Tests\advancedqueue\Kernel; - -use Drupal\advancedqueue\Entity\Queue; -use Drupal\advancedqueue\Job; -use Drupal\KernelTests\KernelTestBase; - -class ProcessorTest extends KernelTestBase { - - ... - -} -``` - ---- - -```php -// tests/src/Kernel/ProcessorTest.php - -protected function setUp() { - parent::setUp(); - - $this->installSchema('advancedqueue', ['advancedqueue']); - - $this->queue = Queue::create([ - 'id' => 'test', - 'label' => 'Test queue', - 'backend' => 'database', - 'backend_configuration' => [ - 'lease_time' => 5, - ], - ]); - $this->queue->save(); - - $this->processor = $this->container->get('advancedqueue.processor'); -} -``` - -^ Steps that need to run before each test method - ---- - -```php, [.highlight: 6] -// tests/src/Kernel/ProcessorTest.php - -protected function setUp() { - parent::setUp(); - - $this->installSchema('advancedqueue', ['advancedqueue']); - - $this->queue = Queue::create([ - 'id' => 'test', - 'label' => 'Test queue', - 'backend' => 'database', - 'backend_configuration' => [ - 'lease_time' => 5, - ], - ]); - $this->queue->save(); - - $this->processor = $this->container->get('advancedqueue.processor'); -} -``` - -^ Create the database tables - ---- - -```php, [.highlight: 8-16] -// tests/src/Kernel/ProcessorTest.php - -protected function setUp() { - parent::setUp(); - - $this->installSchema('advancedqueue', ['advancedqueue']); - - $this->queue = Queue::create([ - 'id' => 'test', - 'label' => 'Test queue', - 'backend' => 'database', - 'backend_configuration' => [ - 'lease_time' => 5, - ], - ]); - $this->queue->save(); - - $this->processor = $this->container->get('advancedqueue.processor'); -} -``` - -^ Create and save a queue - ---- - -```php, [.highlight: 18] -// tests/src/Kernel/ProcessorTest.php - -protected function setUp() { - parent::setUp(); - - $this->installSchema('advancedqueue', ['advancedqueue']); - - $this->queue = Queue::create([ - 'id' => 'test', - 'label' => 'Test queue', - 'backend' => 'database', - 'backend_configuration' => [ - 'lease_time' => 5, - ], - ]); - $this->queue->save(); - - $this->processor = $this->container->get('advancedqueue.processor'); -} -``` - -^ Because it's a kernel test, we have access to the container to get the -AdvancedQueue processor service. - ---- - -```php -// tests/src/Kernel/ProcessorTest.php - -public function testProcessor() { - $first_job = Job::create('simple', [ - 'test' => '1', - ]); - - $second_job = Job::create('flexible', [ - 'expected_state' => Job::STATE_SUCCESS, - 'expected_message' => 'Done!', - ]); - - $third_job = Job::create( - 'flexible', ['expected_exception' => 'DB down!'], - ); - - $fourth_job = Job::create('flexible', [ - 'expected_state' => Job::STATE_FAILURE, - 'expected_message' => 'Failed!', - ]); - - ... -} -``` - -^ Start by creating some jobs. - ---- - -```php, [.highlight: 6-10] -// tests/src/Kernel/ProcessorTest.php - -public function testProcessor() { - ... - - $this->queue->enqueueJob($first_job); - $this->queue->enqueueJob($second_job); - $this->queue->enqueueJob($third_job); - $this->queue->enqueueJob($fourth_job); - - $num_processed = $this->processor->processQueue($this->queue); - - $this->assertEquals(4, $num_processed); -} -``` - -^ Add the jobs to the queue - ---- - -```php, [.highlight: 11-13] -// tests/src/Kernel/ProcessorTest.php - -public function testProcessor() { - ... - - $this->queue->enqueueJob($first_job); - $this->queue->enqueueJob($second_job); - $this->queue->enqueueJob($third_job); - $this->queue->enqueueJob($fourth_job); - - $num_processed = $this->processor->processQueue($this->queue); - - $this->assertEquals(4, $num_processed); -} -``` - -^ Process the queue, and check the number of processed items. - ---- - -[.header: #53B0EB] - ## Functional Tests - Tests end-to-end functionality @@ -621,261 +326,376 @@ public function testProcessor() { --- ```php -// tests/src/Functional/QueueTest.php +class BlockTest extends BlockTestBase { -namespace Drupal\Tests\advancedqueue\Functional; + public function testBlockVisibility() { + $block_name = 'system_powered_by_block'; + $title = $this->randomMachineName(8); -use Drupal\advancedqueue\Entity\Queue; -use Drupal\advancedqueue\Entity\QueueInterface; -use Drupal\Tests\BrowserTestBase; + $default_theme = $this->config('system.theme')->get('default'); -class QueueTest extends BrowserTestBase { - ... -} -``` + $edit = [ + 'id' => strtolower($this->randomMachineName(8)), + 'region' => 'sidebar_first', + 'settings[label]' => $title, + 'settings[label_display]' => TRUE, + ]; -^ Extend BrowserTestBase + $edit['visibility[request_path][pages]'] = '/user*'; + $edit['visibility[request_path][negate]'] = TRUE; + $edit['visibility[user_role][roles][' . RoleInterface::AUTHENTICATED_ID . ']'] = TRUE; ---- - -```php, [.highlight: 6-8] -// tests/src/Functional/QueueTest.php - -protected function setUp() { - parent::setUp(); - - $this->placeBlock('local_tasks_block'); - $this->placeBlock('local_actions_block'); - $this->placeBlock('page_title_block'); - - $this->adminUser = $this->drupalCreateUser(['administer advancedqueue']); - $this->drupalLogin($this->adminUser); -} -``` - -^ We have the ability to place blocks And create users with permissions and log -them in - ---- - -```php, [.highlight: 10-11] -// tests/src/Functional/QueueTest.php - -protected function setUp() { - parent::setUp(); - - $this->placeBlock('local_tasks_block'); - $this->placeBlock('local_actions_block'); - $this->placeBlock('page_title_block'); - - $this->adminUser = $this->drupalCreateUser(['administer advancedqueue']); - $this->drupalLogin($this->adminUser); -} + // ... + } ``` --- -```php, [.highlight: 4-11] -// tests/src/Functional/QueueTest.php +```php, [.highlight: 4-18] +class BlockTest extends BlockTestBase { -public function testQueueDeletion() { - $queue = Queue::create([ - 'id' => 'test', - 'label' => 'Test', - 'backend' => 'database', - 'processor' => QueueInterface::PROCESSOR_DAEMON, - 'processing_time' => 100, - ]); - $queue->save(); - $this->drupalGet('admin/config/system/queues/manage/' . $queue->id() . '/delete'); - $this->submitForm([], 'Delete'); - $this->assertSession()->addressEquals('admin/config/system/queues'); + public function testBlockVisibility() { + $block_name = 'system_powered_by_block'; + $title = $this->randomMachineName(8); - $queue_exists = (bool) Queue::load('test'); - $this->assertEmpty($queue_exists, 'The queue has been deleted from the database.'); -} + $default_theme = $this->config('system.theme')->get('default'); + + $edit = [ + 'id' => strtolower($this->randomMachineName(8)), + 'region' => 'sidebar_first', + 'settings[label]' => $title, + 'settings[label_display]' => TRUE, + ]; + + $edit['visibility[request_path][pages]'] = '/user*'; + $edit['visibility[request_path][negate]'] = TRUE; + $edit['visibility[user_role][roles][' . RoleInterface::AUTHENTICATED_ID . ']'] = TRUE; + + // ... + } ``` ---- - -```php, [.highlight: 12-14] -// tests/src/Functional/QueueTest.php - -public function testQueueDeletion() { - $queue = Queue::create([ - 'id' => 'test', - 'label' => 'Test', - 'backend' => 'database', - 'processor' => QueueInterface::PROCESSOR_DAEMON, - 'processing_time' => 100, - ]); - $queue->save(); - $this->drupalGet('admin/config/system/queues/manage/' . $queue->id() . '/delete'); - $this->submitForm([], 'Delete'); - $this->assertSession()->addressEquals('admin/config/system/queues'); - - $queue_exists = (bool) Queue::load('test'); - $this->assertEmpty($queue_exists, 'The queue has been deleted from the database.'); -} -``` - -^ I prefer to use the route name and Url::fromRoute +^ Arrange step --- -```php, [.highlight: 16-17] -// tests/src/Functional/QueueTest.php - -public function testQueueDeletion() { - $queue = Queue::create([ - 'id' => 'test', - 'label' => 'Test', - 'backend' => 'database', - 'processor' => QueueInterface::PROCESSOR_DAEMON, - 'processing_time' => 100, - ]); - $queue->save(); - $this->drupalGet('admin/config/system/queues/manage/' . $queue->id() . '/delete'); - $this->submitForm([], 'Delete'); - $this->assertSession()->addressEquals('admin/config/system/queues'); - - $queue_exists = (bool) Queue::load('test'); - $this->assertEmpty($queue_exists, 'The queue has been deleted from the database.'); -} -``` - ---- - -[.header: #FFFFFF, alignment(left)] - -### _How do I know_ - -## Which type of test to use? - ---- - -### _Need a browser?_ - -## Use a functional test - ---- - -### _Interact with other services?_ - -## Use a kernel test - ---- - -### _Isolated PHP code?_ - -## Use a unit test - ---- - -## _Should you test that_
a block is rendered correctly? - ---- - -## _Or should you test_
your render array to generate the block? - -^ The answer might be 'both'. The right type of test to use might not be that -obvious. You may be able to use a different type of test if you take a different -approach. - ---- - -[.header: #53B0EB] - -## Setup (functional) - -``` -drupalCreateUser() -drupalCreateRole() - -drupalLogin() -drupalLogout() - -drupalGet() -drupalPost() -drupalPostForm() -``` - ---- - -## _Setup (kernel)_ - ```php -# UserCreationTrait -createUser() -createAdminRole() -createRole() -checkPermissions() -# CommentTestTrait -addDefaultCommentField() +class BlockTest extends BlockTestBase { -# AssertMailTrait -getMails() -assertMail() + public function testBlockVisibility() { + // ... + + $this->drupalGet('admin/structure/block/add/' . $block_name . '/' . $default_theme); + $this->assertFieldChecked('edit-visibility-request-path-negate-0'); + + $this->drupalPostForm(NULL, $edit, t('Save block')); + $this->assertText('The block configuration has been saved.', 'Block was saved'); + + $this->clickLink('Configure'); + $this->assertFieldChecked('edit-visibility-request-path-negate-1'); + + $this->drupalGet(''); + $this->assertText($title, 'Block was displayed on the front page.'); + + $this->drupalGet('user'); + $this->assertNoText($title, 'Block was not displayed according to block visibility rules.'); + + // ... + } ``` --- -[.header: #53B0EB] +```php, [.highlight: 6,9,12,15,18] +class BlockTest extends BlockTestBase { -## Assertions + public function testBlockVisibility() { + // ... -```php -assertTrue() -assertFalse() + $this->drupalGet('admin/structure/block/add/' . $block_name . '/' . $default_theme); + $this->assertFieldChecked('edit-visibility-request-path-negate-0'); -assertEquals() -assertSame() + $this->drupalPostForm(NULL, $edit, t('Save block')); + $this->assertText('The block configuration has been saved.', 'Block was saved'); -assertNull() -assertNotNull() + $this->clickLink('Configure'); + $this->assertFieldChecked('edit-visibility-request-path-negate-1'); -assertCount() -assertEmpty() + $this->drupalGet(''); + $this->assertText($title, 'Block was displayed on the front page.'); -assertArraySubset() + $this->drupalGet('user'); + $this->assertNoText($title, 'Block was not displayed according to block visibility rules.'); + + // ... + } ``` --- -[.header: #53B0EB] +```php, [.highlight: 7,10,13,16,19] +class BlockTest extends BlockTestBase { -## Assertions (functional) + public function testBlockVisibility() { + // ... + + $this->drupalGet('admin/structure/block/add/' . $block_name . '/' . $default_theme); + $this->assertFieldChecked('edit-visibility-request-path-negate-0'); + + $this->drupalPostForm(NULL, $edit, t('Save block')); + $this->assertText('The block configuration has been saved.', 'Block was saved'); + + $this->clickLink('Configure'); + $this->assertFieldChecked('edit-visibility-request-path-negate-1'); + + $this->drupalGet(''); + $this->assertText($title, 'Block was displayed on the front page.'); + + $this->drupalGet('user'); + $this->assertNoText($title, 'Block was not displayed according to block visibility rules.'); + + // ... + } +``` + +--- + +## Kernel Tests + +- Integration tests +- Can install modules, interact with services, container, database +- Minimal Drupal bootstrap +- Faster than functional tests +- More setup required + +--- ```php -assertSession() -pageTextContains() -pageTextNotContains() +class BlockRebuildTest extends KernelTestBase { -linkByHrefExists() -linkByHrefNotExists() + use BlockCreationTrait; -statusCodeEquals() -statusCodeNotEquals() + public static $modules = ['block', 'system']; + + protected function setUp() { + parent::setUp(); + + $this->container->get('theme_installer') + ->install(['stable', 'classy']); + + $this->container->get('config.factory') + ->getEditable('system.theme') + ->set('default', 'classy') + ->save(); + } + + // ... + +} +``` + +--- + +```php, [.highlight: 5, 10-16] + +class BlockRebuildTest extends KernelTestBase { + + use BlockCreationTrait; + + public static $modules = ['block', 'system']; + + protected function setUp() { + parent::setUp(); + + $this->container->get('theme_installer') + ->install(['stable', 'classy']); + + $this->container->get('config.factory') + ->getEditable('system.theme') + ->set('default', 'classy') + ->save(); + } + + // ... + +} +``` + +^ Still have access to the service container, via $this->container + +--- + +```php +class BlockRebuildTest extends KernelTestBase { + + // ... + + public function testRebuildNoBlocks() { + block_rebuild(); + + $messages = \Drupal::messenger()->all(); + \Drupal::messenger()->deleteAll(); + + $this->assertEquals([], $messages); + } + +} +``` + +^ Can still access services like \Drupal::messenger() + +--- + +## Unit Tests + +- Tests PHP logic +- No database interaction +- Fast to run +- Need to mock dependencies +- Can become tightly coupled +- Can be hard to refactor + +--- + +```php +class BlockRepositoryTest extends UnitTestCase { + + public function testGetVisibleBlocksPerRegion(array $blocks_config, array $expected_blocks) { + $blocks = []; + + foreach ($blocks_config as $block_id => $block_config) { + $block = $this->getMock('Drupal\block\BlockInterface'); + + $block->expects($this->once()) + ->method('access') + ->will($this->returnValue($block_config[0])); + + $block->expects($block_config[0] ? $this->atLeastOnce() : $this->never()) + ->method('getRegion') + ->willReturn($block_config[1]); + + $block->expects($this->any()) + ->method('label') + ->willReturn($block_id); + + // ... + + $blocks[$block_id] = $block; + } + + // ... + } +} +``` + +--- + +```php, [.highlight: 7-20] + +class BlockRepositoryTest extends UnitTestCase { + + public function testGetVisibleBlocksPerRegion(array $blocks_config, array $expected_blocks) { + $blocks = []; + + foreach ($blocks_config as $block_id => $block_config) { + $block = $this->getMock('Drupal\block\BlockInterface'); + + $block->expects($this->once()) + ->method('access') + ->will($this->returnValue($block_config[0])); + + $block->expects($block_config[0] ? $this->atLeastOnce() : $this->never()) + ->method('getRegion') + ->willReturn($block_config[1]); + + $block->expects($this->any()) + ->method('label') + ->willReturn($block_id); + + // ... + + $blocks[$block_id] = $block; + } + + // ... + } +} +``` + +--- + +```php +class BlockRepositoryTest extends UnitTestCase { + + public function testGetVisibleBlocksPerRegion(array $blocks_config, array $expected_blocks) { + // .. + + $this->blockStorage->expects($this->once()) + ->method('loadByProperties') + ->with(['theme' => $this->theme]) + ->willReturn($blocks); + + $result = []; + $cacheable_metadata = []; + + foreach ($this->blockRepository->getVisibleBlocksPerRegion($cacheable_metadata) as $region => $resulting_blocks) { + $result[$region] = []; + + foreach ($resulting_blocks as $plugin_id => $block) { + $result[$region][] = $plugin_id; + } + } + + $this->assertEquals($expected_blocks, $result); + } + +} +``` + +--- + +```php, [.highlight: 14-22] +class BlockRepositoryTest extends UnitTestCase { + + public function testGetVisibleBlocksPerRegion(array $blocks_config, array $expected_blocks) { + // .. + + $this->blockStorage->expects($this->once()) + ->method('loadByProperties') + ->with(['theme' => $this->theme]) + ->willReturn($blocks); + + $result = []; + $cacheable_metadata = []; + + foreach ($this->blockRepository->getVisibleBlocksPerRegion($cacheable_metadata) as $region => $resulting_blocks) { + $result[$region] = []; + + foreach ($resulting_blocks as $plugin_id => $block) { + $result[$region][] = $plugin_id; + } + } + + $this->assertEquals($expected_blocks, $result); + } + +} ``` --- [.header: alignment(center)] -# _Real life_ example +# **Example** --- -[.background-color: #FFFFFF][.footer-style: #2f2f2f] - -![inline 150%](images/broadbean.png) +![](images/broadbean-website.png) --- -[.header: #53B0EB] - ## Specification - Job adverts created in Broadbean UI, needs to create nodes in Drupal @@ -883,14 +703,7 @@ statusCodeNotEquals() - Jobs need to be linked to offices - Job length specified in number of days - Path is specified as a field in the API -- Application URL constructed from domain, includes role ID as a GET parameter - and optionally UTM parameters - ---- - -[.background-color: #FFFFFF] - -![inline 125%](images/broadbean-drupal-flow-1.png) +- Application URL constructed from domain, includes role ID as a GET parameter and optionally UTM parameters --- @@ -900,6 +713,16 @@ statusCodeNotEquals() --- +## Implementation + +- Added route to accept data from API as XML +- Added system user with API role to authenticate +- **active_for** converted from number of days to UNIX timestamp +- **branch_name** and **locations** converted from plain text to entity reference (job node to office node) +- **url_alias** property mapped to **path** + +--- + ```php $data = [ 'command' => 'add', @@ -927,21 +750,33 @@ $data = [ --- -[.header: #53B0EB] - -## Implementation - -- Added route to accept data from API as XML -- Added system user with API role to authenticate -- _active_for_ converted from number of days to UNIX timestamp -- _branch_name_ and _locations_ converted from plain text to entity reference - (job node to office node) -- _url_alias_ property mapped to _path_ +```php, [.highlight: 5,8,15,21] +$data = [ + 'command' => 'add', + 'username' => 'bobsmith', + 'password' => 'p455w0rd', + 'active_for' => '365', + 'application_email' => 'bob.12345.123@smith.aplitrak.com', + 'branch_address' => '123 Fake St, Bristol, BS1 2AB', + 'branch_name' => 'Test', + 'contract' => 'Temporary', + 'details' => 'This is the detailed description.', + 'job_id' => 'abc123_1234567', + 'job_title' => 'Healthcare Assistant (HCA)', + 'job_type' => 'Care at Home', + 'keywords' => 'flexible, Bristol, part-time', + 'locations' => 'Bath, Devizes', + 'role_id' => 'A/52/86', + 'salary' => '32,000.00 per annum', + 'salary_prefix' => 'Basic Salary', + 'status' => 'Part time', + 'summary' => 'This is the short description.', + 'url_alias' => 'healthcare-assistant-aldershot-june17', +]; +``` --- -[.header: #53B0EB] - ## Implementation - If no error, create the job node, return OK response to Broadbean @@ -951,87 +786,46 @@ $data = [ --- -[.header: #53B0EB] - ## Testing Goals -- Ensure job nodes are _successfully created_ -- Ensure that fields are _mapped correctly_ -- Ensure that _calculations are correct_ -- Ensure that entity references are _linked correctly_ +- Ensure job nodes are **successfully created** +- Ensure that fields are **mapped correctly** +- Ensure that **calculations are correct** +- Ensure that entity references are **linked correctly** --- -[.header: #53B0EB] - ## Types of tests -- _Unit:_ ensure number of days are converted to timestamps correctly -- _Kernel:_ job nodes can be added and deleted, expired job nodes are deleted, - application URL is generated correctly -- _Functional:_ job nodes are created with the correct URL and the correct - response code is returned -- _FunctionalJavaScript:_ application URL is updated with JavaScript based on - UTM parameters (hosting) +- **Functional:** job nodes are created with the correct URL and the correct response code is returned +- **FunctionalJavaScript:** application URL is updated with JavaScript based on UTM parameters (hosting) --- -[.header: #53B0EB] +## Types of tests + +- **Kernel:** job nodes can be added and deleted, expired job nodes are deleted, application URL is generated correctly +- **Unit:** ensure number of days are converted to timestamps correctly + +--- ## Results -- _0 bugs!_ -- Reduced debugging time +- **0 bugs!** - Easier to identify where issues occurred and responsibilities +- Reduced debugging time +- Added more tests for any bugs to prevent --- [.header: alignment(center)] -## Running Tests +## **Running Tests** --- -### _Option 1_ - -## SimpleTest module (UI) - ---- - -![fit](images/d8-simpletest-1.png) - ---- - -![fit](images/d8-simpletest-2.png) - ---- - -![fit](images/d8-simpletest-3.png) - ---- - -![fit](images/d8-simpletest-4.png) - ---- - -![fit](images/d8-simpletest-5.png) - ---- - -![fit](images/d8-simpletest-6.png) - ---- - -![fit](images/d8-simpletest-7.png) - ---- - -### _Option 2_ - ## Core script ---- - ``` $ php core/scripts/run-tests.sh @@ -1042,84 +836,34 @@ $ php core/scripts/run-tests.sh --class ExampleTest --- -### _Option 3_ - ## PHPUnit +``` +$ vendor/bin/phpunit \ + -c core \ + modules/contrib/examples/phpunit_example +``` + --- -## Prerequisite _(creating a phpunit.xml file)_ +## Prerequisite (creating a phpunit.xml file) - Configures PHPUnit - Needed to run some types of tests - Ignored by Git by default -- Copy _core/phpunit.xml.dist_ to _core/phpunit.xml_ +- Copy **core/phpunit.xml.dist** to **core/phpunit.xml** - Add and change as needed - `SIMPLETEST_BASE_URL`, `SIMPLETEST_DB`, `BROWSERTEST_OUTPUT_DIRECTORY` - `stopOnFailure="true"` --- -``` -vendor/bin/phpunit \ --c core \ -modules/contrib/examples/phpunit_example -``` - ---- - -``` -cd core - -../vendor/bin/phpunit \ -../modules/contrib/examples/phpunit_example -``` - ---- - -``` ---filter - ---testsuite - ---group - ---colors - ---stop-on-failure - ---verbose --debug -``` - ---- - -# _Docksal_ - -``` -fin addon install phpunit - -fin phpunit modules/custom -``` - -^ Copies a stub phpunit.xml file or copies phpunit.xml.dist Runs the phpunit -command within the correct directory - ---- - -![fit](images/phpstorm-integration.png) - -[.footer: opdavi.es/docksal-phpunit-phpstorm] - ---- - [.header: alignment(center)] -## _Test Driven_
Development +## **Test Driven
Development** --- -[.header: #53B0EB] - ## Test Driven Development - Write a test @@ -1131,8 +875,8 @@ command within the correct directory --- -[.background-color: -#FFFFFF][.footer: https://github.com/foundersandcoders/testing-tdd-intro] +[.background-color: #FFFFFF] +[.footer: https://github.com/foundersandcoders/testing-tdd-intro] [.footer-style: #2F2F2F] ![100%](images/tdd-loop.png) @@ -1141,12 +885,10 @@ command within the correct directory [.header: alignment(center)] -## Red, Green, Refactor +## **Red, Green, Refactor** --- -[.header: #53B0EB] - ## Porting Modules to Drupal 8 - Make a new branch @@ -1157,8 +899,6 @@ command within the correct directory --- -[.header: #53B0EB] - ## How I Write Tests - "Outside In" - Start with functional tests @@ -1171,14 +911,10 @@ command within the correct directory [.header: alignment(center)] -## [fit] _Building a new Drupal 8 Module with_ - -## [fit] test driven development +## **Demo: Building a Blog module** --- -[.header: #53B0EB] - ## Acceptance criteria - As a site visitor @@ -1187,8 +923,6 @@ command within the correct directory --- -[.header: #53B0EB] - ## Tasks - Ensure the blog page exists @@ -1197,1132 +931,6 @@ command within the correct directory --- -[.header: #53B0EB] - -## Implementation - -- Use views module -- Do the mininum amount at each step, make no assumptions, let the tests guide - us -- Start with functional test - ---- - -### _Step 1_ - -## Create the module - ---- - -```yml -# tdd_blog.info.yml - -name: 'TDD Blog' -core: '8.x' -type: 'module' -``` - ---- - -### _Step 2_ - -## Ensure the blog page exists - ---- - -```php -// tests/src/Functional/BlogPageTest.php - -namespace Drupal\Tests\tdd_blog\Functional; - -use Drupal\Tests\BrowserTestBase; - -class BlogPageTest extends BrowserTestBase { - - protected static $modules = ['tdd_blog']; - - public function testBlogPageExists() { - $this->drupalGet('/blog'); - - $this->assertSession()->statusCodeEquals(200); - } - -} -``` - ---- - -```php, [.highlight: 3] -// tests/src/Functional/BlogPageTest.php - -namespace Drupal\Tests\tdd_blog\Functional; - -use Drupal\Tests\BrowserTestBase; - -class BlogPageTest extends BrowserTestBase { - - protected static $modules = ['tdd_blog']; - - public function testBlogPageExists() { - $this->drupalGet('/blog'); - - $this->assertSession()->statusCodeEquals(200); - } - -} -``` - ---- - -```php, [.highlight: 5-7] -// tests/src/Functional/BlogPageTest.php - -namespace Drupal\Tests\tdd_blog\Functional; - -use Drupal\Tests\BrowserTestBase; - -class BlogPageTest extends BrowserTestBase { - - protected static $modules = ['tdd_blog']; - - public function testBlogPageExists() { - $this->drupalGet('/blog'); - - $this->assertSession()->statusCodeEquals(200); - } - -} -``` - ---- - -```php, [.highlight: 9] -// tests/src/Functional/BlogPageTest.php - -namespace Drupal\Tests\tdd_blog\Functional; - -use Drupal\Tests\BrowserTestBase; - -class BlogPageTest extends BrowserTestBase { - - protected static $modules = ['tdd_blog']; - - public function testBlogPageExists() { - $this->drupalGet('/blog'); - - $this->assertSession()->statusCodeEquals(200); - } - -} -``` - ---- - -```php, [.highlight: 11-15] -// tests/src/Functional/BlogPageTest.php - -namespace Drupal\Tests\tdd_blog\Functional; - -use Drupal\Tests\BrowserTestBase; - -class BlogPageTest extends BrowserTestBase { - - protected static $modules = ['tdd_blog']; - - public function testBlogPageExists() { - $this->drupalGet('/blog'); - - $this->assertSession()->statusCodeEquals(200); - } - -} -``` - ---- - -```bash, [.highlight: 1] -docker@cli:/var/www/web$ ../vendor/bin/phpunit -c core modules/custom/tdd_blog -PHPUnit 6.5.8 by Sebastian Bergmann and contributors. - -Testing modules/custom/tdd_blog -E 1 / 1 (100%) - -Time: 19.31 seconds, Memory: 6.00MB - -There was 1 error: - -1) Drupal\Tests\tdd_blog\Functional\BlogPageTest::testBlogPageExists -Behat\Mink\Exception\ExpectationException: Current response status code is 404, but 200 expected. - -/var/www/vendor/behat/mink/src/WebAssert.php:768 -/var/www/vendor/behat/mink/src/WebAssert.php:130 -/var/www/web/modules/custom/tdd_blog/tests/src/Functional/BlogPageTest.php:13 - -ERRORS! -Tests: 1, Assertions: 3, Errors: 1. -``` - ---- - -```bash, [.highlight: 4] -docker@cli:/var/www/web$ ../vendor/bin/phpunit -c core modules/custom/tdd_blog -PHPUnit 6.5.8 by Sebastian Bergmann and contributors. - -Testing modules/custom/tdd_blog -E 1 / 1 (100%) - -Time: 19.31 seconds, Memory: 6.00MB - -There was 1 error: - -1) Drupal\Tests\tdd_blog\Functional\BlogPageTest::testBlogPageExists -Behat\Mink\Exception\ExpectationException: Current response status code is 404, but 200 expected. - -/var/www/vendor/behat/mink/src/WebAssert.php:768 -/var/www/vendor/behat/mink/src/WebAssert.php:130 -/var/www/web/modules/custom/tdd_blog/tests/src/Functional/BlogPageTest.php:13 - -ERRORS! -Tests: 1, Assertions: 3, Errors: 1. -``` - ---- - -```bash, [.highlight: 5-13] -docker@cli:/var/www/web$ ../vendor/bin/phpunit -c core modules/custom/tdd_blog -PHPUnit 6.5.8 by Sebastian Bergmann and contributors. - -Testing modules/custom/tdd_blog -E 1 / 1 (100%) - -Time: 19.31 seconds, Memory: 6.00MB - -There was 1 error: - -1) Drupal\Tests\tdd_blog\Functional\BlogPageTest::testBlogPageExists -Behat\Mink\Exception\ExpectationException: Current response status code is 404, but 200 expected. - -/var/www/vendor/behat/mink/src/WebAssert.php:768 -/var/www/vendor/behat/mink/src/WebAssert.php:130 -/var/www/web/modules/custom/tdd_blog/tests/src/Functional/BlogPageTest.php:13 - -ERRORS! -Tests: 1, Assertions: 3, Errors: 1. -``` - ---- - -```bash, [.highlight: 14-16] -docker@cli:/var/www/web$ ../vendor/bin/phpunit -c core modules/custom/tdd_blog -PHPUnit 6.5.8 by Sebastian Bergmann and contributors. - -Testing modules/custom/tdd_blog -E 1 / 1 (100%) - -Time: 19.31 seconds, Memory: 6.00MB - -There was 1 error: - -1) Drupal\Tests\tdd_blog\Functional\BlogPageTest::testBlogPageExists -Behat\Mink\Exception\ExpectationException: Current response status code is 404, but 200 expected. - -/var/www/vendor/behat/mink/src/WebAssert.php:768 -/var/www/vendor/behat/mink/src/WebAssert.php:130 -/var/www/web/modules/custom/tdd_blog/tests/src/Functional/BlogPageTest.php:13 - -ERRORS! -Tests: 1, Assertions: 3, Errors: 1. -``` - ---- - -```bash, [.highlight: 18-19] -docker@cli:/var/www/web$ ../vendor/bin/phpunit -c core modules/custom/tdd_blog -PHPUnit 6.5.8 by Sebastian Bergmann and contributors. - -Testing modules/custom/tdd_blog -E 1 / 1 (100%) - -Time: 19.31 seconds, Memory: 6.00MB - -There was 1 error: - -1) Drupal\Tests\tdd_blog\Functional\BlogPageTest::testBlogPageExists -Behat\Mink\Exception\ExpectationException: Current response status code is 404, but 200 expected. - -/var/www/vendor/behat/mink/src/WebAssert.php:768 -/var/www/vendor/behat/mink/src/WebAssert.php:130 -/var/www/web/modules/custom/tdd_blog/tests/src/Functional/BlogPageTest.php:13 - -ERRORS! -Tests: 1, Assertions: 3, Errors: 1. -``` - ---- - -- _The view has not been created_ -- Create a new view, page display -- Set the path -- Export the config -- Copy it into the module's `config/install` directory - ---- - -![fit](images/tdd-blog-1.png) - ---- - -![fit](images/tdd-blog-2.png) - ---- - -``` -drush cex -y - -cp ../config/default/views.view.blog.yml \ - modules/custom/tdd_blog/config/install -``` - ---- - -```diff -# views.view.blog.yml - -- uuid: 84305edf-7aef-4109-bc93-e87f685fb678 -langcode: en -status: true -dependencies: - config: - - node.type.article - module: - - node - - user -- _core: -- default_config_hash: iGZkqLWpwWNORq6_fy6v_Kn_KE4BjYHqj9vpgQsWJCs -id: blog -... -``` - ---- - -```[.highlight: 11-13] -docker@cli:/var/www/web$ ../vendor/bin/phpunit -c core modules/custom/tdd_blog -PHPUnit 6.5.8 by Sebastian Bergmann and contributors. - -Testing modules/custom/tdd_blog -E 1 / 1 (100%) - -Time: 16.02 seconds, Memory: 6.00MB - -There was 1 error: - -1) Drupal\Tests\tdd_blog\Functional\BlogPageTest::testBlogPageExists -Drupal\Core\Config\UnmetDependenciesException: Configuration objects provided by tdd_blog -have unmet dependencies: views.view.blog (node.type.article, node, views) - -/var/www/web/core/lib/Drupal/Core/Config/UnmetDependenciesException.php:98 -/var/www/web/core/lib/Drupal/Core/Config/ConfigInstaller.php:469 -/var/www/web/core/lib/Drupal/Core/ProxyClass/Config/ConfigInstaller.php:132 -/var/www/web/core/lib/Drupal/Core/Extension/ModuleInstaller.php:145 -/var/www/web/core/lib/Drupal/Core/ProxyClass/Extension/ModuleInstaller.php:83 -/var/www/web/core/lib/Drupal/Core/Test/FunctionalTestSetupTrait.php:437 -/var/www/web/core/tests/Drupal/Tests/BrowserTestBase.php:1055 -/var/www/web/core/tests/Drupal/Tests/BrowserTestBase.php:490 - -ERRORS! -Tests: 1, Assertions: 0, Errors: 1. -``` - ---- - -```yml,[.highlight: 1, 7-10] -# tdd_blog.info.yml - -name: 'TDD Blog' -description: 'A demo module to show test driven module development.' -core: 8.x -type: module - -dependencies: - - 'drupal:node' - - 'drupal:views' -``` - ---- - -```[.highlight: 10-13] -docker@cli:/var/www/web$ ../vendor/bin/phpunit -c core modules/custom/tdd_blog -PHPUnit 6.5.8 by Sebastian Bergmann and contributors. - -Testing modules/custom/tdd_blog -E 1 / 1 (100%) - -Time: 20 seconds, Memory: 6.00MB - -There was 1 error: - -1) Drupal\Tests\tdd_blog\Functional\BlogPageTest::testBlogPageExists -Drupal\Core\Config\UnmetDependenciesException: Configuration objects provided by tdd_blog -have unmet dependencies: views.view.blog (node.type.article) - -/var/www/web/core/lib/Drupal/Core/Config/UnmetDependenciesException.php:98 -/var/www/web/core/lib/Drupal/Core/Config/ConfigInstaller.php:469 -/var/www/web/core/lib/Drupal/Core/ProxyClass/Config/ConfigInstaller.php:132 -/var/www/web/core/lib/Drupal/Core/Extension/ModuleInstaller.php:145 -/var/www/web/core/lib/Drupal/Core/ProxyClass/Extension/ModuleInstaller.php:83 -/var/www/web/core/lib/Drupal/Core/Test/FunctionalTestSetupTrait.php:437 -/var/www/web/core/tests/Drupal/Tests/BrowserTestBase.php:1055 -/var/www/web/core/tests/Drupal/Tests/BrowserTestBase.php:490 - -ERRORS! -Tests: 1, Assertions: 0, Errors: 1. -``` - ---- - -- Add the article content type - ---- - -```[.highlight: 5, 9] -docker@cli:/var/www/web$ ../vendor/bin/phpunit -c core modules/custom/tdd_blog -PHPUnit 6.5.8 by Sebastian Bergmann and contributors. - -Testing modules/custom/tdd_blog -. 1 / 1 (100%) - -Time: 23.36 seconds, Memory: 6.00MB - -OK (1 test, 3 assertions) -``` - ---- - -[.build-lists: false] - -## _Tasks_ - -- ~~Ensure the blog page exists~~ -- Ensure only published articles are shown -- Ensure the articles are shown in the correct order - ---- - -### _Step 3_ - -## Ensure only published articles are shown - ---- - -```php -public function testOnlyPublishedArticlesAreShown() { - // Given I have a mixture of published and unpublished articles, - // as well as other types of content. - - // When I view the blog page. - - // I should only see the published articles. -} -``` - ---- - -### _Option 1_ - -## Functional tests - ---- - -```php,[.highlight: 1, 4-8] -// modules/custom/tdd_blog/tests/src/Functional/BlogPageTest.php - -public function testOnlyPublishedArticlesAreShown() { - // Given I have a mixture of published and unpublished articles, - // as well as other types of content. - $node1 = $this->drupalCreateNode(['type' => 'page', 'status' => 1]); - $node2 = $this->drupalCreateNode(['type' => 'article', 'status' => 1]); - $node3 = $this->drupalCreateNode(['type' => 'article', 'status' => 0]); - - // When I view the blog page. - $this->drupalGet('/blog'); - - // I should only see the published articles. - $assert = $this->assertSession(); - $assert->pageTextContains($node2->label()); - $assert->pageTextNotContains($node1->label()); - $assert->pageTextNotContains($node3->label()); -} -``` - -^ Different ways to achieve this. This is taking the functional test approach. - ---- - -```php,[.highlight: 10-12] -// modules/custom/tdd_blog/tests/src/Functional/BlogPageTest.php - -public function testOnlyPublishedArticlesAreShown() { - // Given I have a mixture of published and unpublished articles, - // as well as other types of content. - $node1 = $this->drupalCreateNode(['type' => 'page', 'status' => 1]); - $node2 = $this->drupalCreateNode(['type' => 'article', 'status' => 1]); - $node3 = $this->drupalCreateNode(['type' => 'article', 'status' => 0]); - - // When I view the blog page. - $this->drupalGet('/blog'); - - // I should only see the published articles. - $assert = $this->assertSession(); - $assert->pageTextContains($node2->label()); - $assert->pageTextNotContains($node1->label()); - $assert->pageTextNotContains($node3->label()); -} -``` - ---- - -```php, [.highlight: 13-17] -// modules/custom/tdd_blog/tests/src/Functional/BlogPageTest.php - -public function testOnlyPublishedArticlesAreShown() { - // Given I have a mixture of published and unpublished articles, - // as well as other types of content. - $node1 = $this->drupalCreateNode(['type' => 'page', 'status' => 1]); - $node2 = $this->drupalCreateNode(['type' => 'article', 'status' => 1]); - $node3 = $this->drupalCreateNode(['type' => 'article', 'status' => 0]); - - // When I view the blog page. - $this->drupalGet('/blog'); - - // I should only see the published articles. - $assert = $this->assertSession(); - $assert->pageTextContains($node2->label()); - $assert->pageTextNotContains($node1->label()); - $assert->pageTextNotContains($node3->label()); -} -``` - ---- - -### _Option 2_ - -## Kernel tests - ---- - -```php -namespace Drupal\Tests\tdd_blog\Kernel; - -use Drupal\KernelTests\Core\Entity\EntityKernelTestBase; -use Drupal\Tests\node\Traits\NodeCreationTrait; - -class BlogPageTest extends EntityKernelTestBase { - - use NodeCreationTrait; - - public static $modules = ['node']; - - public function testOnlyPublishedArticlesAreShown() { - $this->createNode(['type' => 'page', 'status' => 1]); - $this->createNode(['type' => 'article', 'status' => 1]); - $this->createNode(['type' => 'article', 'status' => 0]); - } - -} -``` - -^ Kernel test approach Dropping down a level No need for the brower, not -asserting against HTML Faster to run - ---- - -```php, [.highlight: 1-6] -namespace Drupal\Tests\tdd_blog\Kernel; - -use Drupal\KernelTests\Core\Entity\EntityKernelTestBase; -use Drupal\Tests\node\Traits\NodeCreationTrait; - -class BlogPageTest extends EntityKernelTestBase { - - use NodeCreationTrait; - - public static $modules = ['node']; - - public function testOnlyPublishedArticlesAreShown() { - $this->createNode(['type' => 'page', 'status' => 1]); - $this->createNode(['type' => 'article', 'status' => 1]); - $this->createNode(['type' => 'article', 'status' => 0]); - } - -} -``` - ---- - -```php, [.highlight: 8] -namespace Drupal\Tests\tdd_blog\Kernel; - -use Drupal\KernelTests\Core\Entity\EntityKernelTestBase; -use Drupal\Tests\node\Traits\NodeCreationTrait; - -class BlogPageTest extends EntityKernelTestBase { - - use NodeCreationTrait; - - public static $modules = ['node']; - - public function testOnlyPublishedArticlesAreShown() { - $this->createNode(['type' => 'page', 'status' => 1]); - $this->createNode(['type' => 'article', 'status' => 1]); - $this->createNode(['type' => 'article', 'status' => 0]); - } - -} -``` - ---- - -```php, [.highlight: 12-16] -namespace Drupal\Tests\tdd_blog\Kernel; - -use Drupal\KernelTests\Core\Entity\EntityKernelTestBase; -use Drupal\Tests\node\Traits\NodeCreationTrait; - -class BlogPageTest extends EntityKernelTestBase { - - use NodeCreationTrait; - - public static $modules = ['node']; - - public function testOnlyPublishedArticlesAreShown() { - $this->createNode(['type' => 'page', 'status' => 1]); - $this->createNode(['type' => 'article', 'status' => 1]); - $this->createNode(['type' => 'article', 'status' => 0]); - } - -} -``` - ---- - -```[.highlight: 9-16] -docker@cli:/var/www/web$ ../vendor/bin/phpunit -c core modules/custom/tdd_blog/tests/src/Kernel/ -PHPUnit 6.5.8 by Sebastian Bergmann and contributors. - -Testing modules/custom/tdd_blog/tests/src/Kernel/ -E 1 / 1 (100%) - -Time: 6.22 seconds, Memory: 6.00MB - -There was 1 error: - -1) Drupal\Tests\tdd_blog\Kernel\BlogPageTest::testOnlyPublishedArticlesAreShown -Error: Call to a member function id() on boolean - -/var/www/web/core/modules/filter/filter.module:212 -/var/www/web/core/modules/node/tests/src/Traits/NodeCreationTrait.php:73 -/var/www/web/modules/custom/tdd_blog/tests/src/Kernel/BlogPageTest.php:13 - -ERRORS! -Tests: 1, Assertions: 2, Errors: 1. -``` - ---- - -```php, [.highlight: 2] -public function testOnlyPublishedArticlesAreShown() { - $this->installConfig(['filter']); - - $this->createNode(['type' => 'page', 'status' => 1]); - $this->createNode(['type' => 'article', 'status' => 1]); - $this->createNode(['type' => 'article', 'status' => 0]); -} -``` - ---- - -```php, [.highlight: 8] -public function testOnlyPublishedArticlesAreShown() { - $this->installConfig(['filter']); - - $this->createNode(['type' => 'page', 'status' => 1]); - $this->createNode(['type' => 'article', 'status' => 1]); - $this->createNode(['type' => 'article', 'status' => 0]); - - $results = views_get_view_result('blog'); -} -``` - ---- - -```php, [.highlight: 3] -... - -public static $modules = ['node', 'tdd_blog', 'views']; - -public function testOnlyPublishedArticlesAreShown() { - $this->installConfig(['filter', 'tdd_blog']); - - $this->createNode(['type' => 'page', 'status' => 1]); - $this->createNode(['type' => 'article', 'status' => 1]); - $this->createNode(['type' => 'article', 'status' => 0]); - - $results = views_get_view_result('blog'); - - $this->assertCount(1, $results); - $this->assertEquals(2, $results[0]->_entity->id()); -} -``` - ---- - -```php, [.highlight: 6] -... - -public static $modules = ['node', 'tdd_blog', 'views']; - -public function testOnlyPublishedArticlesAreShown() { - $this->installConfig(['filter', 'tdd_blog']); - - $this->createNode(['type' => 'page', 'status' => 1]); - $this->createNode(['type' => 'article', 'status' => 1]); - $this->createNode(['type' => 'article', 'status' => 0]); - - $results = views_get_view_result('blog'); - - $this->assertCount(1, $results); - $this->assertEquals(2, $results[0]->_entity->id()); -} -``` - ---- - -```php, [.highlight: 8-15] -... - -public static $modules = ['node', 'tdd_blog', 'views']; - -public function testOnlyPublishedArticlesAreShown() { - $this->installConfig(['filter', 'tdd_blog']); - - $this->createNode(['type' => 'page', 'status' => 1]); - $this->createNode(['type' => 'article', 'status' => 1]); - $this->createNode(['type' => 'article', 'status' => 0]); - - $results = views_get_view_result('blog'); - - $this->assertCount(1, $results); - $this->assertEquals(2, $results[0]->_entity->id()); -} -``` - -^ Assert Should only be one result, should be node 2 Node IDs are reset on each -test method - ---- - -``` -PHPUnit 6.5.8 by Sebastian Bergmann and contributors. - -Testing web/modules/custom/tdd_blog/tests/src/Kernel -F 1 / 1 (100%) - -Time: 2.16 seconds, Memory: 6.00MB - -There was 1 failure: - -1) Drupal\Tests\tdd_blog\Kernel\BlogPageTest::testOnlyPublishedArticlesAreShown -Failed asserting that actual size 2 matches expected size 1. - -/Users/opdavies/Code/drupal-testing-workshop/web/modules/custom/tdd_blog/tests/src/Kernel/BlogPageTest.php:23 - -FAILURES! -Tests: 1, Assertions: 4, Failures: 1. -``` - ---- - -```[.highlight: 8-13] -PHPUnit 6.5.8 by Sebastian Bergmann and contributors. - -Testing web/modules/custom/tdd_blog/tests/src/Kernel -F 1 / 1 (100%) - -Time: 2.16 seconds, Memory: 6.00MB - -There was 1 failure: - -1) Drupal\Tests\tdd_blog\Kernel\BlogPageTest::testOnlyPublishedArticlesAreShown -Failed asserting that actual size 2 matches expected size 1. - -/Users/opdavies/Code/drupal-testing-workshop/web/modules/custom/tdd_blog/tests/src/Kernel/BlogPageTest.php:23 - -FAILURES! -Tests: 1, Assertions: 4, Failures: 1. -``` - ---- - -![inline](images/tdd-blog-3.png) - ---- - -> - _There is no content type filter on the view_ - -- Add the filter -- Re-export and save the view - ---- - -![inline](images/tdd-blog-4.png) - ---- - -```[.highlight: 3-8] -PHPUnit 6.5.8 by Sebastian Bergmann and contributors. - -Testing web/modules/custom/tdd_blog/tests/src/Kernel -. 1 / 1 (100%) - -Time: 2.02 seconds, Memory: 6.00MB - -OK (1 test, 6 assertions) -``` - ---- - -[.build-lists: false] - -## _Tasks_ - -- ~~Ensure the blog page exists~~ -- ~~Ensure only published articles are shown~~ -- Ensure the articles are shown in the correct order - ---- - -### _Step 4_ - -## Ensure the articles are ordered by date - ---- - -```php -// modules/custom/tdd_blog/tests/src/Kernel/BlogPageTest.php - -public function testArticlesAreOrderedByDate() { - // Given that I have numerous articles with different post dates. - - // When I go to the blog page. - - // The articles are ordered by post date. -} -``` - ---- - -```php, [.highlight: 4-9] -// modules/custom/tdd_blog/tests/src/Kernel/BlogPageTest.php - -public function testArticlesAreOrderedByDate() { - // Given that I have numerous articles with different post dates. - $this->createNode(['type' => 'article', 'created' => (new \DateTime('+1 day'))->getTimestamp()]); - $this->createNode(['type' => 'article', 'created' => (new \DateTime('+1 month'))->getTimestamp()]); - $this->createNode(['type' => 'article', 'created' => (new \DateTime('+3 days'))->getTimestamp()]); - $this->createNode(['type' => 'article', 'created' => (new \DateTime('+1 hour'))->getTimestamp()]); - - // When I go to the blog page. - - // The articles are ordered by post date. -} -``` - ---- - -```php -$this->createNode([ - 'type' => 'article', - 'created' => (new \DateTime())->modify('+1 day')->getTimestamp(), -]); -``` - -^ Array of default values - ---- - -```php, [.highlight: 10-11] -// modules/custom/tdd_blog/tests/src/Kernel/BlogPageTest.php - -public function testArticlesAreOrderedByDate() { - // Given that I have numerous articles with different post dates. - $this->createNode(['type' => 'article', 'created' => (new \DateTime())->modify('+1 day')->getTimestamp()]); - $this->createNode(['type' => 'article', 'created' => (new \DateTime())->modify('+1 month')->getTimestamp()]); - $this->createNode(['type' => 'article', 'created' => (new \DateTime())->modify('+3 days')->getTimestamp()]); - $this->createNode(['type' => 'article', 'created' => (new \DateTime())->modify('+1 hour')->getTimestamp()]); - - // When I go to the blog page. - $results = views_get_view_result('blog'); - - // The articles are ordered by post date. -} -``` - ---- - -```php, [.highlight:10-15] -// modules/custom/tdd_blog/tests/src/Kernel/BlogPageTest.php - -public function testArticlesAreOrderedByDate() { - // Given that I have numerous articles with different post dates. - $this->createNode(['type' => 'article', 'created' => (new \DateTime())->modify('+1 day')->getTimestamp()]); - $this->createNode(['type' => 'article', 'created' => (new \DateTime())->modify('+1 month')->getTimestamp()]); - $this->createNode(['type' => 'article', 'created' => (new \DateTime())->modify('+3 days')->getTimestamp()]); - $this->createNode(['type' => 'article', 'created' => (new \DateTime())->modify('+1 hour')->getTimestamp()]); - - // When I go to the blog page. - $results = views_get_view_result('blog'); - - $nids = array_map(function(ResultRow $result) { - return $result->_entity->id(); - }, $results); - - // The articles are ordered by post date. -} -``` - ---- - -```php, [.highlight: 5-9, 17-18] -// modules/custom/tdd_blog/tests/src/Kernel/BlogPageTest.php - -public function testArticlesAreOrderedByDate() { - // Given that I have numerous articles with different post dates. - $this->createNode(['type' => 'article', 'created' => (new \DateTime())->modify('+1 day')->getTimestamp()]); - $this->createNode(['type' => 'article', 'created' => (new \DateTime())->modify('+1 month')->getTimestamp()]); - $this->createNode(['type' => 'article', 'created' => (new \DateTime())->modify('+3 days')->getTimestamp()]); - $this->createNode(['type' => 'article', 'created' => (new \DateTime())->modify('+1 hour')->getTimestamp()]); - - // When I go to the blog page. - $results = views_get_view_result('blog'); - - $nids = array_map(function(ResultRow $result) { - return $result->_entity->id(); - }, $results); - - // The articles are ordered by post date. - $this->assertEquals([4, 1, 3, 2], $nids); -} -``` - ---- - -``` -PHPUnit 6.5.8 by Sebastian Bergmann and contributors. - -Testing web/modules/custom/tdd_blog/tests/src/Kernel -F 1 / 1 (100%) - -Time: 1.42 seconds, Memory: 6.00MB - -There was 1 failure: - -1) Drupal\Tests\tdd_blog\Kernel\BlogPageTest::testArticlesAreOrderedByDate -Failed asserting that two arrays are equal. ---- Expected -+++ Actual -@@ @@ - Array ( -- 0 => 4 -- 1 => 1 -- 2 => 3 -- 3 => 2 -+ 0 => '1' -+ 1 => '2' -+ 2 => '3' -+ 3 => '4' - -/Users/opdavies/Code/drupal-testing-workshop/web/core/tests/Drupal/KernelTests/KernelTestBase.php:1114 -/Users/opdavies/Code/drupal-testing-workshop/web/modules/custom/tdd_blog/tests/src/Kernel/BlogPageTest.php:43 - -FAILURES! -Tests: 1, Assertions: 4, Failures: 1. -``` - ---- - -[.text: comic sans] - -```[.highlight: 8-26] -PHPUnit 6.5.8 by Sebastian Bergmann and contributors. - -Testing web/modules/custom/tdd_blog/tests/src/Kernel -F 1 / 1 (100%) - -Time: 1.42 seconds, Memory: 6.00MB - -There was 1 failure: - -1) Drupal\Tests\tdd_blog\Kernel\BlogPageTest::testArticlesAreOrderedByDate -Failed asserting that two arrays are equal. ---- Expected -+++ Actual -@@ @@ - Array ( -- 0 => 4 -- 1 => 1 -- 2 => 3 -- 3 => 2 -+ 0 => '1' -+ 1 => '2' -+ 2 => '3' -+ 3 => '4' - -/Users/opdavies/Code/drupal-testing-workshop/web/core/tests/Drupal/KernelTests/KernelTestBase.php:1114 -/Users/opdavies/Code/drupal-testing-workshop/web/modules/custom/tdd_blog/tests/src/Kernel/BlogPageTest.php:43 - -FAILURES! -Tests: 1, Assertions: 4, Failures: 1. -``` - ---- - -![inline](images/tdd-blog-4.png) - ---- - -- _There is no sort order defined on the view_ -- Add the sort order -- Re-export the view - ---- - -![inline](images/tdd-blog-5.png) - ---- - -```[.highlight:3-8] -PHPUnit 6.5.8 by Sebastian Bergmann and contributors. - -Testing web/modules/custom/tdd_blog/tests/src/Kernel -. 1 / 1 (100%) - -Time: 1.74 seconds, Memory: 6.00MB - -OK (1 test, 5 assertions) -``` - ---- - -[.build-lists: false] - -## _Tasks_ - -- ~~Ensure the blog page exists~~ -- ~~Ensure only published articles are shown~~ -- ~~Ensure the articles are shown in the correct order~~ - ---- - -![inline fit](images/tdd-blog-directories.png) - ---- - -![fit](images/tdd-blog-installed.png) - -^ Using the minimal installation profile Post 3 is unpublished - ---- - -![inline 75%](images/tada.png) - ---- - -[.header: alignment(center)] - -## Take Aways - ---- - -- Testing has made me a _better developer_ -- Testing can produce _better quality code_ -- Use the _right type of test_ for the right situation -- Use the _right base class_, use available _traits_ -- Writing tests is an _investment_ -- OK to _start small_, introduce tests gradually -- Easier to _refactor_ -- Tests can pass, but things can _still be broken_. Tests only report on what - they cover. - -^ Made me think about how I'm going to do something more starting to do it Less -cruft, only write code that serves a purpose Spending time writing tests pays -dividends later on Start by introducing tests for new features or regression -tests when fixing bugs If you know things pass, then you can refactor code -knowing if something is broken Manual testing is still important - ---- - -[.header: alignment(center)] - -## [fit] _Having tests does not mean_ - -## [fit] there will be no bugs - -^ Only means that the tests you wrote are passing You may not have included a -certain use case Be sure to test in the UI! We can test what happens in a test -when a user has a permission, but in our site we still need to assign the -permission to a role and the role to a user. - ---- - -### _You might be testing the wrong thing_ - -## Maybe it doesn't work the way you think it does - ---- - -### _Have you written enough assertions?_ - -## Have you only covered the 'happy path' scenarios? - -^ If your tests are passing but there is an issue, maybe you haven't written -enough assertions Be sure to check for the negative use cases too Check that -something is not included as well as what should be included What if you pass in -an incorrect value? - ---- - -### _Other modules can affect things_ - -## Tests may pass, but fail when other modules are enabled - ---- - -[.header: alignment(center)] - -## [fit] _Testing may add time now_ - -## [fit] but save more time in the future - ---- - -[.header: alignment(center)] - -## [fit] _How do you get quicker at writing tests?_ - -# [fit] By writing more tests - -^ Practice makes perfect Become more familar with and knowledge of recurring -errors Find better practices and approaches. Different base classes? Less setup -steps. Less time, more productive. - ---- - -## _Start small_ - -## Some tests are better than no tests - ---- - [.background-color: #FFFFFF] ![140%](images/tawny-tweet-1.png) @@ -2335,22 +943,19 @@ steps. Less time, more productive. --- -[.text: alignment(center)] +[.header: alignment(center)] -> ![inline 150%](images/when-you-do-things-right.jpg) +## **TestDrivenDrupal.com** + +--- + +![100%](../images/drupal-association-logo.png) --- [.header: alignment(center)] -# Questions? +# **Questions?** ---- - -[.header: alignment(center)] - -# Thanks - -### _@opdavies_ - -### _oliverdavies.uk_ +### @opdavies +### oliverdavies.uk