diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2659611 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +composer.lock 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/views.view.blog.yml b/config/install/views.view.blog.yml new file mode 100644 index 0000000..783846f --- /dev/null +++ b/config/install/views.view.blog.yml @@ -0,0 +1,228 @@ +langcode: en +status: true +dependencies: + config: + - node.type.article + module: + - node + - user +id: blog +label: Blog +module: views +description: '' +tag: '' +base_table: node_field_data +base_field: nid +core: 8.x +display: + default: + display_plugin: default + id: default + display_title: Master + position: 0 + display_options: + access: + type: perm + options: + perm: 'access content' + cache: + type: tag + options: { } + query: + type: views_query + options: + disable_sql_rewrite: false + distinct: false + replica: false + query_comment: '' + query_tags: { } + exposed_form: + type: basic + options: + submit_button: Apply + reset_button: false + reset_button_label: Reset + exposed_sorts_label: 'Sort by' + expose_sort_order: true + sort_asc_label: Asc + sort_desc_label: Desc + pager: + type: mini + options: + items_per_page: 10 + offset: 0 + id: 0 + total_pages: null + expose: + items_per_page: false + items_per_page_label: 'Items per page' + items_per_page_options: '5, 10, 25, 50' + items_per_page_options_all: false + items_per_page_options_all_label: '- All -' + offset: false + offset_label: Offset + tags: + previous: ‹‹ + next: ›› + style: + type: default + options: + grouping: { } + row_class: '' + default_row_class: true + uses_fields: false + row: + type: fields + options: + inline: { } + separator: '' + hide_empty: false + default_field_elements: true + fields: + title: + id: title + table: node_field_data + field: title + entity_type: node + entity_field: title + label: '' + alter: + alter_text: false + make_link: false + absolute: false + trim: false + word_boundary: false + ellipsis: false + strip_tags: false + html: false + hide_empty: false + empty_zero: false + settings: + link_to_entity: true + plugin_id: field + relationship: none + group_type: group + admin_label: '' + exclude: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_alter_empty: true + click_sort_column: value + type: string + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + filters: + status: + value: '1' + table: node_field_data + field: status + plugin_id: boolean + entity_type: node + entity_field: status + id: status + expose: + operator: '' + group: 1 + type: + id: type + table: node_field_data + field: type + relationship: none + group_type: group + admin_label: '' + operator: in + value: + article: article + group: 1 + exposed: false + expose: + operator_id: '' + label: '' + description: '' + use_operator: false + operator: '' + identifier: '' + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + reduce: false + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + entity_type: node + entity_field: type + plugin_id: bundle + sorts: + created: + id: created + table: node_field_data + field: created + 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: { } + relationships: { } + arguments: { } + display_extenders: { } + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url.query_args + - 'user.node_grants:view' + - user.permissions + tags: { } + page_1: + display_plugin: page + id: page_1 + display_title: Page + position: 1 + display_options: + display_extenders: { } + path: blog + cache_metadata: + max-age: -1 + contexts: + - '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 53% rename from tdd_dublin.info.yml rename to tdd_blog.info.yml index 4698633..1cdb6c0 100644 --- a/tdd_dublin.info.yml +++ b/tdd_blog.info.yml @@ -1,4 +1,9 @@ -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: + - drupal:node + - drupal:views diff --git a/tests/src/Functional/PageListTest.php b/tests/src/Functional/PageListTest.php index 9c1d9e6..6205de1 100644 --- a/tests/src/Functional/PageListTest.php +++ b/tests/src/Functional/PageListTest.php @@ -1,24 +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'; + + public function testBlogPageExists() { + $this->drupalGet('blog'); + + $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)); + } + +}