Slide 1

Slide 1 text

TDD: Test Driven Drupal Oliver Davies (@opdavies) https://opdavi.es/dcg

Slide 2

Slide 2 text

Software Developer, Consultant, open-source maintainer @opdavies

Slide 3

Slide 3 text

@opdavies

Slide 4

Slide 4 text

@opdavies

Slide 5

Slide 5 text

@opdavies

Slide 6

Slide 6 text

Why write tests? • Peace of mind • Prevent regressions • Catch bugs earlier • Write less code • Documentation • Drupal core requirement • More important with regular D10/D11 releases and supporting multiple versions @opdavies

Slide 7

Slide 7 text

Testing in Drupal • 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 from core, moved back to contrib @opdavies

Slide 8

Slide 8 text

Writing PHPUnit Tests for Drupal • 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 @opdavies

Slide 9

Slide 9 text

Arrange, Act, Assert @opdavies

Slide 10

Slide 10 text

Given, When, Then @opdavies

Slide 11

Slide 11 text

What to test? • Creating nodes with data from an API • Calculating attendance figures for an event • Determining if an event is purchasable • Promotions and coupons for new users • Cloning events • Queuing private message requests • Re-opening closed support tickets when comments are added @opdavies

Slide 12

Slide 12 text

@opdavies

Slide 13

Slide 13 text

What does a test look like? 1 // web/modules/custom/example/tests/src/Functional. 2 3 namespace Drupal\Tests\example\Functional; 4 5 use Drupal\Tests\BrowserTestBase; 6 7 class ExampleTest extends BrowserTestBase { 8 9 public function testSomething() { 10 $this->assertTrue(FALSE); 11 } 12 13 } @opdavies

Slide 14

Slide 14 text

What does a test look like? 1 // web/modules/custom/example/tests/src/Functional. 2 3 namespace Drupal\Tests\example\Functional; 4 5 use Drupal\Tests\BrowserTestBase; 6 7 class ExampleTest extends BrowserTestBase { 8 9 public function testSomething() { 10 $this->assertTrue(FALSE); 11 } 12 13 } @opdavies

Slide 15

Slide 15 text

What does a test look like? 1 // web/modules/custom/example/tests/src/Functional. 2 3 namespace Drupal\Tests\example\Functional; 4 5 use Drupal\Tests\BrowserTestBase; 6 7 class ExampleTest extends BrowserTestBase { 8 9 public function testSomething() { 10 $this->assertTrue(FALSE); 11 } 12 13 } @opdavies

Slide 16

Slide 16 text

What does a test look like? 1 // web/modules/custom/example/tests/src/Functional. 2 3 namespace Drupal\Tests\example\Functional; 4 5 use Drupal\Tests\BrowserTestBase; 6 7 class ExampleTest extends BrowserTestBase { 8 9 public function testSomething() { 10 $this->assertTrue(FALSE); 11 } 12 13 } @opdavies

Slide 17

Slide 17 text

What does a test look like? 1 // web/modules/custom/example/tests/src/Functional. 2 3 namespace Drupal\Tests\example\Functional; 4 5 use Drupal\Tests\BrowserTestBase; 6 7 class ExampleTest extends BrowserTestBase { 8 9 public function testSomething() { 10 $this->assertTrue(FALSE); 11 } 12 13 } @opdavies

Slide 18

Slide 18 text

Writing test methods 1 public function testSomething() 2 3 public function test_something() 4 5 /** @test */ 6 public function it_does_something() @opdavies

Slide 19

Slide 19 text

Writing test methods 1 public function testSomething() 2 3 public function test_something() 4 5 /** @test */ 6 public function it_does_something() @opdavies

Slide 20

Slide 20 text

Writing test methods 1 public function testSomething() 2 3 public function test_something() 4 5 /** @test */ 6 public function it_does_something() @opdavies

Slide 21

Slide 21 text

Writing test methods 1 public function testSomething() 2 3 public function test_something() 4 5 /** @test */ 6 public function it_does_something() @opdavies

Slide 22

Slide 22 text

Types of Tests • Functional/FunctionalJavascript (web, browser, feature) • Kernel (integration) • Unit @opdavies

Slide 23

Slide 23 text

Functional Tests • Tests end-to-end functionality • UI testing • Interacts with database • Full Drupal installation • Slower to run • With/without JavaScript @opdavies

Slide 24

Slide 24 text

Kernel tests • Integration tests • Can install modules, interact with services, container, database • Minimal Drupal bootstrap • Faster than functional tests • More setup required @opdavies

Slide 25

Slide 25 text

Unit Tests • Tests PHP logic • No database interaction • Fast to run • Need to mock dependencies • Can become tightly coupled • Can be hard to refactor @opdavies

Slide 26

Slide 26 text

Running Tests @opdavies

Slide 27

Slide 27 text

Core script $ php web/core/scripts/run-tests.sh $ php web/core/scripts/run-tests.sh \ --all $ php web/core/scripts/run-tests.sh \ --module example $ php web/core/scripts/run-tests.sh \ --class ExampleTest @opdavies

Slide 28

Slide 28 text

Core script $ php web/core/scripts/run-tests.sh \ --module example \ --sqlite /dev/shm/test.sqlite \ --url http://web @opdavies

Slide 29

Slide 29 text

Drupal test run --------------- Tests to be run: - Drupal\Tests\example\Functional\ExamplePageTest Test run started: Saturday, October 14, 2023 - 10:28 Test summary ------------ Drupal\Tests\example\Functional\ExamplePageTest 1 passes Test run duration: 7 sec @opdavies

