Skip to main content

Making your Drupal 8 kernel tests fail when there is an exception during cron

Several times in the past I've been caught out by Drupal's cron handler silently catching exceptions during tests.

Your test fails, and there is no clue as to why.

Read on to find out how to shine some light on this, by making your kernel tests fail on any exception during cron.

by lee.rowlands /

If you're running cron during a kernel test and expecting something to happen, but it doesn't - it can be hard to debug why.

Ordinarily an uncaught exception during a test will cause PHPUnit to fail, and you can pinpoint the issue.

However, if you're running cron in the test this may not be the case.

This is because, by default Drupal's cron handler catches all exceptions and silently logs them. This is colloquially known as Pokemon exception handling.

The act of logging an exception is not enough to fail a test.

So your test skips the exception and carries on, failing in other ways unexpectedly.

This is exacerbated by the fact that PHP Unit throws an exception for warnings. So the slightest issue in your code will cause it to halt execution. In an ordinary scenario, this exception causes the test to fail. But the pokemon catch block in the Cron class prevents that, and your test continues in a weird state.

This is the code in question in the cron handler

<?php
try {
$queue_worker->processItem($item->data);
$queue->deleteItem($item);
}
// ... catch (\Exception $e) {
// In case of any other kind of exception, log it and leave the item
  // in the queue to be processed again later.
  watchdog_exception('cron', $e);
}

So how do you make this fail your test? In the end, it's quite simple.

Firstly, you make your test a logger and use the handy trait to do the bulk of the work.

You only need to implement the log method, as the trait takes care of handling all other methods.

In this case, watchdog_exception logs exceptions as RfcLogLevel::ERROR. The log levels are integers, from most severe to least severe. So in this implementation we tell PHP Unit to fail the test with any messages logged where the severity is ERROR or worse.

use \Drupal\KernelTests\KernelTestBase;
use Psr\Log\LoggerInterface;
use Drupal\Core\Logger\RfcLoggerTrait;
use Drupal\Core\Logger\RfcLogLevel;

class MyTest extends KernelTestBase implements LoggerInterface {
  use RfcLoggerTrait;

  /**
   * {@inheritdoc}
   */
  public function log($level, $message, array $context = []) {
    if ($level <= RfcLogLevel::ERROR) {
      $this->fail(strtr($message, $context));
    }
  }
}

Then in your setUp method, you register your test as a logger.

$this->container->get('logger.factory')->addLogger($this);

And that's it - now any errors that are logged will cause the test to fail.

If you think we should do this by default, please comment on this core issue.