Re-organise

Signed-off-by: Oliver Davies <oliver@oliverdavies.uk>
This commit is contained in:
Oliver Davies 2025-09-25 21:52:45 +01:00
parent 34a2740106
commit 6891a7517a
440 changed files with 0 additions and 16 deletions

View file

@ -0,0 +1,4 @@
TDD: Test Driven Drupal
#######################
https://www.oliverdavies.uk/talks/tdd-test-driven-drupal

View file

@ -0,0 +1,13 @@
// web/modules/custom/example/tests/src/Functional.
namespace Drupal\Tests\example\Functional;
use Drupal\Tests\BrowserTestBase;
class ExampleTest extends BrowserTestBase {
public function testSomething() {
$this->assertTrue(FALSE);
}
}

View file

@ -0,0 +1,6 @@
# drupalcon.info.yml
name: DrupalCon demo
type: module
core_version_requirement: ^10
package: DrupalCon

View file

@ -0,0 +1,30 @@
// start code
namespace Drupal\drupalcon\Repository;
final class ArticleRepository {
public function getAll(): array {
return [];
}
}
// end code
// start output
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.
// end output
/app/vendor/phpunit/phpunit/src/Framework/Constraint/Constraint.php:121
/app/vendor/phpunit/phpunit/src/Framework/Constraint/Constraint.php:55
/app/web/modules/custom/drupalcon/tests/src/ArticleRepositoryTest.php:20
/app/vendor/phpunit/phpunit/src/Framework/TestResult.php:728
FAILURES!
Tests: 1, Assertions: 5, Failures: 1.

View file

@ -0,0 +1,47 @@
<?php
// start code 1
namespace Drupal\drupalcon\Repository;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
final class ArticleRepository {
private EntityStorageInterface $nodeStorage;
public function __construct(
private EntityTypeManagerInterface $entityTypeManager,
) {
$this->nodeStorage = $this->entityTypeManager->getStorage('node');
} // end code 1
// start code 2
public function getAll(): array {
return $this->nodeStorage->loadMultiple();
}
}
// end code 2
---
E 1 / 1 (100%)
Time: 00:00.401, Memory: 6.00 MB
There was 1 error:
1) Drupal\Tests\drupalcon\Kernel\ArticleRepositoryTest::it_returns_blog_posts
ArgumentCountError: Too few arguments to function Drupal\drupalcon\Repository\ArticleR
epository::__construct(), 0 passed and exactly 1 expected
/app/web/modules/custom/drupalcon/src/Repository/ArticleRepository.php:9
/app/vendor/symfony/dependency-injection/ContainerBuilder.php:1140
/app/vendor/symfony/dependency-injection/ContainerBuilder.php:586
/app/vendor/symfony/dependency-injection/ContainerBuilder.php:531
/app/web/modules/custom/drupalcon/tests/src/ArticleRepositoryTest.php:15
/app/vendor/phpunit/phpunit/src/Framework/TestResult.php:728
ERRORS!
Tests: 1, Assertions: 4, Errors: 1.

View file

@ -0,0 +1,41 @@
// start services1
# drupalcon.services.yml
services:
Drupal\drupalcon\Repository\ArticleRepository:
autowire: true
// end services1
// start services2
services:
Drupal\drupalcon\Repository\ArticleRepository:
arguments:
- '@entity_type.manager'
// end services2
---
// start output
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.
// end output
/app/web/core/lib/Drupal/Core/Entity/EntityTypeManager.php:139
/app/web/core/lib/Drupal/Core/Entity/EntityTypeManager.php:253
/app/web/core/lib/Drupal/Core/Entity/EntityTypeManager.php:192
/app/web/modules/custom/drupalcon/src/Repository/ArticleRepository.php:12
/app/vendor/symfony/dependency-injection/ContainerBuilder.php:1140
/app/vendor/symfony/dependency-injection/ContainerBuilder.php:586
/app/vendor/symfony/dependency-injection/ContainerBuilder.php:531
/app/web/modules/custom/drupalcon/tests/src/ArticleRepositoryTest.php:15
/app/vendor/phpunit/phpunit/src/Framework/TestResult.php:728
ERRORS!
Tests: 1, Assertions: 4, Errors: 1.

View file

@ -0,0 +1,25 @@
// start test
public static $modules = [
'drupalcon',
'node',
];
// end test
// start output
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.
// end output
/app/vendor/phpunit/phpunit/src/Framework/Constraint/Constraint.php:121
/app/vendor/phpunit/phpunit/src/Framework/Constraint/Constraint.php:55
/app/web/modules/custom/drupalcon/tests/src/ArticleRepositoryTest.php:19
/app/vendor/phpunit/phpunit/src/Framework/TestResult.php:728
FAILURES!
Tests: 1, Assertions: 9, Failures: 1.

View file