Slide 30

Slide 30 text

PHPUnit $ export SIMPLETEST_BASE_URL=http://web $ web/vendor/bin/phpunit \ -c web/core \ modules/contrib/examples/modules/phpunit_example @opdavies

Slide 31

Slide 31 text

PHPUnit 9.6.13 by Sebastian Bergmann and contributors. Testing /app/web/modules/contrib/examples/modules/phpunit_example ................................. 33 / 33 (100%) Time: 00:08.660, Memory: 10.00 MB OK (33 tests, 43 assertions) @opdavies

Slide 32

Slide 32 text

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 • Add and change as needed • SIMPLETEST_BASE_URL, SIMPLETEST_DB, BROWSERTEST_OUTPUT_DIRECTORY • stopOnFailure="true" @opdavies

Slide 33

Slide 33 text

Example @opdavies

Slide 34

Slide 34 text

@opdavies

Slide 35

Slide 35 text

Specification • Job adverts created in Broadbean UI, create nodes in Drupal. • Application URL links users to separate application system. • Constructed from domain, includes role ID as a GET parameter and optionally UTM parameters. • Jobs need to be linked to offices. • Job length specified in number of days. • Path is specified as a field in the API. @opdavies

Slide 36

Slide 36 text

@opdavies

Slide 37

Slide 37 text

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 @opdavies

Slide 38

Slide 38 text

