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 articles 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 basic pages 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 73% rename from config/install/views.view.pages.yml rename to config/install/views.view.blog.yml index bf293cd..783846f 100644 --- a/config/install/views.view.pages.yml +++ b/config/install/views.view.blog.yml @@ -1,11 +1,13 @@ langcode: en status: true dependencies: + config: + - node.type.article module: - - node - - user -id: pages -label: pages + - node + - user +id: blog +label: Blog module: views description: '' tag: '' @@ -136,22 +138,62 @@ display: 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 - 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: { } @@ -161,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 @@ -174,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 9c1d9e6..6205de1 100644 --- a/tests/src/Functional/PageListTest.php +++ b/tests/src/Functional/PageListTest.php @@ -1,24 +1,20 @@ 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 @@ +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)); + } + +}