Часть кода делает предположение о чем-то: текущем состоянии объекта, или параметра, или локальной переменной цикла. Обычно это предположение никогда не будет нарушено, но может быть в случае появления ошибки.
Давайте сделаем предположения явными, как мы делаем для подсказок типа на параметрах метода; обоснование такое же, как проверка служит как для обеспечения правильности и для целей документирования. Эта документация хранится как утверждение, встроенное в производственный код (которое отличается от утверждений, сделанных в тестах, хотя поведение такое же).
Почему утверждение?
Если предположение неверно и код даст бессмысленный результат, лучше немедленно прекратить (например, вызвать исключение). Совет Фаулера состоит в том, чтобы использовать утверждения только для того, что должно быть правдой, а не для всего, что является правдой в определенной точке программы (которая бесконечна).
Если код работает с ошибочным утверждением, вы должны удалить его, так как он не предполагает, что код делает. В противном случае это означает, что у вас пропущен тест …
Случаи применения
Я пытаюсь сделать утверждения о недоступности определенных частей кода, которые должны оставаться там из-за того, как работает язык. Например, когда достигается неявное возвращаемое значение null:
public function doSomething() { if ($blue) { ... } else if ($notBlue) { ... } assert('false'); }
Лучше выполнить утверждение, которое вызовет исключение в худшем случае, чем возвращать ноль и получать фатальную ошибку при вызове (нуль) -> метода ().
Вы также можете сделать утверждение о количестве элементов в коллекции перед тем, как получить первые, что даст более приятную ошибку в случае возникновения проблем:
assert('count($array) >= 2'); $variable = $array[0] + $array[1]; ...
Утверждение против исключений
Когда утверждения становятся плохими? Когда исключение лучше. Классы исключений могут быть выбраны для предоставления точного отчета об ошибке, в то время как утверждения обычно генерируют одно и то же общее исключение (см. Следующий раздел).
На самом деле, некоторые исключения могут быть эволюцией утверждений (с тем уловом, что их можно поймать … что за ужасный каламбур.) Существует много мнений об утверждениях и исключениях, но, на мой взгляд, невосстановимые исключения эквивалентны утверждениям (кроме от ввода), поскольку они указывают на ошибку и никогда не должно произойти.
В PHP
Общее практическое правило заключается в использовании исключений для ошибок других людей, таких как предварительное условие при вводе, и утверждений для ваших собственных ошибок, таких как ошибки в коде.
Фактически, я использовал их еще во времена ковбойского кодирования в середине сложного кода, чтобы легко изолировать ошибку. Теперь, когда мы проверяем все с помощью PHPUnit, утверждения менее полезны. Однако проверки assert () могут быть удалены в производственном коде (путем отключения обработчика assert ()).
Случай PHP также специфичен, потому что часто ошибка просто останавливает ответ на HTTP-запрос, а не останавливает все приложение. Но в приложениях Ajax клиентская сторона может быть не в состоянии обработать ошибку в ответе, возвращаемом сервером.
Предположения?
В дебатах между кодом самопроверки и кодом, проводимым внешним набором тестов, важно учитывать локальность утверждений. Если вы сделаете предположение о наличии синглтона, не очень хорошо помещать утверждение в производственный код. Было бы лучше улучшить код, сделав его более локальным, и подвергнуть сомнению это предположение.
The test suite substitutes defensive programming in many cases to simplify testability, as it collects almost all the assertions you are gonna make. I suggest to write assertions only for data that comes from the integration of a group of objects not under your control, or for data deep inside a computation that you cannot easily expose to a unit test.
It’s important to ensure the quality of code deployed in production, but also to separate the concerns of testability from the concerns of functionality. Add to that the beneficial effects of testability on design and now you know why a battery of unit test is almost always superior to embedded assert().
After all, you would remove scaffolding from a finished building.
Example
in this example we see how to add an assertion over the current state of an object before. then we transform it in a custom exception, to see how this change can be done easily in case it applies to your code. In the initial state, there is nothing stopping the tax rate from becoming negative:
<?php class IntroduceAssertion extends PHPUnit_Framework_TestCase { public function testTaxesAreAddedToTheNetPrice() { $price = new Price(1000); $price->addTaxRate(20); $this->assertEquals(1200, $price->value()); } public function testTaxesCanBeLoweredBy10PerCentAtTheTime() { $price = new Price(1000); $price->addTaxRate(20); $price->lowerTaxRate(); $price->lowerTaxRate(); $this->assertEquals(1000, $price->value()); } } class Price { private $net; private $taxRate; public function __construct($net) { $this->net = $net; } public function addTaxRate($rate) { $this->taxRate = $rate; } public function lowerTaxRate() { $this->taxRate -= 10; } public function value() { return $this->net * (1 + $this->taxRate / 100); } }
We add a test to check this corner case (which now will fail). The goal is just to show the behavior of assertions in this example: a test shouldn’t be needed in real code once you’re familiar with assert().
public function testTaxesCannotBeLoweredBelowZeroForAValidPrice() { $price = new Price(1000); $price->addTaxRate(20); $price->lowerTaxRate(); $price->lowerTaxRate(); $price->lowerTaxRate(); $this->setExpectedException('AssertionException'); $price->value(); }
Now we introduce the assertion. We have to configure a callback that assert() will call after an expression evaluates to false. The argument of assert() is expressed as a string so that it can be reported to the programmer when the assertion fails (passing a boolean will not result in the same behavior).
<?php class AssertionException extends Exception {} assert_options(ASSERT_CALLBACK, function($file, $line, $message) { throw new AssertionException($message); } ); class Price { private $net; private $taxRate; public function __construct($net) { $this->net = $net; } public function addTaxRate($rate) { $this->taxRate = $rate; } public function lowerTaxRate() { $this->taxRate -= 10; } public function value() { assert('$this->taxRate >= 0'); return $this->net * (1 + $this->taxRate / 100); } }
Here is the same behavior obtained via a Factory Method on the assertion class. This time we use directly exceptions; note that we invert the logic and we don’t have code inside a string anymore.
<?php class AssertionException extends Exception { public static function throwIf($condition) { if ($condition) { throw new self('Assertion failed.'); } } } class Price { private $net; private $taxRate; public function __construct($net) { $this->net = $net; } public function addTaxRate($rate) { $this->taxRate = $rate; } public function lowerTaxRate() { $this->taxRate -= 10; } public function value() { AssertionException::throwIf($this->taxRate < 0); return $this->net * (1 + $this->taxRate / 100); } }
I’m quite favorable to exceptions in any case where the object isn’t in a perfectly defined state, as the alternative is to produce garbage as a result. If I had to continue working on this code, I will move the exception to the lowerTaxRate() method to catch the problem earlier.