Incoming data $data = [ 'command' => 'add', 'username' => 'bobsmith', 'password' => 'p455w0rd', 'active_for' => '365', 'details' => 'This is the detailed description.', 'job_title' => 'Healthcare Assistant (HCA)', 'locations' => 'Bath, Devizes', 'role_id' => 'A/52/86', 'summary' => 'This is the short description.', 'url_alias' => 'healthcare-assistant-aldershot-june17', // ... ]; @opdavies

Slide 39

Slide 39 text

Incoming data $data = [ 'command' => 'add', 'username' => 'bobsmith', 'password' => 'p455w0rd', 'active_for' => '365', 'details' => 'This is the detailed description.', 'job_title' => 'Healthcare Assistant (HCA)', 'locations' => 'Bath, Devizes', 'role_id' => 'A/52/86', 'summary' => 'This is the short description.', 'url_alias' => 'healthcare-assistant-aldershot-june17', // ... ]; @opdavies

Slide 40

Slide 40 text

Incoming data $data = [ 'command' => 'add', 'username' => 'bobsmith', 'password' => 'p455w0rd', 'active_for' => '365', 'details' => 'This is the detailed description.', 'job_title' => 'Healthcare Assistant (HCA)', 'locations' => 'Bath, Devizes', 'role_id' => 'A/52/86', 'summary' => 'This is the short description.', 'url_alias' => 'healthcare-assistant-aldershot-june17', // ... ]; @opdavies

Slide 41

Slide 41 text

Incoming data $data = [ 'command' => 'add', 'username' => 'bobsmith', 'password' => 'p455w0rd', 'active_for' => '365', 'details' => 'This is the detailed description.', 'job_title' => 'Healthcare Assistant (HCA)', 'locations' => 'Bath, Devizes', 'role_id' => 'A/52/86', 'summary' => 'This is the short description.', 'url_alias' => 'healthcare-assistant-aldershot-june17', // ... ]; @opdavies

Slide 42

Slide 42 text

Incoming data $data = [ 'command' => 'add', 'username' => 'bobsmith', 'password' => 'p455w0rd', 'active_for' => '365', 'details' => 'This is the detailed description.', 'job_title' => 'Healthcare Assistant (HCA)', 'locations' => 'Bath, Devizes', 'role_id' => 'A/52/86', 'summary' => 'This is the short description.', 'url_alias' => 'healthcare-assistant-aldershot-june17', // ... ]; @opdavies

Slide 43

Slide 43 text

Incoming data $data = [ 'command' => 'add', 'username' => 'bobsmith', 'password' => 'p455w0rd', 'active_for' => '365', 'details' => 'This is the detailed description.', 'job_title' => 'Healthcare Assistant (HCA)', 'locations' => 'Bath, Devizes', 'role_id' => 'A/52/86', 'summary' => 'This is the short description.', 'url_alias' => 'healthcare-assistant-aldershot-june17', // ... ]; @opdavies

Slide 44

Slide 44 text

Implementation • If no error, create the job node, return OK response to Broadbean • If an Exception is thrown, return an error code and message @opdavies

Slide 45

Slide 45 text

Types of tests • 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) • 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 @opdavies

Slide 46

Slide 46 text

Results • 0 bugs! • Easier to identify where issues occurred and responsibilities • Reduced debugging time @opdavies

Slide 47

Slide 47 text

Test Driven Development • Write a failing test • Write code until the test passes • Refactor • Repeat @opdavies

Slide 48

Slide 48 text

Red, Green, Refactor @opdavies

Slide 49

Slide 49 text

Porting Modules From Drupal 7 • Make a new branch • Add/update the tests • Write code to make the tests pass • Refactor • Repeat @opdavies

Slide 50

Slide 50 text

How I Write Tests - "Outside In" • Start with functional tests • Drop down to integration or unit tests where needed • Programming by wishful thinking • Write comments first, then fill in the code • Sometimes write assertions first @opdavies

Slide 51

Slide 51 text

How I Write Tests - "Outside In" • Functional - 57 tests, 180 assertions • Kernel - 38 tests, 495 assertions • Unit - 5 tests, 18 assertions Run in 2-3 minutes in a CI pipeline with GitHub Actions. @opdavies

Slide 52

Slide 52 text

Building a blog module @opdavies

Slide 53

Slide 53 text

Acceptance criteria • As a site visitor • I want to see a list of published articles at /blog • Ordered by post date, most recent first @opdavies

Slide 54

Slide 54 text

Tasks • Ensure the blog page exists • Ensure only published articles are shown • Ensure the articles are shown in the correct order @opdavies

Slide 55

Slide 55 text

1 // tests/src/Functional/BlogPageTest.php 2 3 namespace Drupal\Tests\drupalcon\Functional; 4 5 use Drupal\Tests\BrowserTestBase; 6 7 final class BlogPageTest extends BrowserTestBase { 8 9 public $defaultTheme = 'stark'; 10 11 public static $modules = []; 12 13 } @opdavies

Slide 56

Slide 56 text

1 // tests/src/Functional/BlogPageTest.php 2 3 namespace Drupal\Tests\drupalcon\Functional; 4 5 use Drupal\Tests\BrowserTestBase; 6 7 final class BlogPageTest extends BrowserTestBase { 8 9 public $defaultTheme = 'stark'; 10 11 public static $modules = []; 12 13 } @opdavies

Slide 57

Slide 57 text

1 // tests/src/Functional/BlogPageTest.php 2 3 namespace Drupal\Tests\drupalcon\Functional; 4 5 use Drupal\Tests\BrowserTestBase; 6 7 final class BlogPageTest extends BrowserTestBase { 8 9 public $defaultTheme = 'stark'; 10 11 public static $modules = []; 12 13 } @opdavies

Slide 58

Slide 58 text

1 // tests/src/Functional/BlogPageTest.php 2 3 namespace Drupal\Tests\drupalcon\Functional; 4 5 use Drupal\Tests\BrowserTestBase; 6 7 final class BlogPageTest extends BrowserTestBase { 8 9 public $defaultTheme = 'stark'; 10 11 public static $modules = []; 12 13 } @opdavies

Slide 59

Slide 59 text

1 // tests/src/Functional/BlogPageTest.php 2 3 /** @test */ 4 public function it_loads_the_blog_page(): void { 5 $this->drupalGet('/blog'); 6 7 $this->assertSession()->statusCodeEquals(200); 8 } 9 @opdavies

Slide 60

Slide 60 text

1 // tests/src/Functional/BlogPageTest.php 2 3 /** @test */ 4 public function it_loads_the_blog_page(): void { 5 $this->drupalGet('/blog'); 6 7 $this->assertSession()->statusCodeEquals(200); 8 } 9 @opdavies

Slide 61

Slide 61 text

1 // tests/src/Functional/BlogPageTest.php 2 3 /** @test */ 4 public function it_loads_the_blog_page(): void { 5 $this->drupalGet('/blog'); 6 7 $this->assertSession()->statusCodeEquals(200); 8 } 9 @opdavies

Slide 62

Slide 62 text

1 // tests/src/Functional/BlogPageTest.php 2 3 /** @test */ 4 public function it_loads_the_blog_page(): void { 5 $this->drupalGet('/blog'); 6 7 $this->assertSession()->statusCodeEquals(200); 8 } 9 @opdavies

Slide 63

Slide 63 text

E 1 / 1 (100%) Time: 00:01.379, Memory: 6.00 MB There was 1 error: 1) Drupal\Tests\drupalcon\Functional\BlogPageTest::it_loads_the_blog_page Behat\Mink\Exception\ExpectationException: Current response status code is 404, but 200 expected. /app/vendor/behat/mink/src/WebAssert.php:794 /app/vendor/behat/mink/src/WebAssert.php:130 /app/web/modules/custom/drupalcon/tests/src/BlogPageTest.php:16 /app/vendor/phpunit/phpunit/src/Framework/TestResult.php:728 ERRORS! Tests: 1, Assertions: 2, Errors: 1. @opdavies

Slide 64

Slide 64 text

E 1 / 1 (100%) Time: 00:01.379, Memory: 6.00 MB There was 1 error: 1) Drupal\Tests\drupalcon\Functional\BlogPageTest::it_loads_the_blog_page Behat\Mink\Exception\ExpectationException: Current response status code is 404, but 200 expected. /app/vendor/behat/mink/src/WebAssert.php:794 /app/vendor/behat/mink/src/WebAssert.php:130 /app/web/modules/custom/drupalcon/tests/src/BlogPageTest.php:16 /app/vendor/phpunit/phpunit/src/Framework/TestResult.php:728 ERRORS! Tests: 1, Assertions: 2, Errors: 1. @opdavies

Slide 65

Slide 65 text

E 1 / 1 (100%) Time: 00:01.379, Memory: 6.00 MB There was 1 error: 1) Drupal\Tests\drupalcon\Functional\BlogPageTest::it_loads_the_blog_page Behat\Mink\Exception\ExpectationException: Current response status code is 404, but 200 expected. /app/vendor/behat/mink/src/WebAssert.php:794 /app/vendor/behat/mink/src/WebAssert.php:130 /app/web/modules/custom/drupalcon/tests/src/BlogPageTest.php:16 /app/vendor/phpunit/phpunit/src/Framework/TestResult.php:728 ERRORS! Tests: 1, Assertions: 2, Errors: 1. @opdavies

Slide 66

Slide 66 text

E 1 / 1 (100%) Time: 00:01.379, Memory: 6.00 MB There was 1 error: 1) Drupal\Tests\drupalcon\Functional\BlogPageTest::it_loads_the_blog_page Behat\Mink\Exception\ExpectationException: Current response status code is 404, but 200 expected. /app/vendor/behat/mink/src/WebAssert.php:794 /app/vendor/behat/mink/src/WebAssert.php:130 /app/web/modules/custom/drupalcon/tests/src/BlogPageTest.php:16 /app/vendor/phpunit/phpunit/src/Framework/TestResult.php:728 ERRORS! Tests: 1, Assertions: 2, Errors: 1. @opdavies

Slide 67

Slide 67 text

1 # drupalcon.routing.yml 2 3 blog.page: 4 path: /blog 5 defaults: 6 _controller: Drupal\drupalcon\Controller\BlogPageController 7 _title: Blog 8 requirements: 9 _permission: access content 10 @opdavies

Slide 68

Slide 68 text

1 # drupalcon.routing.yml 2 3 blog.page: 4 path: /blog 5 defaults: 6 _controller: Drupal\drupalcon\Controller\BlogPageController 7 _title: Blog 8 requirements: 9 _permission: access content 10 @opdavies

Slide 69

Slide 69 text

E 1 / 1 (100%) Time: 00:01.379, Memory: 6.00 MB There was 1 error: 1) Drupal\Tests\drupalcon\Functional\BlogPageTest::it_loads_the_blog_page Behat\Mink\Exception\ExpectationException: Current response status code is 404, but 200 expected. /app/vendor/behat/mink/src/WebAssert.php:794 /app/vendor/behat/mink/src/WebAssert.php:130 /app/web/modules/custom/drupalcon/tests/src/BlogPageTest.php:16 /app/vendor/phpunit/phpunit/src/Framework/TestResult.php:728 ERRORS! Tests: 1, Assertions: 2, Errors: 1. @opdavies