@ -0,0 +1,40 @@
namespace Drupal\Tests\drupalcon\Kernel;
use Drupal\drupalcon\Repository\ArticleRepository;
use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
use Drupal\Tests\node\Traits\NodeCreationTrait;
class ArticleRepositoryTest extends EntityKernelTestBase {
public static $modules = [
'drupalcon',
'node',
];
// start test
use NodeCreationTrait;
/** @test */
public function it_returns_blog_posts() {
$this->createNode(['type' => 'article']);
/** @var ArticleRepository */
$repository = $this->container->get(ArticleRepository::class);
$articles = $repository->getAll();
$this->assertCount(1, $articles);
}
// end test
}
---
// start output
. 1 / 1 (100%)
Time: 00:00.439, Memory: 6.00 MB
OK (1 test, 11 assertions)
// end output

View file

@ -0,0 +1,49 @@
<?php
namespace Drupal\Tests\drupalcon\Kernel;
use Drupal\drupalcon\Repository\ArticleRepository;
use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
use Drupal\node\NodeInterface;
use Drupal\Tests\node\Traits\NodeCreationTrait;
class ArticleRepositoryTest extends EntityKernelTestBase {
use NodeCreationTrait;
public static $modules = [
'drupalcon',
'node',
];
/** @test */
public function it_returns_blog_posts() {
// start test
$this->createNode([
'title' => 'Test post',
'type' => 'article',
]);
$repository = $this->container->get(ArticleRepository::class);
$articles = $repository->getAll();
$this->assertCount(1, $articles);
$this->assertIsObject($articles[1]);
$this->assertInstanceOf(NodeInterface::class, $articles[1]);
$this->assertSame('article', $articles[1]->bundle());
$this->assertSame('Test post', $articles[1]->label());
// end test
}
}
---
Article Repository (Drupal\Tests\drupalcon\Kernel\ArticleRepository)
✔ It returns blog posts
Time: 00:00.449, Memory: 6.00 MB
OK (1 test, 15 assertions)

View file

@ -0,0 +1,36 @@
// start test
public function only_published_articles_are_returned() {
$this->createNode(['type' => 'article', 'status' => Node::PUBLISHED]);
$this->createNode(['type' => 'article', 'status' => Node::NOT_PUBLISHED]);
$this->createNode(['type' => 'article', 'status' => Node::PUBLISHED]);
$this->createNode(['type' => 'article', 'status' => Node::NOT_PUBLISHED]);
$this->createNode(['type' => 'article', 'status' => Node::PUBLISHED]);
$repository = $this->container->get(ArticleRepository::class);
$articles = $repository->getAll();
$this->assertCount(3, $articles);
} // end test
---
// start output
.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.
// end output
/app/vendor/phpunit/phpunit/src/Framework/Constraint/Constraint.php:121
/app/vendor/phpunit/phpunit/src/Framework/Constraint/Constraint.php:55
/app/web/modules/custom/drupalcon/tests/src/ArticleRepositoryTest.php:40
/app/vendor/phpunit/phpunit/src/Framework/TestResult.php:728
FAILURES!
Tests: 2, Assertions: 22, Failures: 1.

View file

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Drupal\drupalcon\Repository;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\node\NodeInterface;
final class ArticleRepository {
private EntityStorageInterface $nodeStorage;
public function __construct(
private EntityTypeManagerInterface $entityTypeManager,
) {
$this->nodeStorage = $this->entityTypeManager->getStorage('node');
}
// start code
public function getAll(): array {
return $this->nodeStorage->loadByProperties([
'status' => NodeInterface::PUBLISHED,
]);
}
// end code
}
// start output
.. 2 / 2 (100%)
Time: 00:00.891, Memory: 6.00 MB
OK (2 tests, 22 assertions)
// end output

View file

@ -0,0 +1,54 @@
// start test
public function nodes_are_ordered_by_date_and_returned_newest_first(): void {
$this->createNode(['type' => 'article',
'created' => (new DrupalDateTime('-2 days'))->getTimestamp()]);
$this->createNode(['type' => 'article',
'created' => (new DrupalDateTime('-1 week'))->getTimestamp()]);
$this->createNode(['type' => 'article',
'created' => (new DrupalDateTime('-1 hour'))->getTimestamp()]);
$this->createNode(['type' => 'article',
'created' => (new DrupalDateTime('-1 year'))->getTimestamp()]);
$this->createNode(['type' => 'article',
'created' => (new DrupalDateTime('-1 month'))->getTimestamp()]);
$repository = $this->container->get(ArticleRepository::class);
$nodes = $repository->getAll();
$this->assertSame([3, 1, 2, 5, 4], array_keys($nodes));
// end test
}
// start output
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
- 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.
// end output

View file

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace Drupal\drupalcon\Repository;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\node\NodeInterface;
final class ArticleRepository {
private EntityStorageInterface $nodeStorage;
public function __construct(
private EntityTypeManagerInterface $entityTypeManager,
) {
$this->nodeStorage = $this->entityTypeManager->getStorage('node');
}
// start code
public function getAll(): array {
$articles = $this->nodeStorage->loadByProperties([
'status' => NodeInterface::PUBLISHED,
]);
uasort($articles, fn (NodeInterface $a, NodeInterface $b) =>
$b->getCreatedTime() <=> $a->getCreatedTime());
return $articles;
} // end code
}
---
// start output
. 1 / 1 (100%)
Time: 00:00.462, Memory: 6.00 MB
OK (1 test, 11 assertions)
// end output

