-
Writing Tests for Drush Commands
There are plenty of examples of these in the wild, but I figured I’d show a stripped down version of an automated Kernel test that successfully tests a drush command. The trick here is making sure you set up a logger and that you stub a few methods (if you happen to use $this->logger() and dt() in your Drush commands). Also featured in this example is the use of Faker to generate realistic test data.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150<?phpdeclare(strict_types = 1);namespace Drupal\mymodule\Tests\Kernel {use Drupal\KernelTests\KernelTestBase;use Drupal\mymodule\Drush\Commands\QueueCommands;use Drush\Log\DrushLoggerManager;use Faker\Factory;use Faker\Generator;use Psr\Log\LoggerInterface;/*** Tests the QueueCommands Drush command file.*/final class QueueTest extends KernelTestBase {/*** {@inheritdoc}*/protected static $modules = [// Enable all the modules that this test relies on.'auto_entitylabel','block','datetime','eck','field','file','image','link','mymodule','mysql','options','path','path_alias','system','text','user','views','views_bulk_operations',];/*** The Faker generator.** @var \Faker\Generator*/protected Generator $faker;/*** The logger.** @var \Drush\Log\DrushLoggerManager|\Psr\Log\LoggerInterface*/protected DrushLoggerManager|LoggerInterface $logger;/*** The Drush queue commands.** @var \Drupal\mymodule\Drush\Commands\QueueCommands*/protected QueueCommands $drushQueueCommands;/*** Set up the test environment.*/protected function setUp(): void {parent::setUp();// Install required config.$this->installConfig(['mymodule']);$this->faker = Factory::create();$logger_class = class_exists(DrushLoggerManager::class) ? DrushLoggerManager::class : LoggerInterface::class;$this->logger = $this->prophesize($logger_class)->reveal();$drushQueueCommands = new QueueCommands();$drushQueueCommands->setLogger($this->logger);}/*** Tests the mymodule:close-all-registration-entities drush command.*/public function testCloseAllRegistrationEntities() {// Create a single registration entity.// Just demoing simple Faker usage for blog post.$registration = \Drupal::entityTypeManager()->getStorage('registration')->create(['type' => 'appt','field_echeckin_link' => $this->faker->url(),'field_epic_id' => $this->faker->randomNumber(9),'field_first_name' => $this->faker->firstName(),'field_has_echeckin_link' => $this->faker->boolean(),'field_outcome' => NULL,'field_phone_number' => $this->faker->phoneNumber(),'field_sequence_is_open' => 1,]);$registration->save();// This method exists in my module's Drush command file.// It sets field_sequence_is_open field to 0 for all open reg entities.// drush mymodule:close-all-registration-entities.$this->drushQueueCommands->closeAllRegistrationEntities();// Assert that the registration entity was updated.$this->assertCount(1, \Drupal::entityTypeManager()->getStorage('registration')->loadByProperties(['field_sequence_is_open' => 0]));}}}namespace {if (!function_exists('dt')) {/*** Stub for dt().** @param string $message* The text.* @param array $replace* The replacement values.** The text.*/function dt($message, array $replace = []): string {return strtr($message, $replace);}}if (!function_exists('drush_op')) {/*** Stub for drush_op.** @param callable $callable* The function to call.*/function drush_op(callable $callable) {$args = func_get_args();array_shift($args);return call_user_func_array($callable, $args);}}}I learned this via the migrate_tools project here.
-
Using Data Providers in PHPUnit Tests
This is the before code, where a single test is run, and each scenario I’m testing could be influenced by the previous scenario (not a good thing, unless that was my goal, which it was not).
123456789101112131415161718192021/*** Tests the getRegistrationsWithinNumDays function.*/public function testGetRegistrationsWithinNumDays() {$firstRegDateTimeStr = '2024-03-18 08:00:00';$this->mhpreregTestHelpers->createRegistration(['field_reg_datetime' => '2024-03-18 09:00:00']);$this->mhpreregTestHelpers->createRegistration(['field_reg_datetime' => '2024-03-19 13:00:00']);$this->mhpreregTestHelpers->createRegistration(['field_reg_datetime' => '2024-03-20 14:20:00']);$this->mhpreregTestHelpers->createRegistration(['field_reg_datetime' => '2024-03-20 17:35:00']);$this->mhpreregTestHelpers->createRegistration(['field_reg_datetime' => '2024-03-21 17:35:00']);$this->mhpreregTestHelpers->createRegistration(['field_reg_datetime' => '2024-03-22 08:00:00']);$registrations = Utils::getIdsOfRegistrationsWithinNumDays(1, $firstRegDateTimeStr);$this->assertEquals(2, count($registrations));$registrations = Utils::getIdsOfRegistrationsWithinNumDays(2, $firstRegDateTimeStr);$this->assertEquals(3, count($registrations));$registrations = Utils::getIdsOfRegistrationsWithinNumDays(3, $firstRegDateTimeStr);$this->assertEquals(5, count($registrations));}This is the after code, where three tests are run. Unfortunately this takes 3x longer to execute. The upside is that each scenario cannot affect the others and it’s perhaps more readable and easier to add additional scenarios.
123456789101112131415161718192021222324252627/*** Data provider for testGetRegistrationsWithinNumDays.*/public function registrationsDataProvider() {return [// Label => [numDays, expectedCount].'1 day' => [1, 2],'2 days' => [2, 3],'3 days' => [3, 5],];}/*** Tests the getRegistrationsWithinNumDays function using a data provider.** @dataProvider registrationsDataProvider*/public function testGetRegistrationsWithinNumDays($numDays, $expectedCount) {$this->mhpreregTestHelpers->createRegistration(['field_reg_datetime' => '2024-03-18 08:00:00']);$this->mhpreregTestHelpers->createRegistration(['field_reg_datetime' => '2024-03-19 09:00:00']);$this->mhpreregTestHelpers->createRegistration(['field_reg_datetime' => '2024-03-20 13:00:00']);$this->mhpreregTestHelpers->createRegistration(['field_reg_datetime' => '2024-03-21 14:20:00']);$this->mhpreregTestHelpers->createRegistration(['field_reg_datetime' => '2024-03-21 17:35:00']);$this->mhpreregTestHelpers->createRegistration(['field_reg_datetime' => '2024-03-22 17:35:00']);$registrations = Utils::getIdsOfRegistrationsWithinNumDays($numDays, '2024-03-18 08:00:00');$this->assertEquals($expectedCount, count($registrations), "Failed for {$numDays} days.");} -
Testing Cookie Modification in Laravel 8
If there’s a better way to pass a cookie from one request to another, in a phpunit feature test in Laravel, please let me know! Here’s one way to handle it:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546<?phpnamespace Tests\Feature;use Illuminate\Cookie\CookieValuePrefix;use Tests\TestCase;class DiveDeeperTest extends TestCase{/*** Tests if the diveDeeper route appends the passed value to the cookie arr.* @return void*/public function test_it_updates_path_cookie(){// Post a deeperPath to the diveDeeper route (with a starter cookie).// This should append the "deeperPath" value to the "path" cookie array.$resp = $this->withCookie('path', serialize(['/']))->post(route('diveDeeper'), ['deeperPath' => 'Users']);// Assert that the cookie has been modified.$resp->assertCookie('path', serialize(['/', 'Users']));// If we make another post request here, it won't include our// modified "path" cookie. We have to retrieve it, then pass it// through to the next request.// Decrypt the cookie (now modified by the diveDeeper route).$cookieValue = CookieValuePrefix::remove(app('encrypter')->decrypt($resp->getCookie('path')->getValue(), false));// Hit the diveDeeper route again using the modified cookie value.$resp = $this->withCookie('path', $cookieValue)->post(route('diveDeeper'), ['deeperPath' => 'adam']);// Assert that the cookie has been modified again.$resp->assertCookie('path', serialize(['/', 'Users', 'adam']));// Etc...}}