Slide 70

Slide 70 text

1 public static $modules = ['drupalcon']; E 1 / 1 (100%) Time: 00:01.532, Memory: 6.00 MB There was 1 error: 1) Drupal\Tests\drupalcon\Functional\BlogPageTest::it_loads_the_blog_page Behat\Mink\Exception\ExpectationException: Current response status code is 403, but 200 expected. @opdavies

Slide 71

Slide 71 text

1 public static $modules = ['node', 'drupalcon']; E 1 / 1 (100%) Time: 00:01.906, Memory: 6.00 MB There was 1 error: 1) Drupal\Tests\drupalcon\Functional\BlogPageTest::it_loads_the_blog_page Behat\Mink\Exception\ExpectationException: Current response status code is 500, but 200 expected. @opdavies

Slide 72

Slide 72 text

1 // src/Controller/BlogPageController.php 2 3 namespace Drupal\drupalcon\Controller; 4 5 declare(strict_types=1); 6 7 final class BlogPageController { 8 9 public function __invoke(): array { 10 return []; 11 } 12 13 } 14 @opdavies

Slide 73

Slide 73 text

1 // src/Controller/BlogPageController.php 2 3 namespace Drupal\drupalcon\Controller; 4 5 declare(strict_types=1); 6 7 final class BlogPageController { 8 9 public function __invoke(): array { 10 return []; 11 } 12 13 } 14 @opdavies

Slide 74

Slide 74 text

1 // src/Controller/BlogPageController.php 2 3 namespace Drupal\drupalcon\Controller; 4 5 declare(strict_types=1); 6 7 final class BlogPageController { 8 9 public function __invoke(): array { 10 return []; 11 } 12 13 } 14 @opdavies

Slide 75

Slide 75 text

1 // src/Controller/BlogPageController.php 2 3 namespace Drupal\drupalcon\Controller; 4 5 declare(strict_types=1); 6 7 final class BlogPageController { 8 9 public function __invoke(): array { 10 return []; 11 } 12 13 } 14 @opdavies

Slide 76

Slide 76 text

. 1 / 1 (100%) Time: 00:01.916, Memory: 6.00 MB OK (1 test, 3 assertions) Task completed in 0m2.147s @opdavies

Slide 77

Slide 77 text

