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.
Running tests locally
When running tests on your local machine, it is important to make sure that runtime assertions are being tested. You can verify this by adding a line like this to a test you are running locally (which should make the test fail):
assert(FALSE);
If you are running a Functional or FunctionalJavaScript test, you should also put that line into a Controller or Form class method that is being called to render output during the test, to verify that runtime assertions in that code will be tested.
If runtime assertions are not being tested and you have a custom phpunit.xml file, add the following lines in the PHP error reporting section:
<!-- Do our best to make sure assertions behave as expected. We can't change zend.assertions though. -->
<ini name="assert.active" value="1"/>
If they still are not being tested, you'll need to find the php.ini files that are being used for running PHP at the command line and in the web server (/etc/php/VERSION/cli/php.ini and /etc/php/VERSION/apache2/php.ini on Linux-like systems, where VERSION is your PHP version), and make sure they both have this setting in them:
zend.assertions = 1
Note that after changing the apache php.ini file, you will need to restart your web server for the changes to take effect.
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 to 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 method argument be 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 document 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. Assertions provide a method to test these expectations when it is critical to test them - during development, and then skip the testing once the code enters production.
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 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, unlike unit tests, are also constantly on the watch while you are writing your code. Finally, they are checked when the unit tests are running, providing an extra layer of testing to the system.
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.
Occassionally assert() statements are placed as the default of a switch case statement. Consider the following example from php.net 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, and also to replace expensive code structure validations such as those in the caching system in order to optimize the code base.
An assertion inspector component has been provided to do several common generic tests. They are invoked like so.
assert('\\Drupal\\Component\\Assertion\\Inspector::assertAllStrings($array)');
This will assert that all the elements of $array are strings. The full list of assertions in this class are:
- assertTraversable: The variable is safe for use with foreach()
- assertAllStrings: The variables in the collection are all strings.
- assertAllStringable: The variables in the collection can all be safely handled as strings.
- assertAllArrays: The variable is a collection of arrays, such as a collection of table rows.
- assertStrictArray: The variable is an array as found most languages, where the keys are always 0, 1, 2...
- assertAllStrictArrays: The variable is a collection of the above.
- 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.
- assertAllInteger: The variable is a collection of integers.
- assertAllFloat: The variable is a collection of floating point numbers.
- assertAllCallable: The variable is a collection of callbacks.
- assertAllNotEmpty: All members of the variable aren't empty.
- assertAllNumeric: The variable is a collection of numbers.
- assertAllMatch:The variable is a collection of strings that contain the specified string.
- assertAllRegularExpressionMatch: The variable is a collection of strings matching the passed regular expression.
- assertAllObjects: The variable is a collection of objects, and you may specify interfaces and/or classes to match.
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 insure 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 insure 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.
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 only strings in single quotes to assert() - these will be passed to eval() for testing. This makes assert() in PHP 5 just as vulnerable as eval() to unsanitized data. This said, unsanitized data should not reach an assert() statement - such data needs to be tested with normal validators throwing normal exceptions when the data is bad before any assert() statement sees it.
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 7 can be configured to throw an \AssertionError when assertions fail. Drupal uses an assertion callback to do the same in PHP 5. This is done for the rare occasions that unit tests need directly test if an assertion will throw (which is usually redundant).
Impact on Site Builders
Out of the box Drupal uses the .htaccess file to disable assertions in production. In a development environment assertions are enabled in the settings.local file which allows them to be active when module and theme code is being executed.
As assertions are added to core it is possible that an older module may fail an assertion. The developer of the module should be notified of this. In the interim consider using a staging environment that, like production, has assertions disabled, and manually test the module there. If there are no problems then the assertion failure can be ignored, at least in the short term. In the long term the offending module does need to be updated to follow the API correctly, and in some cases the assertion may aid in solving outstanding bugs whose solutions had eluded the development teams. This is the reason for their addition to the Drupal code base.
Impact on Module Developers
Assertions exist to ensure your code interacts with Drupal in the manner Drupal expects to be interacted with. In the long term this will ease development headaches by catching problems early and providing clearer error messages than would be encountered if the code was allowed to proceed onward to a crash point.
In the short term you may encounter upgrades causing assertion failures whenever your module wasn't following the API. These can be annoying to fix, but the correcting them will improve the code for all.
Impact on Themers
There will be relatively few assert statements in the theme code since by design theme code should be more forgiving than logic code. Most of them revolve around the setup of themes. When they are failed the same concerns as those faced by module developers apply.