Статьи

Практический PHP-рефакторинг: замена исключения на тестовую

Иногда перехват исключения может быть преобразован в предварительную проверку, которая в первую очередь исключает возникновение исключения. Затем код вызывается только в обычном случае, тогда как ранее исключительные просто завершают текущий метод. Так что речь идет не об автоматизированном тестировании, а об условно вставленном в нужном месте.

Почему избегать исключения?

В прошлой статье мы видели, что исключения, как правило, являются более гибким решением в отношении кодов ошибок. Тем не менее, они не являются более чистым решением в отношении всего остального . Например, большая часть управления потоком должна управляться с помощью обычного кода, поскольку это легче понять, чем последовательность блоков try / catch. Исключения должны быть зарезервированы для реальных ошибок .

Таким образом, когда исключение можно легко избежать с помощью предварительной проверки, предпочтительно выполнять этот тест вместо вызова метода, в то время как мы уже можем точно знать, что вызов завершится неудачей.

Первоначально этот рефакторинг также был связан с медленными реализациями исключений. На самом деле, это просто простота: зачем создавать и перехватывать объекты для связи, когда выделенный вызов метода может сделать то же самое?

меры

В исходной ситуации клиентский код вызывает метод на объекте сервера.

  1. Вставьте условие для ошибочного случая: возможно, ему придется делегировать объекту сервера некоторую часть своей логики.
  2. Скопируйте код из блока catch в оператор if . Обычно это генерирует новое исключение или возвращает определенное значение.
  3. Теперь улов должен генерировать базовый объект Exception, чтобы проверить, что он не выполняется ни в одном из рассматриваемых случаев. Запустите тестовый набор.
  4. Удалите защелку и даже структуру try, если нет других исключений для управления.

пример

Этот рефакторинг редко встречается в PHP-функциях, которые все еще являются процедурными. Тем не менее, такие библиотеки, как Zend Framework, Symfony и Doctrine, имеют богатую иерархию исключений и могут побудить разработчика попытаться и поймать, а не пытаться полностью понять их API и поведение.

Однако мы будем использовать одну из немногих базовых объектно-ориентированных библиотек PHP, PDO, для переносимости этого примера.

Этот объект предназначен для того, чтобы скрыть PDO от остальной части приложения: как таковой, он также генерирует свое собственное исключение вместо того, чтобы заставить клиентский код зависеть от объектов PDOException и работать с ними. Я моделирую только фазу создания, опуская все методы, не относящиеся к этому примеру:

<?php
class ReplaceExceptionWithTest extends PHPUnit_Framework_TestCase
{
    /**
     * @expectedException DatabaseException
     */
    public function testTheConnectionIsNotCreatedIfTheDriverIsNotSupported()
    {
        $database = new Database('somecloneofsqlite', ':memory:');
    }
}

class Database
{
    private $connection;

    public function __construct($driver, $restOfDsn)
    {
        try {
            $dsn = $driver . ':' . $restOfDsn;
            $this->connection = new PDO($dsn);
        } catch (PDOException $e) {
            throw new DatabaseException("The connection was not successful: check the configuration (dsn: '$dsn').");
        }
    }
}

class DatabaseException extends Exception {}

Мы копируем содержимое catch, которое выбрасывает другое исключение, в предложение guard .

class Database
{
    private $connection;
    private $supportedDrivers = array('sqlite', 'mysql');

    public function __construct($driver, $restOfDsn)
    {
        $dsn = $driver . ':' . $restOfDsn;
        if (!in_array($driver, $this->supportedDrivers)) {
            throw new DatabaseException("The connection was not successful: check the configuration (dsn: '$dsn').");
        }
        try {
            $this->connection = new PDO($dsn);
        } catch (PDOException $e) {
            throw new DatabaseException("The connection was not successful: check the configuration (dsn: '$dsn').");
        }
    }
}

Чтобы убедиться, что улов больше не достигнут, мы добавляем в него общее исключение. PHPUnit всегда будет сигнализировать об общем исключении как об ошибке, даже если мы добавим его в @expectedException.

    public function __construct($driver, $restOfDsn)
    {
        $dsn = $driver . ':' . $restOfDsn;
        if (!in_array($driver, $this->supportedDrivers)) {
            throw new DatabaseException("The connection was not successful: check the configuration (dsn: '$dsn').");
        }
        try {
            $this->connection = new PDO($dsn);
        } catch (PDOException $e) {
            throw Exception();
        }
    }

Выполнение тестов подтверждает, что код catch является недоступным (конечно, для случаев, охватываемых нашим набором тестов):

[17:15:37][giorgio@Galen:~/Dropbox/practical-php-refactoring]$ phpunit ReplaceExceptionWithTest.php
PHPUnit 3.6.4 by Sebastian Bergmann.

.

Time: 1 second, Memory: 2.50Mb

OK (1 test, 1 assertion)

Теперь мы можем удалить блок try / catch, чтобы окончательно очистить код:

class Database
{
    private $connection;
    private $supportedDrivers = array('sqlite', 'mysql');

    public function __construct($driver, $restOfDsn)
    {
        $dsn = $driver . ':' . $restOfDsn;
        if (!in_array($driver, $this->supportedDrivers)) {
            throw new DatabaseException("The connection was not successful: check the configuration (dsn: '$dsn').");
        }
        $this->connection = new PDO($dsn);
    }
}

Другие ошибки в конфигурации могут вызвать исключение PDOException. Но полезно обнаружить, что некоторые объекты PDOException всплывают и покрыть их дополнительным тестом: альтернатива — продолжать вызывать DatabaseException для всех случаев, содержащих либо очень общее, либо неправильное сообщение об ошибке.