1 /** @test */ 2 public function it_loads_the_blog_page(): void { 3 $this->drupalGet('/blog'); 4 5 $session = $this->assertSession(); 6 $session->statusCodeEquals(200); 7 8 $session->responseContains('

Blog

'); 9 $session->pageTextContains('Welcome to my blog!'); 10 } 11 @opdavies

Slide 78

Slide 78 text

1 /** @test */ 2 public function it_loads_the_blog_page(): void { 3 $this->drupalGet('/blog'); 4 5 $session = $this->assertSession(); 6 $session->statusCodeEquals(200); 7 8 $session->responseContains('

Blog

'); 9 $session->pageTextContains('Welcome to my blog!'); 10 } 11 @opdavies

Slide 79

Slide 79 text

1 /** @test */ 2 public function it_loads_the_blog_page(): void { 3 $this->drupalGet('/blog'); 4 5 $session = $this->assertSession(); 6 $session->statusCodeEquals(200); 7 8 $session->responseContains('

Blog

'); 9 $session->pageTextContains('Welcome to my blog!'); 10 } 11 @opdavies

Slide 80

Slide 80 text

There was 1 error: 1) Drupal\Tests\drupalcon\Functional\BlogPageTest::it_loads_the_blog_page Behat\Mink\Exception\ResponseTextException: The text "Welcome to my blog!" was not found anywhere in the text of the current page. @opdavies

Slide 81

Slide 81 text

1 namespace Drupal\drupalcon\Controller; 2 3 use Drupal\Core\StringTranslation\StringTranslationTrait; 4 5 class BlogPageController { 6 7 use StringTranslationTrait; 8 9 public function __invoke(): array { 10 return [ 11 '#markup' => $this->t('Welcome to my blog!'), 12 ]; 13 } 14 15 } 16 @opdavies

Slide 82

Slide 82 text

1 namespace Drupal\drupalcon\Controller; 2 3 use Drupal\Core\StringTranslation\StringTranslationTrait; 4 5 class BlogPageController { 6 7 use StringTranslationTrait; 8 9 public function __invoke(): array { 10 return [ 11 '#markup' => $this->t('Welcome to my blog!'), 12 ]; 13 } 14 15 } 16 @opdavies

Slide 83

Slide 83 text

. 1 / 1 (100%) Time: 00:01.911, Memory: 6.00 MB OK (1 test, 3 assertions) @opdavies

Slide 84

Slide 84 text

1 // tests/src/ArticleRepositoryTest.php 2 3 namespace Drupal\Tests\drupalcon\Kernel; 4 5 use Drupal\KernelTests\Core\Entity\EntityKernelTestBase; 6 7 class ArticleRepositoryTest extends EntityKernelTestBase { 8 9 /** @test */ 10 public function it_returns_blog_posts(): void { 11 $repository = $this->container->get(ArticleRepository::class); 12 13 $this->assertCount(1, $repository->getAll()); 14 } 15 @opdavies

Slide 85

Slide 85 text

1 // tests/src/ArticleRepositoryTest.php 2 3 namespace Drupal\Tests\drupalcon\Kernel; 4 5 use Drupal\KernelTests\Core\Entity\EntityKernelTestBase; 6 7 class ArticleRepositoryTest extends EntityKernelTestBase { 8 9 /** @test */ 10 public function it_returns_blog_posts(): void { 11 $repository = $this->container->get(ArticleRepository::class); 12 13 $this->assertCount(1, $repository->getAll()); 14 } 15 @opdavies

Slide 86

Slide 86 text

1 // tests/src/ArticleRepositoryTest.php 2 3 namespace Drupal\Tests\drupalcon\Kernel; 4 5 use Drupal\KernelTests\Core\Entity\EntityKernelTestBase; 6 7 class ArticleRepositoryTest extends EntityKernelTestBase { 8 9 /** @test */ 10 public function it_returns_blog_posts(): void { 11 $repository = $this->container->get(ArticleRepository::class); 12 13 $this->assertCount(1, $repository->getAll()); 14 } 15 @opdavies

Slide 87

Slide 87 text

1 // tests/src/ArticleRepositoryTest.php 2 3 namespace Drupal\Tests\drupalcon\Kernel; 4 5 use Drupal\KernelTests\Core\Entity\EntityKernelTestBase; 6 7 class ArticleRepositoryTest extends EntityKernelTestBase { 8 9 /** @test */ 10 public function it_returns_blog_posts(): void { 11 $repository = $this->container->get(ArticleRepository::class); 12 13 $this->assertCount(1, $repository->getAll()); 14 } 15 @opdavies

Slide 88

Slide 88 text

1 // tests/src/ArticleRepositoryTest.php 2 3 namespace Drupal\Tests\drupalcon\Kernel; 4 5 use Drupal\KernelTests\Core\Entity\EntityKernelTestBase; 6 7 class ArticleRepositoryTest extends EntityKernelTestBase { 8 9 /** @test */ 10 public function it_returns_blog_posts(): void { 11 $repository = $this->container->get(ArticleRepository::class); 12 13 $this->assertCount(1, $repository->getAll()); 14 } 15 @opdavies

Slide 89

Slide 89 text

E 1 / 1 (100%) Time: 00:00.405, Memory: 6.00 MB There was 1 error: 1) Drupal\Tests\drupalcon\Kernel\ArticleRepositoryTest::it_returns_blog_posts Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException: You have requested a non-existent service "Drupal\Tests\drupalcon\Kernel\ArticleRepository". E 1 / 1 (100%) Time: 00:00.405, Memory: 6.00 MB There was 1 error: @opdavies

Slide 90

Slide 90 text

1) Drupal\Tests\drupalcon\Kernel\ArticleRepositoryTest::it_returns_blog_posts Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException: You have requested a non-existent service "Drupal\Tests\drupalcon\Kernel\ArticleRepository". @opdavies

Slide 91

Slide 91 text

1 // src/Repository/ArticleNodeRepository.php 2 3 namespace Drupal\drupalcon\Repository; 4 5 final class ArticleRepository { 6 } 7 1 # drupalcon.services.yml 2 3 services: 4 Drupal\drupalcon\Repository\ArticleRepository: ~ 5 @opdavies

Slide 92

Slide 92 text

E 1 / 1 (100%) Time: 00:00.403, Memory: 6.00 MB There was 1 error: 1) Drupal\Tests\drupalcon\Kernel\ArticleRepositoryTest::it_returns_blog_posts Error: Call to undefined method Drupal\drupalcon\Repository\ArticleRepository::getAll() /app/web/modules/custom/drupalcon/tests/src/ArticleRepositoryTest.php:18 /app/vendor/phpunit/phpunit/src/Framework/TestResult.php:728 ERRORS! Tests: 1, Assertions: 4, Errors: 1. @opdavies

Slide 93

Slide 93 text

1 namespace Drupal\drupalcon\Repository; 2 3 final class ArticleRepository { 4 5 public function getAll(): array { 6 return []; 7 } 8 9 } 10 @opdavies

Slide 94

Slide 94 text

F 1 / 1 (100%) Time: 00:00.266, Memory: 6.00 MB There was 1 failure: 1) Drupal\Tests\drupalcon\Kernel\ArticleRepositoryTest::it_returns_blog_posts Failed asserting that actual size 0 matches expected size 1. @opdavies

Slide 95

Slide 95 text

1 namespace Drupal\drupalcon\Repository; 2 3 use Drupal\Core\Entity\EntityStorageInterface; 4 use Drupal\Core\Entity\EntityTypeManagerInterface; 5 6 final class ArticleRepository { 7 8 private EntityStorageInterface $nodeStorage; 9 10 public function __construct( 11 private EntityTypeManagerInterface $entityTypeManager, 12 ) { 13 $this->nodeStorage = $this->entityTypeManager->getStorage('node'); 14 } 15 @opdavies

Slide 96

Slide 96 text

1 namespace Drupal\drupalcon\Repository; 2 3 use Drupal\Core\Entity\EntityStorageInterface; 4 use Drupal\Core\Entity\EntityTypeManagerInterface; 5 6 final class ArticleRepository { 7 8 private EntityStorageInterface $nodeStorage; 9 10 public function __construct( 11 private EntityTypeManagerInterface $entityTypeManager, 12 ) { 13 $this->nodeStorage = $this->entityTypeManager->getStorage('node'); 14 } 15 @opdavies

Slide 97

Slide 97 text

1 namespace Drupal\drupalcon\Repository; 2 3 use Drupal\Core\Entity\EntityStorageInterface; 4 use Drupal\Core\Entity\EntityTypeManagerInterface; 5 6 final class ArticleRepository { 7 8 private EntityStorageInterface $nodeStorage; 9 10 public function __construct( 11 private EntityTypeManagerInterface $entityTypeManager, 12 ) { 13 $this->nodeStorage = $this->entityTypeManager->getStorage('node'); 14 } 15 @opdavies

Slide 98

Slide 98 text

1 public function getAll(): array { 2 return $this->nodeStorage->loadMultiple(); 3 } 4 5 } 6 @opdavies

Slide 99

Slide 99 text

1 public function getAll(): array { 2 return $this->nodeStorage->loadMultiple(); 3 } 4 5 } 6 @opdavies

Slide 100

Slide 100 text

1 # drupalcon.services.yml 2 3 services: 4 Drupal\drupalcon\Repository\ArticleRepository: 5 autowire: true 6 1 services: 2 Drupal\drupalcon\Repository\ArticleRepository: 3 arguments: 4 - '@entity_type.manager' 5 @opdavies

Slide 101

Slide 101 text

E 1 / 1 (100%) Time: 00:00.405, Memory: 6.00 MB There was 1 error: 1) Drupal\Tests\drupalcon\Kernel\ArticleRepositoryTest::it_returns_blog_posts Drupal\Component\Plugin\Exception\PluginNotFoundException: The "node" entity type does not exist. 1 public static $modules = [ 2 'drupalcon', 3 'node', 4 ]; 5 @opdavies

Slide 102

Slide 102 text

F 1 / 1 (100%) Time: 00:00.421, Memory: 6.00 MB There was 1 failure: 1) Drupal\Tests\drupalcon\Kernel\ArticleRepositoryTest::it_returns_blog_posts Failed asserting that actual size 0 matches expected size 1. @opdavies

