diff --git a/.idea/oliverdavies-uk.iml b/.idea/oliverdavies-uk.iml
index 96482bc..2a5e0d6 100644
--- a/.idea/oliverdavies-uk.iml
+++ b/.idea/oliverdavies-uk.iml
@@ -8,6 +8,7 @@
       <sourceFolder url="file://$MODULE_DIR$/web/modules/custom/opd_talks/src" isTestSource="false" packagePrefix="Drupal\opd_talks" />
       <sourceFolder url="file://$MODULE_DIR$/web/modules/custom/opdavies_blog/tests/src" isTestSource="true" packagePrefix="Drupal\Tests\opdavies_blog" />
       <excludeFolder url="file://$MODULE_DIR$/vendor/asm89/stack-cors" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/beberlei/assert" />
       <excludeFolder url="file://$MODULE_DIR$/vendor/behat/mink" />
       <excludeFolder url="file://$MODULE_DIR$/vendor/behat/mink-browserkit-driver" />
       <excludeFolder url="file://$MODULE_DIR$/vendor/behat/mink-goutte-driver" />
diff --git a/.idea/php.xml b/.idea/php.xml
index d34eca7..bb00eb9 100644
--- a/.idea/php.xml
+++ b/.idea/php.xml
@@ -141,6 +141,12 @@
       <path value="$PROJECT_DIR$/vendor/cweagans/composer-patches" />
       <path value="$PROJECT_DIR$/vendor/laminas/laminas-zendframework-bridge" />
       <path value="$PROJECT_DIR$/vendor/laminas/laminas-stdlib" />
+      <path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-idn" />
+      <path value="$PROJECT_DIR$/vendor/symfony/polyfill-php80" />
+      <path value="$PROJECT_DIR$/vendor/laminas/laminas-feed" />
+      <path value="$PROJECT_DIR$/vendor/laminas/laminas-diactoros" />
+      <path value="$PROJECT_DIR$/vendor/laminas/laminas-escaper" />
+      <path value="$PROJECT_DIR$/vendor/beberlei/assert" />
     </include_path>
   </component>
   <component name="PhpProjectSharedConfiguration" php_language_level="7.4" />
