Runtime Assertions

Last updated on
9 January 2017

A runtime assertion is a statement that is expected to always be true at the point in the code it appears at. They are tested using PHP's internal assert() statement. If an assertion is ever FALSE it indicates an error in the code or in module or theme configuration files. User-provided configuration files should be verified with standard control structures at all times, not just checked in development environments with assert() statements on.

Since unit tests also use the term "assertion" to refer to test conditions, the term "runtime assertion" will be used when disambiguation is necessary.

What Assertions are and are not.

Assertions are used to test the expectations of the programmer at run time regarding the code state. They openly state conditions that should be impossible when code and configuration are in an error free state.

Testing to see whether these conditions are actually true can in some cases introduce considerable overhead. For example, cache tags must be strings to be valid and if they are not the cache will become corrupted. Cache tags are only introduced when module developers are creating new code - after tests are concluded and the module deployed such tests become redundant, and they can involve traversing arrays with thousands of elements with an "is_string" statement.

This redundancy is undesirable in a production environment, but commenting out such checks creates a new code state to test and applying the commenting out by hand introduces a possibility for human error. So for these reasons the assert() statement can be disabled and in this state PHP ignores them.

This ability to be disabled is what separates assertions from other exceptions. PHP 7 natively treats assertion failures as error exceptions, and Drupal uses an assert callback handler to force PHP 5.x to do the same. Whenever they fail an AssertionError is thrown (In PHP 7 it extends Error, Drupal's handler is forced to extend from Exception in PHP 5) and it can be caught normally.

There is also a matter of context that separates them from other exceptions. When you read an assert() statement you are reading something the writer of that assert() believed impossible. If it's not then the code that follows will act in some buggy manner or fail outright. By stopping the code flow at the assertion point the code following the assertion can be ruled out as the actual source of the problem even though that would be the point of failure where the assert() statement not there. Other exceptions meanwhile can sometimes occur, although such may be very unlikely.

Expectations: Design by Contract

Assert() statements help to define the expectations of the codebase. Drupal has a large API with many public methods that your modules may call, and many that in accordance with the API guidelines your code is expected to call. These methods often expect to receive data from your module in a particular way. PHP 5 solves some of this problem with type hints - enforcing that a module argument is an object or array. But what if it should be a string? What if it should be one of a few specific keywords - such as "template", "module", or so on? What if an array is called for where all the members of the array must fit a certain constraint such as all of them being strings or numbers?

PHP documentation code in the comments can check for this, but it can't verify that it's actually being done. Only an assert() statement can do that cleanly. It could be checked for with normal control structures, but the performance loss of checking some of Drupal's larger data structures such as render arrays every single time they are passed between public functions would be enormous.

Actively checking expectations this way is a programming paradigm known as "Design by Contract." Think of the API description as a contract - you provide the Drupal code with data in a known range and format and you will receive the same. This isn't a replacement for Test Driven Design but a supplement to it. Unit tests check known scenarios and configurations, and collections of such tests constantly recheck such scenarios to prevent regressions in the system. Functional tests check to see if the whole system behaves as expected, again in known scenarios. At the time of this writing, there are nearly 100,000 such tests in Drupal 8. But what about the unknown?

This is the role of the Design by Contract approach. The tests were done with the assert() statement aren't so much about the components of Drupal themselves but the interactions between them and their interactions with module code written by anyone.

Assert() statements most frequently show up at the start and the end of functions. Those assert() statements at the start are known as "preconditions." They also show up just before the return statement of the code - these are known as post conditions.

Occasionally assert() statements are placed as the default of a switch case statement. Consider the following example dealing with card suits.

switch($suit) {
  case 'heart':
    //some action
    break;
  case 'diamond':
    // some action
    break;
  case 'club':
    // some action
    break;
  case 'spade':
    // some action
    break;
  default:
    assert(false, "Invalid suit {$suit}");
}

Here the assertion only fails when the function this branching appears in gets passed a suit that doesn't exist.

Assertions in Drupal

Assertions are only now being added to Drupal - at this time they are still large areas of the code that aren't being tested this way. The first assertions that will be added to Drupal will deal with the areas of the code module and theme developers work with most frequently, and working out from there.

An assertion inspector component has been provided to do several common generic tests. They are invoked like so.

  assert(Inspector::assertAllStrings($array));

This will assert that all the elements of the $array are strings. The full list of assertions in this class are:

The above is just an overview - see the api documentation for more information.

Configuration Checking

Drupal has several objects that the services.yml file of modules can change. These objects may contain assertions about their own state to make sure they haven't been misconfigured, such as \Drupal\Template\TwigExtension.

  public function getPath($name, $parameters = array(), $options = array()) {
    assert($this->urlGenerator instanceof UrlGeneratorInterface, 'Generator missing. The most likely culprit is a misconfigured services.yml file in a module.');
    $options['absolute'] = FALSE;
    return $this->urlGenerator->generateFromRoute($name, $parameters, $options);
  }

Using Assert

You are encouraged to use unit tests whenever possible to test your own code and modules. The assert statement is most useful for modules that provide API's that will be used by other modules. As in Drupal core, assert can be used in those modules to ensure the calling code is meeting the expectations of the API by providing variables of the correct data type. You can further use return assertions to ensure your code is providing what has been promised to the outside code.

The assert statement takes two arguments. The first is the assertion, which is the condition to be tested. If a string is provided it will be evaluated as PHP code just as with the eval statement, which is dangerous and not recommended. Unfortunately, PHP 5 will execute these statements before checking the assertion even when assertions are turned off. To prevent a performance loss Drupal's Inspector class checks the assert active flag before running any assertion and always returns TRUE if assertions are inactive. To preserve performance in PHP 5 your code must do the same.

The second argument is a description of what is being asserted, similar to the error message a thrown error would include.

PHP 5 vs. 7

PHP 7 changes how assert() is handled in several ways.

First, in PHP 5 it is technically a function, and turning assertions off simply steps around the function call. This means expressions and function calls in assert() will evaluate whether this is desired or not. This can be prevented by passing in a string to be passed to eval() for testing, but this comes with its severe security problems if the string contains unsanitized data. Drupal's Inspector directly checks the assert active flag before running any of its methods to avoid performance loss - assertions that don't use the Inspector should be kept to extremely simple checks.

PHP 7 can be set up in one of three ways - it can have assertions on; it can do the step around of PHP 5, or it can be instructed to not even attempt a compile on the statements to begin with. This third mode is the recommended default. Whether the second mode is used or not, in PHP 7 assert() is a statement, not a function, and all arguments to it are ignored when assertions are turned off.

Second, PHP 5 triggers an E_USER_WARNING when assert() statements fail. PHP 7 throws \AssertionError. Drupal uses an assertion callback to do the same so that unit tests can look for the throw if they expect it.

  1. assertTraversable: The variable is safe for use with foreach()
  2. assertAllStrings: The variables in the collection are all strings.
  3. assertAllStringable: The variables in the collection can all be safely handled as strings.
  4. assertAllArrays: The variable is a collection of arrays, such as a collection of table rows.
  5. assertStrictArray: The variable is an array as found most languages, where the keys are always 0, 1, 2...
  6. assertAllStrictArrays: The variable is a collection of the above.
  7. assertAllHaveKey: This method takes a variable and then one or more string keys and only returns true if the variable is a collection and all its members have the keys specified.
  8. assertAllInteger: The variable is a collection of integers.
  9. assertAllFloat: The variable is a collection of floating point numbers.
  10. assertAllCallable: The variable is a collection of callbacks.
  11. assertAllNotEmpty: All members of the variable aren't empty.
  12. assertAllNumeric: The variable is a collection of numbers.
  13. assertAllMatch:The variable is a collection of strings that contain the specified string.
  14. assertAllRegularExpressionMatch: The variable is a collection of strings matching the passed regular expression.
  15. assertAllObjects: The variable is a collection of objects, and you may specify interfaces and/or classes to match.