Slide 103

Slide 103 text

1 use NodeCreationTrait; 2 3 /** @test */ 4 public function it_returns_blog_posts() { 5 $this->createNode(['type' => 'article']); 6 7 /** @var ArticleRepository */ 8 $repository = $this->container->get(ArticleRepository::class); 9 10 $articles = $repository->getAll(); 11 12 $this->assertCount(1, $articles); 13 } 14 @opdavies

Slide 104

Slide 104 text

1 use NodeCreationTrait; 2 3 /** @test */ 4 public function it_returns_blog_posts() { 5 $this->createNode(['type' => 'article']); 6 7 /** @var ArticleRepository */ 8 $repository = $this->container->get(ArticleRepository::class); 9 10 $articles = $repository->getAll(); 11 12 $this->assertCount(1, $articles); 13 } 14 @opdavies

Slide 105

Slide 105 text

. 1 / 1 (100%) Time: 00:00.439, Memory: 6.00 MB OK (1 test, 11 assertions) @opdavies

Slide 106

Slide 106 text

1 $this->createNode([ 2 'title' => 'Test post', 3 'type' => 'article', 4 ]); 5 6 $repository = $this->container->get(ArticleRepository::class); 7 8 $articles = $repository->getAll(); 9 10 $this->assertCount(1, $articles); 11 $this->assertIsObject($articles[1]); 12 13 $this->assertInstanceOf(NodeInterface::class, $articles[1]); 14 $this->assertSame('article', $articles[1]->bundle()); 15 $this->assertSame('Test post', $articles[1]->label()); 16 @opdavies

Slide 107

Slide 107 text

1 $this->createNode([ 2 'title' => 'Test post', 3 'type' => 'article', 4 ]); 5 6 $repository = $this->container->get(ArticleRepository::class); 7 8 $articles = $repository->getAll(); 9 10 $this->assertCount(1, $articles); 11 $this->assertIsObject($articles[1]); 12 13 $this->assertInstanceOf(NodeInterface::class, $articles[1]); 14 $this->assertSame('article', $articles[1]->bundle()); 15 $this->assertSame('Test post', $articles[1]->label()); 16 @opdavies