View file

@ -0,0 +1,6 @@
public function testSomething()
public function test_something()
/** @test */
public function it_does_something()

View file

@ -0,0 +1,13 @@
// tests/src/Functional/BlogPageTest.php
namespace Drupal\Tests\drupalcon\Functional;
use Drupal\Tests\BrowserTestBase;
final class BlogPageTest extends BrowserTestBase {
public $defaultTheme = 'stark';
public static $modules = [];
}

View file

@ -0,0 +1,41 @@
// tests/src/Functional/BlogPageTest.php
/** @test */
public function it_loads_the_blog_page(): void {
$this->drupalGet('/blog');
$this->assertSession()->statusCodeEquals(200);
}
// end test
// start output
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.
// end output
// start routing
# drupalcon.routing.yml
blog.page:
path: /blog
defaults:
_controller: Drupal\drupalcon\Controller\BlogPageController
_title: Blog
requirements:
_permission: access content
// end routing

View file

@ -0,0 +1,28 @@
public static $modules = ['drupalcon'];// output
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.
// end output
/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:17
/app/vendor/phpunit/phpunit/src/Framework/TestResult.php:728
ERRORS!
Tests: 1, Assertions: 3, Errors: 1.
/app/vendor/behat/mink/src/WebAssert.php:794
/app/vendor/behat/mink/src/WebAssert.php:130
/app/web/tests/src/Functional/BlogPageTest.php:23
/app/vendor/phpunit/phpunit/src/Framework/TestResult.php:728
ERRORS!
Tests: 1, Assertions: 3, Errors: 1.

View file

@ -0,0 +1,21 @@
public static $modules = ['node', 'drupalcon']; // end code
// start output
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.
// end output
/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:17
/app/vendor/phpunit/phpunit/src/Framework/TestResult.php:728
ERRORS!
Tests: 1, Assertions: 3, Errors: 1.

View file

@ -0,0 +1,27 @@
// start code
// src/Controller/BlogPageController.php
namespace Drupal\drupalcon\Controller;
declare(strict_types=1);
final class BlogPageController {
public function __invoke(): array {
return [];
}
}
// end code
// start output
. 1 / 1 (100%)
Time: 00:01.916, Memory: 6.00 MB
OK (1 test, 3 assertions)
Task completed in 0m2.147s
// end output
Task completed in 0m2.124s

View file

@ -0,0 +1,61 @@
// start test
/** @test */
public function it_loads_the_blog_page(): void {
$this->drupalGet('/blog');
$session = $this->assertSession();
$session->statusCodeEquals(200);
$session->responseContains('<h1>Blog</h1>');
$session->pageTextContains('Welcome to my blog!');
}
// end test
E 1 / 1 (100%)
Time: 00:02.101, Memory: 6.00 MB
// start output
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.
// end output
/app/vendor/behat/mink/src/WebAssert.php:907
/app/vendor/behat/mink/src/WebAssert.php:293
/app/web/modules/custom/drupalcon/tests/src/Functional/BlogPageTest.php:17
/app/vendor/phpunit/phpunit/src/Framework/TestResult.php:728
ERRORS!
Tests: 1, Assertions: 3, Errors: 1.
// start code
namespace Drupal\drupalcon\Controller;
use Drupal\Core\StringTranslation\StringTranslationTrait;
class BlogPageController {
use StringTranslationTrait;
public function __invoke(): array {
return [
'#markup' => $this->t('Welcome to my blog!'),
];
}
}
// end code
// start output2
. 1 / 1 (100%)
Time: 00:01.911, Memory: 6.00 MB
OK (1 test, 3 assertions)
// end output2
Task completed in 0m2.124s

View file

@ -0,0 +1,43 @@
// start code
// tests/src/ArticleRepositoryTest.php
namespace Drupal\Tests\drupalcon\Kernel;
use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
class ArticleRepositoryTest extends EntityKernelTestBase {
/** @test */
public function it_returns_blog_posts(): void {
$repository = $this->container->get(ArticleRepository::class);
$this->assertCount(1, $repository->getAll());
}// end code
}
---
// start output
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".
// end output
/app/vendor/symfony/dependency-injection/ContainerBuilder.php:992
/app/vendor/symfony/dependency-injection/ContainerBuilder.php:568
/app/vendor/symfony/dependency-injection/ContainerBuilder.php:531
/app/web/modules/custom/drupalcon/tests/src/ArticleRepositoryTest.php:11
/app/vendor/phpunit/phpunit/src/Framework/TestResult.php:728
ERRORS!
Tests: 1, Assertions: 4, Errors: 1.
Time: 00:00.409, Memory: 8.00 MB

