diff --git a/.gitignore b/.gitignore
index bd3bc9a..cb76843 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,7 +4,7 @@
# rst2df.
/**/*.pdf
/**/*.rst.build_temp
-/dist/
+/dist/*.pdf
# pdfpc.
/**/*.pdfpc
diff --git a/dist/.keep b/dist/.keep
new file mode 100644
index 0000000..e69de29
diff --git a/run b/run
index 538318d..04a2ed1 100755
--- a/run
+++ b/run
@@ -7,8 +7,8 @@ RST_FILENAME=slides.rst
THUMBNAIL_FILENAME=thumbnail.jpg
function clean {
- mkdir -p dist
rm -fr dist/*
+ touch dist/.keep
find . \
-type f \( -name "${PDF_FILENAME}*" -o -name *.build_temp -o -name ${THUMBNAIL_FILENAME} \) \
@@ -34,7 +34,7 @@ function pdf:generate {
--fit-background-mode scale \
--font-path ../fonts \
--output "../dist/${DIRECTORY_NAME}.pdf" \
- --stylesheets ../styles/style-light,tango \
+ --stylesheets ../styles/style-light,vs \
"${@}"
popd
diff --git a/test-driven-drupal/code/1.txt b/test-driven-drupal/code/1.txt
new file mode 100644
index 0000000..65a11bd
--- /dev/null
+++ b/test-driven-drupal/code/1.txt
@@ -0,0 +1,6 @@
+# drupalcon.info.yml
+
+name: DrupalCon demo
+type: module
+core_version_requirement: ^10
+package: DrupalCon
diff --git a/test-driven-drupal/code/10.txt b/test-driven-drupal/code/10.txt
new file mode 100644
index 0000000..87cfd42
--- /dev/null
+++ b/test-driven-drupal/code/10.txt
@@ -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.
diff --git a/test-driven-drupal/code/11.txt b/test-driven-drupal/code/11.txt
new file mode 100644
index 0000000..53a6900
--- /dev/null
+++ b/test-driven-drupal/code/11.txt
@@ -0,0 +1,48 @@
+nodeStorage = $this->entityTypeManager->getStorage('node');
+ }
+
+
+
+ public function getAll(): array {
+ return $this->nodeStorage->loadMultiple();
+ }
+
+}
+// end code
+
+---
+
+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.
diff --git a/test-driven-drupal/code/12.txt b/test-driven-drupal/code/12.txt
new file mode 100644
index 0000000..2e1c9ef
--- /dev/null
+++ b/test-driven-drupal/code/12.txt
@@ -0,0 +1,34 @@
+// start services
+# drupalcon.services.yml
+
+services:
+ Drupal\drupalcon\Repository\ArticleRepository:
+ autowire: true
+// end services
+
+---
+
+// 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.
diff --git a/test-driven-drupal/code/13.txt b/test-driven-drupal/code/13.txt
new file mode 100644
index 0000000..13097c7
--- /dev/null
+++ b/test-driven-drupal/code/13.txt
@@ -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.
diff --git a/test-driven-drupal/code/14.txt b/test-driven-drupal/code/14.txt
new file mode 100644
index 0000000..b4e7e99
--- /dev/null
+++ b/test-driven-drupal/code/14.txt
@@ -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
diff --git a/test-driven-drupal/code/15.txt b/test-driven-drupal/code/15.txt
new file mode 100644
index 0000000..984fee1
--- /dev/null
+++ b/test-driven-drupal/code/15.txt
@@ -0,0 +1,49 @@
+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)
diff --git a/test-driven-drupal/code/16.txt b/test-driven-drupal/code/16.txt
new file mode 100644
index 0000000..e25b91c
--- /dev/null
+++ b/test-driven-drupal/code/16.txt
@@ -0,0 +1,37 @@
+public function only_published_articles_are_returned() {
+// start test
+ $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.
diff --git a/test-driven-drupal/code/17.txt b/test-driven-drupal/code/17.txt
new file mode 100644
index 0000000..843b65e
--- /dev/null
+++ b/test-driven-drupal/code/17.txt
@@ -0,0 +1,37 @@
+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
diff --git a/test-driven-drupal/code/18.txt b/test-driven-drupal/code/18.txt
new file mode 100644
index 0000000..3959d8a
--- /dev/null
+++ b/test-driven-drupal/code/18.txt
@@ -0,0 +1,54 @@
+public function nodes_are_ordered_by_date_and_returned_newest_first(): void {
+// start test
+ $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
diff --git a/test-driven-drupal/code/19.txt b/test-driven-drupal/code/19.txt
new file mode 100644
index 0000000..054b6f4
--- /dev/null
+++ b/test-driven-drupal/code/19.txt
@@ -0,0 +1,44 @@
+nodeStorage = $this->entityTypeManager->getStorage('node');
+ }
+
+ public function getAll(): array {
+// start code
+ $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
diff --git a/test-driven-drupal/code/2.txt b/test-driven-drupal/code/2.txt
new file mode 100644
index 0000000..0528d63
--- /dev/null
+++ b/test-driven-drupal/code/2.txt
@@ -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 = [];
+
+}
diff --git a/test-driven-drupal/code/3.txt b/test-driven-drupal/code/3.txt
new file mode 100644
index 0000000..d98b96f
--- /dev/null
+++ b/test-driven-drupal/code/3.txt
@@ -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
diff --git a/test-driven-drupal/code/4.txt b/test-driven-drupal/code/4.txt
new file mode 100644
index 0000000..0dc2a19
--- /dev/null
+++ b/test-driven-drupal/code/4.txt
@@ -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.
diff --git a/test-driven-drupal/code/5.txt b/test-driven-drupal/code/5.txt
new file mode 100644
index 0000000..73ccc4a
--- /dev/null
+++ b/test-driven-drupal/code/5.txt
@@ -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.
diff --git a/test-driven-drupal/code/6.txt b/test-driven-drupal/code/6.txt
new file mode 100644
index 0000000..e62f1a8
--- /dev/null
+++ b/test-driven-drupal/code/6.txt
@@ -0,0 +1,25 @@
+// start code
+// src/Controller/BlogPageController.php
+
+namespace Drupal\drupalcon\Controller;
+
+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
diff --git a/test-driven-drupal/code/7.txt b/test-driven-drupal/code/7.txt
new file mode 100644
index 0000000..f52ccf9
--- /dev/null
+++ b/test-driven-drupal/code/7.txt
@@ -0,0 +1,40 @@
+// start test
+/** @test */
+public function it_loads_the_blog_page(): void {
+ $this->drupalGet('/blog');
+
+ $session = $this->assertSession();
+ $session->statusCodeEquals(200);
+
+ $session->responseContains('
Blog
');
+ $session->pageTextContains('Welcome to my blog!');
+}
+// end test
+
+// 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 output
+. 1 / 1 (100%)
+
+Time: 00:01.911, Memory: 6.00 MB
+
+OK (1 test, 3 assertions)
+// end output
+
+Task completed in 0m2.124s
diff --git a/test-driven-drupal/code/8.txt b/test-driven-drupal/code/8.txt
new file mode 100644
index 0000000..56976dd
--- /dev/null
+++ b/test-driven-drupal/code/8.txt
@@ -0,0 +1,46 @@
+// 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);
+
+ $articles = $repository->getAll();
+
+ $this->assertCount(1, $articles);
+ }
+// 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
diff --git a/test-driven-drupal/code/9.txt b/test-driven-drupal/code/9.txt
new file mode 100644
index 0000000..c7ecdde
--- /dev/null
+++ b/test-driven-drupal/code/9.txt
@@ -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
diff --git a/test-driven-drupal/demo.rst b/test-driven-drupal/demo.rst
new file mode 100644
index 0000000..6ef6cdc
--- /dev/null
+++ b/test-driven-drupal/demo.rst
@@ -0,0 +1,337 @@
+.. page:: titlePage
+
+.. class:: centredtitle
+
+Demo: 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
+
+.. page::
+
+.. Creating the test class.
+
+.. code-block:: php
+ :include: code/2.txt
+ :linenos:
+ :startinline: true
+
+.. page::
+
+.. Adding the first test.
+
+.. code-block:: php
+ :end-before: // end test
+ :include: code/3.txt
+ :linenos:
+ :startinline: true
+
+.. page::
+
+.. code-block:: plain
+ :end-before: // end output
+ :include: code/3.txt
+ :start-after: // start output
+ :startinline: true
+
+.. page::
+
+.. code-block:: yaml
+ :end-before: // end routing
+ :include: code/3.txt
+ :linenos:
+ :start-after: // start routing
+
+.. page::
+
+.. code-block:: plain
+ :end-before: // end output
+ :include: code/3.txt
+ :start-after: // start output
+ :startinline: true
+
+.. raw:: pdf
+
+ TextAnnotation "Same result as the module isn't enabled."
+
+.. page::
+
+.. code-block:: php
+ :include: code/4.txt
+ :end-before: // output
+ :linenos:
+ :startinline: true
+
+|
+
+.. code-block:: plain
+ :include: code/4.txt
+ :end-before: // end output
+ :start-after: // output
+ :startinline: true
+
+.. page::
+
+.. Enable the node module.
+
+.. code-block:: php
+ :include: code/5.txt
+ :linenos:
+ :end-before: // end code
+ :startinline: true
+
+|
+
+.. code-block:: php
+ :include: code/5.txt
+ :end-before: // end output
+ :start-after: // start output
+
+.. page::
+
+.. Create the Controller.
+
+.. code-block:: php
+ :end-before: // end code
+ :include: code/6.txt
+ :linenos:
+ :startinline: true
+ :start-after: // start code
+
+.. page::
+
+.. code-block:: plain
+ :end-before: // end output
+ :include: code/6.txt
+ :start-after: // start output
+
+.. page::
+
+.. code-block:: php
+ :end-before: // end test
+ :include: code/7.txt
+ :linenos:
+ :startinline: true
+ :start-after: // start test
+
+.. raw:: pdf
+
+ TextAnnotation "Adding extra assertions."
+
+.. page::
+
+.. code-block:: php
+ :end-before: // end code
+ :include: code/7.txt
+ :linenos:
+ :startinline: true
+ :start-after: // start code
+
+.. page::
+
+.. code-block:: plain
+ :end-before: // end output
+ :include: code/7.txt
+ :start-after: // start output
+
+.. page::
+
+.. code-block:: php
+ :end-before: // end code
+ :include: code/8.txt
+ :linenos:
+ :start-after: // start code
+ :startinline: true
+
+
+.. code-block:: php
+ :end-before: // end output
+ :include: code/8.txt
+ :start-after: // start output
+
+.. page::
+
+.. code-block:: php
+ :end-before: // end code
+ :include: code/9.txt
+ :linenos:
+ :start-after: // start code
+ :startinline: true
+
+|
+
+.. code-block:: yaml
+ :end-before: // end services
+ :include: code/9.txt
+ :linenos:
+ :start-after: // start services
+
+.. page::
+
+.. code-block:: yaml
+ :end-before: // end output
+ :include: code/9.txt
+ :start-after: // start output
+
+.. page::
+
+.. code-block:: php
+ :end-before: // end code
+ :include: code/10.txt
+ :linenos:
+ :start-after: // start code
+ :startinline: true
+
+.. page::
+
+.. code-block:: plain
+ :end-before: // end output
+ :include: code/10.txt
+ :start-after: // start output
+
+.. page::
+
+.. code-block:: php
+ :end-before: // end code
+ :include: code/11.txt
+ :linenos:
+ :startinline: true
+ :start-after: // start code
+
+.. code-block:: yaml
+ :end-before: // end services
+ :include: code/12.txt
+ :linenos:
+ :start-after: // start services
+
+.. page::
+
+.. code-block:: plain
+ :end-before: // end output
+ :include: code/12.txt
+ :start-after: // start output
+
+|
+
+.. code-block:: php
+ :end-before: // end test
+ :include: code/13.txt
+ :linenos:
+ :start-after: // start test
+ :startinline: true
+
+.. page::
+
+.. code-block:: plain
+ :end-before: // end output
+ :include: code/13.txt
+ :start-after: // start output
+
+.. page::
+
+.. code-block:: php
+ :end-before: // end test
+ :include: code/14.txt
+ :linenos:
+ :start-after: // start test
+ :startinline: true
+
+.. page::
+
+.. code-block:: plain
+ :end-before: // end output
+ :include: code/14.txt
+ :start-after: // start output
+
+.. page::
+
+
+.. code-block:: php
+ :end-before: // end test
+ :include: code/15.txt
+ :linenos:
+ :start-after: // start test
+ :startinline: true
+
+.. page::
+
+.. code-block:: php
+ :end-before: // end test
+ :include: code/16.txt
+ :linenos:
+ :start-after: // start test
+ :startinline: true
+
+.. page::
+
+.. code-block:: plain
+ :end-before: // end output
+ :include: code/16.txt
+ :start-after: // start output
+
+.. page::
+
+.. code-block:: php
+ :end-before: // end code
+ :include: code/17.txt
+ :linenos:
+ :start-after: // start code
+ :startinline: true
+
+|
+
+.. code-block:: plain
+ :end-before: // end output
+ :include: code/17.txt
+ :start-after: // start output
+
+.. page::
+
+.. code-block:: php
+ :end-before: // end test
+ :include: code/18.txt
+ :linenos:
+ :start-after: // start test
+ :startinline: true
+
+.. page::
+
+.. code-block:: plain
+ :end-before: // end output
+ :include: code/18.txt
+ :start-after: // start output
+
+.. page::
+
+.. code-block:: php
+ :end-before: // end code
+ :include: code/19.txt
+ :linenos:
+ :start-after: // start code
+ :startinline: true
+
+|
+
+.. code-block:: plain
+ :end-before: // end output
+ :include: code/19.txt
+ :start-after: // start output
+
diff --git a/test-driven-drupal/example.rst b/test-driven-drupal/example.rst
new file mode 100644
index 0000000..97184ff
--- /dev/null
+++ b/test-driven-drupal/example.rst
@@ -0,0 +1,107 @@
+.. 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
+ :startinline: true
+
+ $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',
+ // ...
+ ];
+
+.. raw:: pdf
+
+ TextAnnotation "Some pf 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."
diff --git a/test-driven-drupal/images/override-node-options-1.png b/test-driven-drupal/images/override-node-options-1.png
new file mode 100644
index 0000000..2b69070
Binary files /dev/null and b/test-driven-drupal/images/override-node-options-1.png differ
diff --git a/test-driven-drupal/images/override-node-options-2.png b/test-driven-drupal/images/override-node-options-2.png
new file mode 100644
index 0000000..5d876b0
Binary files /dev/null and b/test-driven-drupal/images/override-node-options-2.png differ
diff --git a/test-driven-drupal/images/override-node-options-3.png b/test-driven-drupal/images/override-node-options-3.png
new file mode 100644
index 0000000..07e9ba9
Binary files /dev/null and b/test-driven-drupal/images/override-node-options-3.png differ
diff --git a/test-driven-drupal/slides.rst b/test-driven-drupal/slides.rst
index e8b3e6f..d0a7645 100644
--- a/test-driven-drupal/slides.rst
+++ b/test-driven-drupal/slides.rst
@@ -7,18 +7,23 @@ TDD: Test Driven Drupal
.. class:: titleslideinfo
-Oliver Davies, Inviqa
+Oliver Davies (@opdavies)
+
+|
+.. class:: centred
+
+https://opdavi.es/tdd-test-driven-drupal
.. page:: titlePage
.. class:: centredtitle
-Software Engineer, open-source maintainer and contributor
+Software Engineer, Full Stack Development Consultant, Open-Source Maintainer
.. raw:: pdf
- TextAnnotation "I develop Drupal applications for clients, including custom modules and themes."
- TextAnnotation "I contribute to open source projects including Drupal core."
+ 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
@@ -29,21 +34,30 @@ Software Engineer, open-source maintainer and contributor
.. raw:: pdf
TextAnnotation "Become maintainer in 2012"
- TextAnnotation "~223 most used module on Drupal.org"
- TextAnnotation "~30,000 sites - ~20,000 D7 and ~10,000 D8/9"
-
- TextAnnotation "Had some existing tests, crucial to preventing regressions"
.. page:: imagePage
-.. image:: images/override-node-options-2012-4.png
+.. image:: images/override-node-options-1.png
:width: 18cm
.. page::
-.. image:: images/override-node-options-2020-2.png
+|
+
+.. 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"
+ TextAnnotation "Had some existing tests, crucial to preventing regressions"
+
.. page:: standardPage
Why write tests?
@@ -59,10 +73,10 @@ Why write tests?
.. raw:: pdf
- TextAnnotation "I don't want to break 30,000 Drupal sites when rolling a new release, or causing a regression in a client codebase."
+ 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 and 9, and in theory 10."
+ TextAnnotation "Same projects can work for Drupal 8, 9 and 10 etc."
Testing in Drupal
=================
@@ -71,8 +85,8 @@ Testing in Drupal
* **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
-Writing Tests (Drupal 8/9)
-==========================
+Writing PHPUnit Tests for Drupal
+================================
* PHP class with ``.php`` extension
* ``tests/src`` directory within each module
@@ -83,6 +97,7 @@ Writing Tests (Drupal 8/9)
.. raw:: pdf
+ TextAnnotation "Tests per module."
TextAnnotation "PSR-4 autoloading."
TextAnnotation "Different to D7."
@@ -92,6 +107,10 @@ Writing Tests (Drupal 8/9)
Arrange, Act, Assert
+.. raw:: pdf
+
+ TextAnnotation "Set up the world, perform an action, then make assertions."
+
.. page::
.. class:: centredtitle
@@ -119,10 +138,12 @@ What to test?
.. raw:: pdf
- TextAnnotation "Examples of some things that I tested on a previous project."
+ TextAnnotation "Examples of some things that I tested on previous projects."
.. page:: imagePage
+|
+
.. image:: images/matt-stauffer-tweet.png
:width: 20cm
@@ -151,6 +172,10 @@ Types of Tests
* **Kernel** (integration)
* **Unit**
+.. raw:: pdf
+
+ TextAnnotation "Not just unit tests."
+
Functional Tests
================
@@ -197,26 +222,80 @@ Core script
.. code-block:: shell
- $ php core/scripts/run-tests.sh
+ $ php web/core/scripts/run-tests.sh
- $ php core/scripts/run-tests.sh --module example
+ $ php web/core/scripts/run-tests.sh \
+ --all
- $ php core/scripts/run-tests.sh --class ExampleTest
+ $ 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
- $ vendor/bin/phpunit \
- -c core \
- modules/contrib/examples/phpunit_example
+ $ 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
===========================
@@ -229,111 +308,11 @@ Creating a phpunit.xml file
- ``SIMPLETEST_BASE_URL``, ``SIMPLETEST_DB``, ``BROWSERTEST_OUTPUT_DIRECTORY``
- ``stopOnFailure="true"``
-.. 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."
+ TextAnnotation "For core. For projects, I create a customised phpunit.xml.dist in my project."
-.. 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
- :startinline: true
-
- $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',
- // ...
- ];
-
-.. raw:: pdf
-
- TextAnnotation "Some pf 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
-* Added more tests for any bugs to prevent regressions
-
-.. 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."
+.. include:: example.rst
Test Driven Development
=======================
@@ -384,27 +363,19 @@ How I Write Tests - "Outside In"
TextAnnotation "Write the code in your test that you wish you had, and let the tests tell you what's missing."
-.. page:: titlePage
-.. class:: centredtitle
+How I Write Tests - "Outside In"
+================================
-Demo: Building a blog module
+* Functional - 57 tests, 180 assertions
+* Kernel - 38 tests, 495 assertions
+* Unit - 5 tests, 18 assertions
-.. page:: standardPage
+|
-Acceptance criteria
-===================
+Run in 2-3 minutes in a CI pipeline with GitHub Actions.
-- 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
+.. include:: demo.rst
.. page:: imagePage
@@ -421,11 +392,13 @@ Thanks!
References:
-* https://opdavi.es/testing-workshop
-* https://testdrivendrupal.com
+* https://phpunit.de
+* https://docs.phpunit.de
+* https://www.drupal.org/docs/automated-testing
|
Me:
* https://www.oliverdavies.uk
+* https://www.oliverdavies.uk/atdc