Slide 108

Slide 108 text

1 $this->createNode([ 2 'title' => 'Test post', 3 'type' => 'article', 4 ]); 5 6 $repository = $this->container->get(ArticleRepository::class); 7 8 $articles = $repository->getAll(); 9 10 $this->assertCount(1, $articles); 11 $this->assertIsObject($articles[1]); 12 13 $this->assertInstanceOf(NodeInterface::class, $articles[1]); 14 $this->assertSame('article', $articles[1]->bundle()); 15 $this->assertSame('Test post', $articles[1]->label()); 16 @opdavies

Slide 109

Slide 109 text

1 $this->createNode([ 2 'title' => 'Test post', 3 'type' => 'article', 4 ]); 5 6 $repository = $this->container->get(ArticleRepository::class); 7 8 $articles = $repository->getAll(); 9 10 $this->assertCount(1, $articles); 11 $this->assertIsObject($articles[1]); 12 13 $this->assertInstanceOf(NodeInterface::class, $articles[1]); 14 $this->assertSame('article', $articles[1]->bundle()); 15 $this->assertSame('Test post', $articles[1]->label()); 16 @opdavies

Slide 110

Slide 110 text

1 public function only_published_articles_are_returned() { 2 $this->createNode(['type' => 'article', 'status' => Node::PUBLISHED]); 3 $this->createNode(['type' => 'article', 'status' => Node::NOT_PUBLISHED]); 4 $this->createNode(['type' => 'article', 'status' => Node::PUBLISHED]); 5 $this->createNode(['type' => 'article', 'status' => Node::NOT_PUBLISHED]); 6 $this->createNode(['type' => 'article', 'status' => Node::PUBLISHED]); 7 8 $repository = $this->container->get(ArticleRepository::class); 9 10 $articles = $repository->getAll(); 11 12 $this->assertCount(3, $articles); 13 } 14 @opdavies

Slide 111

Slide 111 text

1 public function only_published_articles_are_returned() { 2 $this->createNode(['type' => 'article', 'status' => Node::PUBLISHED]); 3 $this->createNode(['type' => 'article', 'status' => Node::NOT_PUBLISHED]); 4 $this->createNode(['type' => 'article', 'status' => Node::PUBLISHED]); 5 $this->createNode(['type' => 'article', 'status' => Node::NOT_PUBLISHED]); 6 $this->createNode(['type' => 'article', 'status' => Node::PUBLISHED]); 7 8 $repository = $this->container->get(ArticleRepository::class); 9 10 $articles = $repository->getAll(); 11 12 $this->assertCount(3, $articles); 13 } 14 @opdavies

Slide 112

Slide 112 text

1 public function only_published_articles_are_returned() { 2 $this->createNode(['type' => 'article', 'status' => Node::PUBLISHED]); 3 $this->createNode(['type' => 'article', 'status' => Node::NOT_PUBLISHED]); 4 $this->createNode(['type' => 'article', 'status' => Node::PUBLISHED]); 5 $this->createNode(['type' => 'article', 'status' => Node::NOT_PUBLISHED]); 6 $this->createNode(['type' => 'article', 'status' => Node::PUBLISHED]); 7 8 $repository = $this->container->get(ArticleRepository::class); 9 10 $articles = $repository->getAll(); 11 12 $this->assertCount(3, $articles); 13 } 14 @opdavies

Slide 113

Slide 113 text

1 public function only_published_articles_are_returned() { 2 $this->createNode(['type' => 'article', 'status' => Node::PUBLISHED]); 3 $this->createNode(['type' => 'article', 'status' => Node::NOT_PUBLISHED]); 4 $this->createNode(['type' => 'article', 'status' => Node::PUBLISHED]); 5 $this->createNode(['type' => 'article', 'status' => Node::NOT_PUBLISHED]); 6 $this->createNode(['type' => 'article', 'status' => Node::PUBLISHED]); 7 8 $repository = $this->container->get(ArticleRepository::class); 9 10 $articles = $repository->getAll(); 11 12 $this->assertCount(3, $articles); 13 } 14 @opdavies

Slide 114

Slide 114 text

.F 2 / 2 (100%) Time: 00:00.903, Memory: 6.00 MB There was 1 failure: 1) Drupal\Tests\drupalcon\Kernel\ArticleRepositoryTest:: only_published_articles_are_returned Failed asserting that actual size 5 matches expected size 3. @opdavies

Slide 115

Slide 115 text

1 public function getAll(): array { 2 return $this->nodeStorage->loadByProperties([ 3 'status' => NodeInterface::PUBLISHED, 4 ]); 5 } 6 .. 2 / 2 (100%) Time: 00:00.891, Memory: 6.00 MB OK (2 tests, 22 assertions) @opdavies

Slide 116

Slide 116 text