diff --git a/composer.json b/composer.json
index 0cf5518..915c2c5 100644
--- a/composer.json
+++ b/composer.json
@@ -15,6 +15,7 @@
         }
     ],
     "require": {
+        "beberlei/assert": "^3.2",
         "composer/installers": "^1.2",
         "cweagans/composer-patches": "^1.6",
         "drupal/admin_toolbar": "^2.0",
diff --git a/composer.lock b/composer.lock
index e748675..5b794bc 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "4026a4ca20f7aa831743f07d0d3cc8d7",
+    "content-hash": "3aa40373d1c7d531970ee2137e660534",
     "packages": [
         {
             "name": "asm89/stack-cors",
@@ -58,6 +58,68 @@
             ],
             "time": "2019-12-24T22:41:47+00:00"
         },
+        {
+            "name": "beberlei/assert",
+            "version": "v3.2.7",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/beberlei/assert.git",
+                "reference": "d63a6943fc4fd1a2aedb65994e3548715105abcf"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/beberlei/assert/zipball/d63a6943fc4fd1a2aedb65994e3548715105abcf",
+                "reference": "d63a6943fc4fd1a2aedb65994e3548715105abcf",
+                "shasum": ""
+            },
+            "require": {
+                "ext-ctype": "*",
+                "ext-json": "*",
+                "ext-mbstring": "*",
+                "ext-simplexml": "*",
+                "php": "^7"
+            },
+            "require-dev": {
+                "friendsofphp/php-cs-fixer": "*",
+                "phpstan/phpstan-shim": "*",
+                "phpunit/phpunit": ">=6.0.0 <8"
+            },
+            "suggest": {
+                "ext-intl": "Needed to allow Assertion::count(), Assertion::isCountable(), Assertion::minCount(), and Assertion::maxCount() to operate on ResourceBundles"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Assert\\": "lib/Assert"
+                },
+                "files": [
+                    "lib/Assert/functions.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-2-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Benjamin Eberlei",
+                    "email": "kontakt@beberlei.de",
+                    "role": "Lead Developer"
+                },
+                {
+                    "name": "Richard Quadling",
+                    "email": "rquadling@gmail.com",
+                    "role": "Collaborator"
+                }
+            ],
+            "description": "Thin assertion library for input validation in business models.",
+            "keywords": [
+                "assert",
+                "assertion",
+                "validation"
+            ],
+            "time": "2019-12-19T17:51:41+00:00"
+        },
         {
             "name": "chi-teck/drupal-code-generator",
             "version": "1.32.0",
@@ -9881,6 +9943,7 @@
             "keywords": [
                 "tokenizer"
             ],
+            "abandoned": true,
             "time": "2019-09-17T06:23:10+00:00"
         },
         {
diff --git a/web/modules/custom/opdavies_blog/tests/modules/opdavies_blog_test/src/Factory/PostFactory.php b/web/modules/custom/opdavies_blog/tests/modules/opdavies_blog_test/src/Factory/PostFactory.php
new file mode 100644
index 0000000..5ef0417
--- /dev/null
+++ b/web/modules/custom/opdavies_blog/tests/modules/opdavies_blog_test/src/Factory/PostFactory.php
@@ -0,0 +1,61 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\opdavies_blog_test\Factory;
+
+use Assert\Assert;
+use Drupal\node\Entity\Node;
+use Drupal\opdavies_blog\Entity\Node\Post;
+use Drupal\taxonomy\Entity\Term;
+use Drupal\taxonomy\TermInterface;
+use Illuminate\Support\Collection;
+
+final class PostFactory {
+
+  private Collection $tags;
+
+  private string $title = 'This is a test blog post';
+
+  public function __construct() {
+    $this->tags = new Collection();
+  }
+
+  public function create(array $overrides = []): Post {
+    $this->tags->each(function (TermInterface $tag): void {
+      Assert::that($tag->bundle())->same('tags');
+    });
+
+    $values = [
+      'field_tags' => $this->tags->toArray(),
+      'title' => $this->title,
+      'type' => 'post',
+    ];
+
+    /** @var Post $post */
+    $post = Node::create($values + $overrides);
+
+    return $post;
+  }
+
+  public function setTitle(string $title): self {
+    Assert::that($title)->notEmpty();
+
+    $this->title = $title;
+
+    return $this;
+  }
+
+  public function withTags(array $tags): self {
+    foreach ($tags as $tag) {
+      Assert::that($tag)->notEmpty()->string();
+
+      $this->tags->push(
+        Term::create(['vid' => 'tags', 'name' => $tag])
+      );
+    }
+
+    return $this;
+  }
+
+}
diff --git a/web/modules/custom/opdavies_blog/tests/src/Kernel/Entity/Node/PostTest.php b/web/modules/custom/opdavies_blog/tests/src/Kernel/Entity/Node/PostTest.php
index 7e1c29e..8c87c05 100644
--- a/web/modules/custom/opdavies_blog/tests/src/Kernel/Entity/Node/PostTest.php
+++ b/web/modules/custom/opdavies_blog/tests/src/Kernel/Entity/Node/PostTest.php
@@ -6,9 +6,8 @@ namespace Drupal\Tests\custom\Kernel\Entity\Node;
 
 use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
 use Drupal\node\Entity\Node;
-use Drupal\node\NodeInterface;
 use Drupal\opdavies_blog\Entity\Node\Post;
-use Drupal\taxonomy\Entity\Term;
+use Drupal\opdavies_blog_test\Factory\PostFactory;
 
 final class PostTest extends EntityKernelTestBase {
 
@@ -28,32 +27,24 @@ final class PostTest extends EntityKernelTestBase {
 
   /** @test */
   public function it_can_determine_if_a_post_contains_a_tweet(): void {
-    /** @var Post $post */
-    $post = Node::create(['type' => 'post']);
+    $post = (new PostFactory())->create();
+    $post->save();
+
     $this->assertFalse($post->hasTweet());
 
-    /** @var Post $post */
-    $post = Node::create([
-      'field_has_tweet' => TRUE,
-      'type'  => 'post',
-    ]);
+    $post = (new PostFactory())->create(['field_has_tweet' => TRUE]);
+    $post->save();
+
     $this->assertTrue($post->hasTweet());
   }
 
   /** @test */
   public function it_converts_a_post_to_a_tweet(): void {
-    /** @var Post $post */
-    $post = Node::create([
-      'field_tags' => [
-        Term::create(['vid' => 'tags', 'name' => 'Automated testing']),
-        Term::create(['vid' => 'tags', 'name' => 'DDEV']),
-        Term::create(['vid' => 'tags', 'name' => 'Drupal']),
-        Term::create(['vid' => 'tags', 'name' => 'PHP']),
-      ],
-      'status' => NodeInterface::PUBLISHED,
-      'title' => 'Creating a custom PHPUnit command for DDEV',
-      'type' => 'post',
-    ]);
+    $post = (new PostFactory())
+      ->setTitle('Creating a custom PHPUnit command for DDEV')
+      ->withTags(['Automated testing', 'DDEV', 'Drupal', 'PHP'])
+      ->create();
+
     $post->save();
 
     $expected = <<<EOF
@@ -69,17 +60,11 @@ final class PostTest extends EntityKernelTestBase {
 
   /** @test */
   public function certain_terms_are_not_added_as_hashtags(): void {
-    /** @var Post $post */
-    $post = Node::create([
-      'field_tags' => [
-        Term::create(['vid' => 'tags', 'name' => 'Drupal']),
-        Term::create(['vid' => 'tags', 'name' => 'Drupal Planet']),
-        Term::create(['vid' => 'tags', 'name' => 'PHP']),
-      ],
-      'status' => NodeInterface::PUBLISHED,
-      'title' => 'Drupal Planet should not be added as a hashtag',
-      'type' => 'post',
-    ]);
+    $post = (new PostFactory())
+      ->setTitle('Drupal Planet should not be added as a hashtag')
+      ->withTags(['Drupal', 'Drupal Planet', 'PHP'])
+      ->create();
+
     $post->save();
 
     $expected = <<<EOF