diff --git a/README.md b/README.md index 4125f53..dc05439 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,31 @@ -# TDD Dublin demo module +# TDD Example Drupal 8 Blog Module -A demo module to accompany my [TDD Test Driven Drupal][0] talk at DrupalCamp +A demo module to accompany my [TDD - Test Driven Drupal][0] talk, originally for DrupalCamp Dublin 2017. +In order to see my workflow of writing comments first, converting them into +failing tests, and then writing the implementation code to make them pass, you +can see the [list of previous commits][1] and see each step taken, as well as +[the tags][2] that identify the commits when each failing test is added and +then subsequently passes. + ## Acceptance Criteria This module will be used to demonstrate how to take a test-driven approach to develop a module to the following acceptance criteria: - As a site visitor -- I want to see a list of all published pages at `/pages` -- Ordered alphabetically by title +- I want to see a list of all published blog posts at `/blog` +- Ordered by post date, with the newest posts first + +## Installation + +Within your Drupal 8 site: + +```bash +cd modules +git clone git@github.com:opdavies/drupal-module-tdd-blog.git tdd_blog +``` ## Running the Tests @@ -18,23 +33,23 @@ These tests are functional tests based on the `BrowserTestBase` class so need to be executed with PHPUnit (which is required in core's `composer.json` file). The path to your `vendor` directory may be different depending on your setup. -Because of autoloading, you will need to be inside Drupal's `core` subdirectory -when running the tests for them to execute successfully. +Because of autoloading, you will either need to be inside Drupal's `core` subdirectory +, or add `-c core` to the PHPUnit command when running the tests for them to execute successfully. This also assumes that the module is within a `modules/custom` directory and -named `tdd_dublin` as per the repository name. +named `tdd_blog` as per the repository name. ``` -cd core - -../vendor/bin/phpunit ../modules/custom/tdd_dublin +vendor/bin/phpunit -c core modules/custom/tdd_blog ``` You can use PHPUnit's `--filter` option to specify a single test method to run, rather than all of the tests within the module. For example: ``` -../vendor/bin/phpunit ../modules/custom/tdd_dublin --filter=testOnlyPublishedPagesAreShown +vendor/bin/phpunit -c core modules/custom/tdd_blog --filter=testOnlyPublishedPagesAreShown ``` [0]: https://www.oliverdavies.uk/talks/tdd-test-driven-drupal +[1]: https://github.com/opdavies/drupal-module-tdd-blog/commits/HEAD +[2]: https://github.com/opdavies/drupal-module-tdd-blog/tags diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..aa7e974 --- /dev/null +++ b/composer.json @@ -0,0 +1,4 @@ +{ + "name": "drupal/tdd_blog", + "type": "drupal-custom-module" +} diff --git a/config/install/node.type.article.yml b/config/install/node.type.article.yml new file mode 100644 index 0000000..1fd439c --- /dev/null +++ b/config/install/node.type.article.yml @@ -0,0 +1,10 @@ +langcode: en +status: true +dependencies: { } +name: Article +type: article +description: 'Use <em>articles</em> for time-sensitive content like news, press releases or blog posts.' +help: '' +new_revision: true +preview_mode: 1 +display_submitted: true diff --git a/config/install/node.type.page.yml b/config/install/node.type.page.yml deleted file mode 100644 index a6ddd46..0000000 --- a/config/install/node.type.page.yml +++ /dev/null @@ -1,12 +0,0 @@ -langcode: en -status: true -dependencies: { } -_core: - default_config_hash: KuyA4NHPXcmKAjRtwa0vQc2ZcyrUJy6IlS2TAyMNRbc -name: 'Basic page' -type: page -description: 'Use <em>basic pages</em> for your static content, such as an ''About us'' page.' -help: '' -new_revision: true -preview_mode: 1 -display_submitted: false diff --git a/config/install/views.view.pages.yml b/config/install/views.view.blog.yml similarity index 91% rename from config/install/views.view.pages.yml rename to config/install/views.view.blog.yml index d09a6d2..783846f 100644 --- a/config/install/views.view.pages.yml +++ b/config/install/views.view.blog.yml @@ -2,12 +2,12 @@ langcode: en status: true dependencies: config: - - node.type.page + - node.type.article module: - - node - - user -id: pages -label: pages + - node + - user +id: blog +label: Blog module: views description: '' tag: '' @@ -147,7 +147,7 @@ display: admin_label: '' operator: in value: - page: page + article: article group: 1 exposed: false expose: @@ -183,17 +183,17 @@ display: id: created table: node_field_data field: created - order: DESC - entity_type: node - entity_field: created - plugin_id: date relationship: none group_type: group admin_label: '' + order: ASC exposed: false expose: label: '' granularity: second + entity_type: node + entity_field: created + plugin_id: date header: { } footer: { } empty: { } @@ -203,11 +203,11 @@ display: cache_metadata: max-age: -1 contexts: - - 'languages:language_content' - - 'languages:language_interface' - - url.query_args - - 'user.node_grants:view' - - user.permissions + - 'languages:language_content' + - 'languages:language_interface' + - url.query_args + - 'user.node_grants:view' + - user.permissions tags: { } page_1: display_plugin: page @@ -216,13 +216,13 @@ display: position: 1 display_options: display_extenders: { } - path: pages + path: blog cache_metadata: max-age: -1 contexts: - - 'languages:language_content' - - 'languages:language_interface' - - url.query_args - - 'user.node_grants:view' - - user.permissions + - 'languages:language_content' + - 'languages:language_interface' + - url.query_args + - 'user.node_grants:view' + - user.permissions tags: { } diff --git a/tdd_dublin.info.yml b/tdd_blog.info.yml similarity index 75% rename from tdd_dublin.info.yml rename to tdd_blog.info.yml index 448351b..1cdb6c0 100644 --- a/tdd_dublin.info.yml +++ b/tdd_blog.info.yml @@ -1,6 +1,7 @@ -name: 'TDD Dublin' +name: 'TDD Blog' description: 'A demo module for DrupalCamp Dublin to show test driven module development.' core: 8.x +core_version_requirement: ^8 || ^9 type: module dependencies: diff --git a/tests/src/Functional/PageListTest.php b/tests/src/Functional/PageListTest.php index 93f0e0a..6205de1 100644 --- a/tests/src/Functional/PageListTest.php +++ b/tests/src/Functional/PageListTest.php @@ -1,59 +1,20 @@ <?php -namespace Drupal\Tests\tdd_dublin\Functional; +namespace Drupal\Tests\tdd_blog\Functional; use Drupal\Tests\BrowserTestBase; +use Symfony\Component\HttpFoundation\Response; class PageListTest extends BrowserTestBase { - /** - * {@inheritdoc} - */ - protected static $modules = ['tdd_dublin']; + protected static $modules = ['tdd_blog']; - /** - * Test that the pages listing page exists and is accessible. - */ - public function testListingPageExists() { - // Go to /pages and check that it is accessible by checking the status - // code. - $this->drupalGet('pages'); - $this->assertSession()->statusCodeEquals(200); - } + protected $defaultTheme = 'stark'; - /** - * Ensure that only the correct nodes are returned. - * - * Ensure that only published pages are returned by the view. Unpublished - * pages or content of different types should not be shown. - */ - public function testOnlyPublishedPagesAreShown() { - $this->drupalCreateContentType(['type' => 'article']); + public function testBlogPageExists() { + $this->drupalGet('blog'); - // This is a published page, so it should be visible. - $this->drupalCreateNode(['type' => 'page', 'status' => TRUE]); - - // This is an article, so it should not be visible. - $this->drupalCreateNode(['type' => 'article']); - - // This page is not published, so it should not be visible. - $this->drupalCreateNode(['type' => 'page', 'status' => FALSE]); - - // Rather than testing the rendered HTML, we are going to load the view - // results programmatically and run assertions against the data it returns. - // This makes it easier to test certain scenarios, and ensures that the - // test is future-proofed and won't fail at a later date due to a change in - // the presentation code. - $result = views_get_view_result('pages'); - - // $result contains an array of Drupal\views\ResultRow objects. We can use - // array_column to get the nid from each node and return them as an array. - $nids = array_column($result, 'nid'); - - // Only node 1 matches the criteria of being a published page, so only that - // node ID should be being returned from the view. assertEquals() can be - // used to compare the expected result to what is being returned. - $this->assertEquals([1], $nids); + $this->assertSession()->statusCodeEquals(Response::HTTP_OK); } } diff --git a/tests/src/Kernel/PageListTest.php b/tests/src/Kernel/PageListTest.php new file mode 100644 index 0000000..2e5cb48 --- /dev/null +++ b/tests/src/Kernel/PageListTest.php @@ -0,0 +1,99 @@ +<?php + +namespace Drupal\Tests\tdd_blog\Kernel; + +use Drupal\Core\Datetime\DrupalDateTime; +use Drupal\KernelTests\Core\Entity\EntityKernelTestBase; +use Drupal\Tests\node\Traits\NodeCreationTrait; +use Drupal\views\ResultRow; + +/** + * @group tdd_blog + */ +class PageListTest extends EntityKernelTestBase { + + use NodeCreationTrait; + + /** + * {@inheritdoc} + */ + public static $modules = [ + 'node', + 'tdd_blog', + 'views', + ]; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + $this->installEntitySchema('node'); + $this->installEntitySchema('user'); + + $this->installConfig(['filter', 'tdd_blog']); + } + + /** + * Ensure that only the correct nodes are returned. + * + * Ensure that only published pages are returned by the view. Unpublished + * pages or content of different types should not be shown. + */ + public function testOnlyPublishedArticlesAreShown() { + // This is a published article, so it should be visible. + $this->createNode(['type' => 'page', 'status' => TRUE]); + + // This is a page, so it should not be visible. + $this->createNode(['type' => 'article']); + + // This article is not published, so it should not be visible. + $this->createNode(['type' => 'article', 'status' => FALSE]); + + // Rather than testing the rendered HTML, we are going to load the view + // results programmatically and run assertions against the data it returns. + // This makes it easier to test certain scenarios, and ensures that the + // test is future-proofed and won't fail at a later date due to a change in + // the presentation code. + $nids = $this->getViewResults(); + + // Only node 1 matches the criteria of being a published page, so only that + // node ID should be being returned from the view. assertEquals() can be + // used to compare the expected result to what is being returned. + $this->assertEquals([2], $nids); + } + + /** + * Ensure that the results are ordered by title. + */ + public function testArticlesAreOrderedByDate() { + $this->createNode(['type' => 'article', 'created' => (new DrupalDateTime('+1 day'))->getTimestamp()]); + $this->createNode(['type' => 'article', 'created' => (new DrupalDateTime('+1 month'))->getTimestamp()]); + $this->createNode(['type' => 'article', 'created' => (new DrupalDateTime('+3 days'))->getTimestamp()]); + $this->createNode(['type' => 'article', 'created' => (new DrupalDateTime('+1 hour'))->getTimestamp()]); + + // Get the result data from the view. + $nids = $this->getViewResults(); + + // Compare the expected order based on the titles defined above to the + // ordered results from the view. + $this->assertEquals([4, 1, 3, 2], $nids); + } + + /** + * Load the view and get the results. + * + * @param string $view + * (optional) The name of the view. Defaults to 'blog'. + * + * @return array + * An array of returned entity IDs. + */ + private function getViewResults($view = 'blog') { + return array_map(function (ResultRow $result) { + return $result->_entity->id(); + }, views_get_view_result($view)); + } + +}