View file

@ -0,0 +1,61 @@
// start code
// src/Repository/ArticleNodeRepository.php
namespace Drupal\drupalcon\Repository;
final class ArticleRepository {
}
// end code
---
// start services
# drupalcon.services.yml
services:
Drupal\drupalcon\Repository\ArticleRepository: ~
// end services
---
namespace Drupal\Tests\drupalcon\Kernel;
use Drupal\drupalcon\Repository\ArticleRepository;
use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
class ArticleRepositoryTest extends EntityKernelTestBase {
// start test
public static $modules = [
'drupalcon',
];
/** @test */
public function it_returns_blog_posts() {
/** @var ArticleRepository */
$repository = $this->container->get(ArticleRepository::class);
$articles = $repository->getAll();
$this->assertCount(1, $articles);
}
// end test
}
// start output
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.
// end output

857
test-driven-drupal/demo.rst Normal file
View file

@ -0,0 +1,857 @@
..
This file used a mixture of `plain` and `php` languages for code blocks so that `hl_lines` display correctly.
.. page:: titlePage
.. class:: centredtitle
Building a blog module
.. raw:: pdf
TextAnnotation "Shortened and simplified example."
TextAnnotation "I'd use Views for this in a real situation."
.. page:: standardPage
Acceptance criteria
===================
- As a site visitor
- I want to see a list of published articles at ``/blog``
- Ordered by post date, most recent first
Tasks
=====
- Ensure the blog page exists
- Ensure only published articles are shown
- Ensure the articles are shown in the correct order
.. raw:: pdf
PageBreak
.. Creating the test class.
.. code-block:: php
:include: code/2.txt
:linenos:
:startinline: true
.. raw:: pdf
PageBreak
.. code-block:: php
:include: code/2.txt
:linenos:
:startinline: true
:hl_lines: 1,2,3
.. raw:: pdf
PageBreak
.. code-block:: php
:include: code/2.txt
:linenos:
:startinline: true
:hl_lines: 5,6,7
.. raw:: pdf
PageBreak
.. code-block:: php
:include: code/2.txt
:linenos:
:startinline: true
:hl_lines: 9,10,11
.. raw:: pdf
PageBreak
.. Adding the first test.
.. code-block:: php
:include: code/3.txt
:linenos:
:startinline: true
:end-before: // end test
.. raw:: pdf
PageBreak
.. code-block:: php
:include: code/3.txt
:linenos:
:startinline: true
:end-before: // end test
:hl_lines: 3,4,8
.. raw:: pdf
PageBreak
.. code-block:: php
:include: code/3.txt
:linenos:
:startinline: true
:end-before: // end test
:hl_lines: 5
.. raw:: pdf
PageBreak
.. code-block:: php
:include: code/3.txt
:linenos:
:startinline: true
:end-before: // end test
:hl_lines: 7
.. raw:: pdf
PageBreak
.. code-block:: plain
:include: code/3.txt
:startinline: true
:start-after: // start output
:end-before: // end output
.. code-block:: php
:include: code/3.txt
:startinline: true
:start-after: // start output
:end-before: // end output
:hl_lines: 1,5
.. code-block:: php
:include: code/3.txt
:startinline: true
:start-after: // start output
:end-before: // end output
:hl_lines: 7
.. code-block:: php
:include: code/3.txt
:startinline: true
:start-after: // start output
:end-before: // end output
:hl_lines: 8,9
.. raw:: pdf
PageBreak
.. code-block:: yaml
:include: code/3.txt
:linenos:
:start-after: // start routing
:end-before: // end routing
.. raw:: pdf
PageBreak
.. code-block:: yaml
:include: code/3.txt
:linenos:
:start-after: // start routing
:end-before: // end routing
:hl_lines: 4,6
.. raw:: pdf
TextAnnotation "This controller doesn't exist yet, but the test will tell us that."
.. raw:: pdf
PageBreak
.. code-block:: php
:include: code/3.txt
:startinline: true
:start-after: // start output
:end-before: // end output
:hl_lines: 7,8,9
.. raw:: pdf
TextAnnotation "Same result as the module isn't enabled."
.. raw:: pdf
PageBreak
.. code-block:: php
:include: code/4.txt
:linenos:
:startinline: true
:end-before: // output
|
.. code-block:: plain
:include: code/4.txt
:startinline: true
:start-after: // output
:end-before: // end output
.. raw:: pdf
TextAnnotation "The `access content` permission isn't available."
PageBreak
.. Enable the node module.
.. code-block:: php
:include: code/5.txt
:linenos:
:startinline: true
:end-before: // end code
|
.. code-block:: plain
:include: code/5.txt
:start-after: // start output
:end-before: // end output
.. raw:: pdf
TextAnnotation "The error has changed. This is good."
TextAnnotation "The controller we specified doesn't exist."
PageBreak
.. Create the Controller.
.. code-block:: php
:include: code/6.txt
:linenos:
:startinline: true
:start-after: // start code
:end-before: // end code
.. raw:: pdf
PageBreak
.. code-block:: php
:include: code/6.txt
:linenos:
:startinline: true
:start-after: // start code
:end-before: // end code
:hl_lines: 1,2,3
.. raw:: pdf
PageBreak
.. code-block:: php
:include: code/6.txt
:linenos:
:startinline: true
:start-after: // start code
:end-before: // end code
:hl_lines: 7,13
.. raw:: pdf
PageBreak
.. code-block:: php
:include: code/6.txt
:linenos:
:startinline: true
:start-after: // start code
:end-before: // end code
:hl_lines: 9,10,11
.. raw:: pdf
PageBreak
.. code-block:: plain
:include: code/6.txt
:start-after: // start output
:end-before: // end output
.. raw:: pdf
PageBreak
.. code-block:: php
:include: code/7.txt
:linenos:
:startinline: true
:start-after: // start test
:end-before: // end test
.. raw:: pdf
TextAnnotation "Adding more assertions."
PageBreak
.. code-block:: php
:include: code/7.txt
:linenos:
:startinline: true
:start-after: // start test
:end-before: // end test
:hl_lines: 5,6
.. raw:: pdf
TextAnnotation "Refactor the original assertion."
PageBreak
.. code-block:: php
:include: code/7.txt
:linenos:
:startinline: true
:start-after: // start test
:end-before: // end test
:hl_lines: 8,9
.. raw:: pdf
PageBreak
.. code-block:: plain
:include: code/7.txt
:start-after: // start output
:end-before: // end output
.. raw:: pdf
PageBreak
.. code-block:: php
:include: code/7.txt
:linenos:
:startinline: true
:start-after: // start code
:end-before: // end code
:hl_lines: 3,7
.. raw:: pdf
PageBreak
.. code-block:: php
:include: code/7.txt
:linenos:
:startinline: true
:start-after: // start code
:end-before: // end code
:hl_lines: 10,11,12
.. raw:: pdf
PageBreak
.. code-block:: plain
:include: code/7.txt
:start-after: // start output2
:end-before: // end output2
.. raw:: pdf
PageBreak
.. code-block:: php
:include: code/8.txt
:linenos:
:startinline: true
:start-after: // start code
:end-before: // end code
.. raw:: pdf
PageBreak
.. code-block:: php
:include: code/8.txt
:linenos:
:startinline: true
:start-after: // start code
:end-before: // end code
:hl_lines: 1,2,3,4,5,6,7,15
.. raw:: pdf
PageBreak
.. code-block:: php
:include: code/8.txt
:linenos:
:startinline: true
:start-after: // start code
:end-before: // end code
:hl_lines: 9,10,14
.. raw:: pdf
PageBreak
.. code-block:: php
:include: code/8.txt
:linenos:
:startinline: true
:start-after: // start code
:end-before: // end code
:hl_lines: 11
.. raw:: pdf
PageBreak
.. code-block:: php
:include: code/8.txt
:linenos:
:startinline: true
:start-after: // start code
:end-before: // end code
:hl_lines: 13
.. raw:: pdf
PageBreak
.. code-block:: php
:include: code/8.txt
:start-after: // start output
:end-before: // end output
.. code-block:: php
:include: code/8.txt
:start-after: // start output
:end-before: // end output
.. raw:: pdf
PageBreak
.. code-block:: php
:include: code/9.txt
:linenos:
:startinline: true
:start-after: // start code
:end-before: // end code
|
.. code-block:: yaml
:include: code/9.txt
:linenos:
:start-after: // start services
:end-before: // end services
.. raw:: pdf
PageBreak
.. code-block:: yaml
:include: code/9.txt
:start-after: // start output
:end-before: // end output
.. raw:: pdf
PageBreak
.. code-block:: php
:include: code/10.txt
:linenos:
:startinline: true
:start-after: // start code
:end-before: // end code
:hl_lines: 5,6,7
.. raw:: pdf
PageBreak
.. code-block:: plain
:include: code/10.txt
:start-after: // start output
:end-before: // end output
.. raw:: pdf
PageBreak
.. code-block:: php
:include: code/11.txt
:linenos:
:startinline: true
:start-after: // start code 1
:end-before: // end code 1
.. raw:: pdf
PageBreak
.. code-block:: php
:include: code/11.txt
:linenos:
:startinline: true
:start-after: // start code 1
:end-before: // end code 1
:hl_lines: 10,11,12
.. raw:: pdf
PageBreak
.. code-block:: php
:include: code/11.txt
:linenos:
:startinline: true
:start-after: // start code 1
:end-before: // end code 1
:hl_lines: 8,13
.. raw:: pdf
PageBreak
.. code-block:: php
:include: code/11.txt
:linenos:
:startinline: true
:start-after: // start code 2
:end-before: // end code 2
:hl_lines: 1,3
.. raw:: pdf
PageBreak
.. code-block:: php
:include: code/11.txt
:linenos:
:startinline: true
:start-after: // start code 2
:end-before: // end code 2
:hl_lines: 2
.. raw:: pdf
PageBreak
.. code-block:: yaml
:include: code/12.txt
:linenos:
:start-after: // start services1
:end-before: // end services1
|
.. code-block:: yaml
:include: code/12.txt
:linenos:
:start-after: // start services2
:end-before: // end services2
.. raw:: pdf
TextAnnotation "Declare the Repository as a service."
TextAnnotation "Autowire or declare dependencies explicitly."
.. raw:: pdf
PageBreak
.. code-block:: plain
:include: code/12.txt
:start-after: // start output
:end-before: // end output
|
.. code-block:: php
:include: code/13.txt
:linenos:
:startinline: true
:start-after: // start test
:end-before: // end test
.. raw:: pdf
PageBreak
.. code-block:: plain
:include: code/13.txt
:start-after: // start output
:end-before: // end output
.. raw:: pdf
PageBreak
.. code-block:: php
:include: code/14.txt
:linenos:
:startinline: true
:start-after: // start test
:end-before: // end test
:hl_lines: 1
.. raw:: pdf
PageBreak
.. code-block:: php
:include: code/14.txt
:linenos:
:startinline: true
:start-after: // start test
:end-before: // end test
:hl_lines: 5
.. raw:: pdf
PageBreak
.. code-block:: plain
:include: code/14.txt
:start-after: // start output
:end-before: // end output
.. raw:: pdf
PageBreak
.. code-block:: php
:include: code/15.txt
:linenos:
:start-after: // start test
:end-before: // end test
:startinline: true
.. raw:: pdf
TextAnnotation "We know we're getting a node, but are we getting the correct node?"
PageBreak
.. code-block:: php
:include: code/15.txt
:linenos:
:startinline: true
:start-after: // start test
:end-before: // end test
:hl_lines: 1,2,3,4
.. raw:: pdf
TextAnnotation "Create a node with a specific title."
PageBreak
.. code-block:: php
:include: code/15.txt
:linenos:
:startinline: true
:end-before: // end test
:start-after: // start test
:hl_lines: 10,11
.. raw:: pdf
PageBreak
.. code-block:: php
:include: code/15.txt
:startinline: true
:linenos:
:start-after: // start test
:end-before: // end test
:hl_lines: 13,14,15
.. raw:: pdf
TextAnnotation "Ensure the node is an article and has the correct title."
PageBreak
.. Published or unpublished nodes.
.. code-block:: php
:include: code/16.txt
:linenos:
:startinline: true
:start-after: // start test
:end-before: // end test
.. raw:: pdf
PageBreak
.. code-block:: php
:include: code/16.txt
:linenos:
:startinline: true
:start-after: // start test
:end-before: // end test
:hl_lines: 2,4,6
.. raw:: pdf
PageBreak
.. code-block:: php
:include: code/16.txt
:linenos:
:startinline: true
:start-after: // start test
:end-before: // end test
:hl_lines: 3,5
.. raw:: pdf
PageBreak
.. code-block:: php
:include: code/16.txt
:linenos:
:startinline: true
:start-after: // start test
:end-before: // end test
:hl_lines: 12
.. raw:: pdf
TextAnnotation "We should only have three published articles."
PageBreak
.. code-block:: plain
:include: code/16.txt
:start-after: // start output
:end-before: // end output
.. raw:: pdf
PageBreak
.. code-block:: php
:include: code/17.txt
:linenos:
:startinline: true
:start-after: // start code
:end-before: // end code
|
.. code-block:: plain
:include: code/17.txt
:start-after: // start output
:end-before: // end output
.. raw:: pdf
PageBreak
.. Ensure articles are ordered correctly.
.. code-block:: php
:include: code/18.txt
:linenos:
:startinline: true
:start-after: // start test
:end-before: // end test
.. raw:: pdf
PageBreak
.. code-block:: php
:include: code/18.txt
:linenos:
:startinline: true
:start-after: // start test
:end-before: // end test
:hl_lines: 3,5,7,9,11
.. raw:: pdf
PageBreak
.. code-block:: php
:include: code/18.txt
:linenos:
:startinline: true
:start-after: // start test
:end-before: // end test
:hl_lines: 16
.. raw:: pdf
PageBreak
.. code-block:: plain
:include: code/18.txt
:start-after: // start output
:end-before: // end output
.. raw:: pdf
PageBreak
.. code-block:: php
:include: code/19.txt
:linenos:
:startinline: true
:start-after: // start code
:end-before: // end code
.. raw:: pdf
PageBreak
.. code-block:: php
:include: code/19.txt
:linenos:
:startinline: true
:start-after: // start code
:end-before: // end code
:hl_lines: 2,3,4
.. raw:: pdf
PageBreak
.. code-block:: php
:include: code/19.txt
:linenos:
:startinline: true
:start-after: // start code
:end-before: // end code
:hl_lines: 6,7
.. raw:: pdf
TextAnnotation "Spaceship operator!"
PageBreak
.. code-block:: php
:include: code/19.txt
:linenos:
:startinline: true
:start-after: // start code
:end-before: // end code
:hl_lines: 9
.. raw:: pdf
PageBreak
.. code-block:: plain
:include: code/19.txt
:start-after: // start output
:end-before: // end output