1 public function nodes_are_ordered_by_date_and_returned_newest_first(): void { 2 $this->createNode(['type' => 'article', 3 'created' => (new DrupalDateTime('-2 days'))->getTimestamp()]); 4 $this->createNode(['type' => 'article', 5 'created' => (new DrupalDateTime('-1 week'))->getTimestamp()]); 6 $this->createNode(['type' => 'article', 7 'created' => (new DrupalDateTime('-1 hour'))->getTimestamp()]); 8 $this->createNode(['type' => 'article', 9 'created' => (new DrupalDateTime('-1 year'))->getTimestamp()]); 10 $this->createNode(['type' => 'article', 11 'created' => (new DrupalDateTime('-1 month'))->getTimestamp()]); 12 13 $repository = $this->container->get(ArticleRepository::class); 14 $nodes = $repository->getAll(); 15 16 $this->assertSame([3, 1, 2, 5, 4], array_keys($nodes)); 17 @opdavies

Slide 117

Slide 117 text

1 public function nodes_are_ordered_by_date_and_returned_newest_first(): void { 2 $this->createNode(['type' => 'article', 3 'created' => (new DrupalDateTime('-2 days'))->getTimestamp()]); 4 $this->createNode(['type' => 'article', 5 'created' => (new DrupalDateTime('-1 week'))->getTimestamp()]); 6 $this->createNode(['type' => 'article', 7 'created' => (new DrupalDateTime('-1 hour'))->getTimestamp()]); 8 $this->createNode(['type' => 'article', 9 'created' => (new DrupalDateTime('-1 year'))->getTimestamp()]); 10 $this->createNode(['type' => 'article', 11 'created' => (new DrupalDateTime('-1 month'))->getTimestamp()]); 12 13 $repository = $this->container->get(ArticleRepository::class); 14 $nodes = $repository->getAll(); 15 16 $this->assertSame([3, 1, 2, 5, 4], array_keys($nodes)); 17 @opdavies

Slide 118

Slide 118 text

1 public function nodes_are_ordered_by_date_and_returned_newest_first(): void { 2 $this->createNode(['type' => 'article', 3 'created' => (new DrupalDateTime('-2 days'))->getTimestamp()]); 4 $this->createNode(['type' => 'article', 5 'created' => (new DrupalDateTime('-1 week'))->getTimestamp()]); 6 $this->createNode(['type' => 'article', 7 'created' => (new DrupalDateTime('-1 hour'))->getTimestamp()]); 8 $this->createNode(['type' => 'article', 9 'created' => (new DrupalDateTime('-1 year'))->getTimestamp()]); 10 $this->createNode(['type' => 'article', 11 'created' => (new DrupalDateTime('-1 month'))->getTimestamp()]); 12 13 $repository = $this->container->get(ArticleRepository::class); 14 $nodes = $repository->getAll(); 15 16 $this->assertSame([3, 1, 2, 5, 4], array_keys($nodes)); 17 @opdavies

Slide 119

Slide 119 text

F 1 / 1 (100%) Time: 00:00.449, Memory: 8.00 MB There was 1 failure: 1) Drupal\Tests\drupalcon\Kernel\ArticleRepositoryTest::nodes_are_ordered_by_date_and_ returned_newest_first Failed asserting that two arrays are identical. --- Expected +++ Actual @@ @@ Array &0 ( - 0 => 3 - 1 => 1 - 2 => 2 - 3 => 5 @opdavies

Slide 120

Slide 120 text

- 4 => 4 + 0 => 1 + 1 => 2 + 2 => 3 + 3 => 4 + 4 => 5 ) /app/vendor/phpunit/phpunit/src/Framework/Constraint/Constraint.php:121 /app/vendor/phpunit/phpunit/src/Framework/Constraint/IsIdentical.php:79 /app/web/modules/custom/drupalcon/tests/src/ArticleRepositoryTest.php:60 /app/vendor/phpunit/phpunit/src/Framework/TestResult.php:728 FAILURES! Tests: 1, Assertions: 11, Failures: 1. @opdavies

Slide 121

Slide 121 text

1 public function getAll(): array { 2 $articles = $this->nodeStorage->loadByProperties([ 3 'status' => NodeInterface::PUBLISHED, 4 ]); 5 6 uasort($articles, fn (NodeInterface $a, NodeInterface $b) => 7 $b->getCreatedTime() <=> $a->getCreatedTime()); 8 9 return $articles; 10 } 11 @opdavies

Slide 122

Slide 122 text

1 public function getAll(): array { 2 $articles = $this->nodeStorage->loadByProperties([ 3 'status' => NodeInterface::PUBLISHED, 4 ]); 5 6 uasort($articles, fn (NodeInterface $a, NodeInterface $b) => 7 $b->getCreatedTime() <=> $a->getCreatedTime()); 8 9 return $articles; 10 } 11 @opdavies

Slide 123

Slide 123 text

1 public function getAll(): array { 2 $articles = $this->nodeStorage->loadByProperties([ 3 'status' => NodeInterface::PUBLISHED, 4 ]); 5 6 uasort($articles, fn (NodeInterface $a, NodeInterface $b) => 7 $b->getCreatedTime() <=> $a->getCreatedTime()); 8 9 return $articles; 10 } 11 @opdavies

Slide 124

Slide 124 text

1 public function getAll(): array { 2 $articles = $this->nodeStorage->loadByProperties([ 3 'status' => NodeInterface::PUBLISHED, 4 ]); 5 6 uasort($articles, fn (NodeInterface $a, NodeInterface $b) => 7 $b->getCreatedTime() <=> $a->getCreatedTime()); 8 9 return $articles; 10 } 11 @opdavies

Slide 125

Slide 125 text

. 1 / 1 (100%) Time: 00:00.462, Memory: 6.00 MB OK (1 test, 11 assertions) @opdavies

Slide 126

Slide 126 text

@opdavies

Slide 127

Slide 127 text

Thanks! References: • https://phpunit.de • https://docs.phpunit.de • https://www.drupal.org/docs/automated-testing Me: • https://www.oliverdavies.uk • https://www.oliverdavies.uk/atdc @opdavies