View file

@ -0,0 +1,23 @@
.. page:: titlePage
.. class:: centredtitle
Demo
.. raw:: pdf
PageBreak imagePage
|
.. image:: images/demo.png
:width: 14cm
.. raw:: pdf
PageBreak
|
.. image:: images/demo2.png
:width: 14cm

View file

@ -0,0 +1,134 @@
.. page:: titlePage
.. class:: centredtitle
Example
.. page:: imagePage
.. image:: images/broadbean-website.png
:width: 20cm
.. page:: standardPage
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.
.. raw:: pdf
TextAnnotation "Jobs added to a different system by the client, data POSTed to Drupal."
TextAnnotation "Job applicants would visit the job on the Drupal site, click the application URL and go to another (CRM) system to apply."
TextAnnotation "Client wanted to be able to specify the Drupal path in advance."
.. page:: imagePage
|
|
.. image:: images/broadbean-drupal-flow-2.png
:width: 20cm
.. page:: standardPage
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``
.. raw:: pdf
TextAnnotation "Required field missing."
TextAnnotation "Incorrect branch name."
Incoming data
=============
.. code-block:: php
:include: example/incoming.txt
:startinline: true
Incoming data
=============
.. code-block:: php
:include: example/incoming.txt
:hl_lines: 2
:startinline: true
Incoming data
=============
.. code-block:: php
:include: example/incoming.txt
:hl_lines: 3, 4
:startinline: true
Incoming data
=============
.. code-block:: php
:include: example/incoming.txt
:hl_lines: 5
:startinline: true
Incoming data
=============
.. code-block:: php
:include: example/incoming.txt
:hl_lines: 6, 7, 8, 9, 10
:startinline: true
Incoming data
=============
.. code-block:: php
:include: example/incoming.txt
:hl_lines: 11
:startinline: true
.. raw:: pdf
TextAnnotation "Some of the information sent to our endpoint."
Implementation
==============
* If no error, create the job node, return OK response to Broadbean
* If an Exception is thrown, return an error code and message
.. raw:: pdf
TextAnnotation "Required field missing."
TextAnnotation "Branch name incorrect, Exception caught."
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
Results
=======
* 0 bugs!
* Easier to identify where issues occurred and responsibilities
* Reduced debugging time
.. raw:: pdf
TextAnnotation "Best case scenario."
TextAnnotation "Just because there are tests, it doesn't mean that everything works and everything's passing - just the tests that you wrote are passing."

View file

@ -0,0 +1,13 @@
$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',
// ...
];

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

View file

@ -0,0 +1,490 @@
.. footer:: @opdavies
TDD: Test Driven Drupal
#######################
|
.. class:: titleslideinfo
Oliver Davies (@opdavies)
|
.. class:: titleslideinfo
https://opdavi.es/drupal-london
.. page:: titlePage
.. class:: centredtitle
Software Developer, Consultant, open-source maintainer
.. raw:: pdf
TextAnnotation "I develop and consult on Drupal applications for clients."
TextAnnotation "I contribute to and maintain open-source projects including Drupal core."
TextAnnotation "Different perspectives."
.. page:: imagePage
.. image:: images/timmillwood-ono.png
:width: 22cm
.. raw:: pdf
TextAnnotation "I saw this tweet by Tim Millwood and become the maintainer in 2012."
.. page:: imagePage
.. image:: images/override-node-options-1.png
:width: 18cm
.. raw:: pdf
PageBreak
TextAnnotation "These were the usage statistics from Drupal.org when I became the maintainer."
|
.. image:: images/override-node-options-2.png
:width: 22cm
|
.. image:: images/override-node-options-3.png
:width: 22cm
.. raw:: pdf
TextAnnotation "173rd most used module on Drupal.org"
TextAnnotation "~38,000 sites - ~13,000 D7 and ~24,000 D8/9/10"
.. image:: images/override-node-options-4.png
:width: 22cm
.. raw:: pdf
PageBreak
TextAnnotation "Had some existing tests, crucial to preventing regressions"
.. page:: standardPage
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
.. raw:: pdf
TextAnnotation "I don't want to break 38,000 Drupal sites when rolling a new release, or causing a regression in a client codebase."
TextAnnotation "TDD often results in writing less code as you're figuring things out whilst writing the test, only writing code that's needed for the tests."
TextAnnotation "Drupal core gates. Testing gate requires new tests for new features, failing test cases for bug fixes, and code coverage when refactoring code."
TextAnnotation "Same projects can work for Drupal 8, 9 and 10 etc."
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
.. raw:: pdf
TextAnnotation "Not speaking about Drupal 7 and SimpleTest in this talk, but a lot of the concepts are the same."
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
.. raw:: pdf
TextAnnotation "Tests per module."
TextAnnotation "PSR-4 autoloading."
TextAnnotation "Different to D7."
.. page:: titlePage
.. class:: centredtitle
Arrange, Act, Assert
.. raw:: pdf
TextAnnotation "What are the parts of a test?"
TextAnnotation ""
TextAnnotation "Set up the world, perform an action, then make assertions."
.. raw:: pdf
PageBreak
.. class:: centredtitle
Given, When, Then
.. raw:: pdf
TextAnnotation "Given the About page exists..."
TextAnnotation "When I go to that page..."
TextAnnotation "I should see 'About me' on the page."
.. page:: standardPage
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
.. raw:: pdf
TextAnnotation "Examples of some things that I tested on previous projects."
.. page:: imagePage
|
.. image:: images/matt-stauffer-tweet.png
:width: 20cm
.. page:: standardPage
What does a test look like?
===========================
.. code-block:: php
:include: code/1-example-test.txt
:linenos:
:startinline: true
What does a test look like?
===========================
.. code-block:: php
:include: code/1-example-test.txt
:linenos:
:startinline: true
:hl_lines: 1,2,3,13
What does a test look like?
===========================
.. code-block:: php
:include: code/1-example-test.txt
:linenos:
:startinline: true
:hl_lines: 5,6,7
What does a test look like?
===========================
.. code-block:: php
:include: code/1-example-test.txt
:linenos:
:startinline: true
:hl_lines: 9,11
What does a test look like?
===========================
.. code-block:: php
:include: code/1-example-test.txt
:linenos:
:startinline: true
:hl_lines: 10
Writing test methods
====================
.. code-block:: php
:include: code/2-test-methods.txt
:linenos:
:startinline: true
Writing test methods
====================
.. code-block:: php
:include: code/2-test-methods.txt
:hl_lines: 1
:linenos:
:startinline: true
Writing test methods
====================
.. code-block:: php
:include: code/2-test-methods.txt
:hl_lines: 3
:linenos:
:startinline: true
Writing test methods
====================
.. code-block:: php
:include: code/2-test-methods.txt
:hl_lines: 5, 6
:linenos:
:startinline: true
Types of Tests
==============
* **Functional/FunctionalJavascript** (web, browser, feature)
* **Kernel** (integration)
* **Unit**
.. raw:: pdf
TextAnnotation "Not just unit tests."
Functional Tests
================
* Tests end-to-end functionality
* UI testing
* Interacts with database
* Full Drupal installation
* Slower to run
* With/without JavaScript
.. raw:: pdf
TextAnnotation "Uses the `testing` profile with a fresh installation between tests."
Kernel tests
============
* Integration tests
* Can install modules, interact with services, container, database
* Minimal Drupal bootstrap
* Faster than functional tests
* More setup required
.. raw:: pdf
TextAnnotation "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
.. page:: titlePage
.. class:: centredtitle
Running Tests
.. page:: standardPage
Core script
===========
.. code-block:: shell
$ 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
Core script
===========
.. code-block:: shell
$ php web/core/scripts/run-tests.sh \
--module example \
--sqlite /dev/shm/test.sqlite \
--url http://web
.. raw:: pdf
PageBreak
.. code-block::
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
PHPUnit
=======
.. code-block:: shell
$ export SIMPLETEST_BASE_URL=http://web
$ web/vendor/bin/phpunit \
-c web/core \
modules/contrib/examples/modules/phpunit_example
.. raw:: pdf
TextAnnotation "Update the phpunit path and config file path for your project."
TextAnnotation "-c not needed if the phpunit.xml.dist or phpunit.xml is in the same directory."
.. raw:: pdf
PageBreak
.. code-block:: plain
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)
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"``
.. raw:: pdf
TextAnnotation "For core. For projects, I create a customised phpunit.xml.dist in my project."
.. include:: example.rst
Test Driven Development
=======================
* Write a failing test
* Write code until the test passes
* Refactor
* Repeat
.. raw:: pdf
TextAnnotation "Write enough of a test so that it fails."
TextAnnotation "Write enough code so that the test passes."
TextAnnotation "Refactor if needed."
TextAnnotation "Repeat."
.. page:: titlePage
.. class:: centredtitle
Red, Green, Refactor
.. page:: standardPage
Porting Modules From Drupal 7
=============================
- Make a new branch
- Add/update the tests
- Write code to make the tests pass
- Refactor
- Repeat
.. raw:: pdf
TextAnnotation "Similar to the TDD workflow."
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
.. raw:: pdf
TextAnnotation "Write the code in your test that you wish you had, and let the tests tell you what's missing."
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.
.. .. include:: demo.rst
.. include:: demo2.rst
.. page:: imagePage
.. image:: images/tawny-tweet-2.png
:width: 18cm
.. page:: standardPage
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/drupal-london
* https://www.oliverdavies.uk/atdc