Подтвердить что ты не робот

Модульное тестирование и статические методы

Чтение и сбор данных на модульном тестировании, пытаясь понять следующее сообщение, в котором объясняются трудности статических вызовов функций.

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

Init::loadConfig('settings.php');
Init::setErrorHandler(APP_MODE); 
Init::loggingMode(APP_MODE);

// start loading app related objects ..
$app = new App();

//После прочтения сообщения я теперь нацеливаюсь на это вместо этого...

$init = new Init();
$init->loadConfig('settings.php');
$init->loggingMode(APP_MODE);
 // etc ...

Но, несколько десятков тестов, которые я написал для этого класса, одинаковы. Я ничего не изменил, и они все еще проходят. Я что-то делаю неправильно?

Автор сообщения утверждает следующее:

Основная проблема со статическими методами - это процедурный код. Я понятия не имею, как модульный код с модульным тестированием. Unit-testing предполагает, что я могу создать экземпляр части приложения отдельно. Во время создания я связываю зависимости с mocks/friendlies, которые заменяют реальные зависимости. При процедурной программировании ничего не нужно "прокладывать", поскольку нет объектов, код и данные являются отдельными.

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

Я буду избегать статических методов, но мне понравилось бы иметь представление о КОГДА статические методы полезны, если вообще. Похоже, что этот пост статические методы почти так же злы, как и глобальные переменные, и их следует избегать как можно больше.

Приветствуется любая дополнительная информация или ссылки на эту тему.

4b9b3361

Ответ 1

Статические методы сами по себе не сложнее тестировать, чем методы экземпляров. Проблема возникает, когда метод - статический или другой - вызывает другие статические методы, потому что вы не можете изолировать тестируемый метод. Вот типичный примерный метод, который трудно проверить:

public function findUser($id) {
    Assert::validIdentifier($id);
    Log::debug("Looking for user $id");  // writes to a file
    Database::connect();                 // needs user, password, database info and a database
    return Database::query(...);         // needs a user table with data
}

Что вы можете протестировать с помощью этого метода?

  • Передача чего-либо другого, кроме положительного целого числа, бросает InvalidIdentifierException.
  • Database::query() получает правильный идентификатор.
  • Соответствующий пользователь возвращается, когда найден, null, если нет.

Эти требования просты, но вы также должны настраивать ведение журнала, подключаться к базе данных, загружать ее с данными и т.д. Класс Database должен нести исключительную ответственность за проверку того, что он может подключаться и запрашивать. Класс Log должен делать то же самое для ведения журнала. findUser() не должен иметь дело с этим, но он должен, потому что это зависит от них.

Если вместо этого вышеописанный метод вызвал методы экземпляра в экземплярах Database и Log, тест может проходить в макетных объектах со значениями, возвращаемыми по сценарию, специфичными для теста под рукой.

function testFindUserReturnsNullWhenNotFound() {
    $log = $this->getMock('Log');  // ignore all logging calls
    $database = $this->getMock('Database', array('connect', 'query');
    $database->expects($this->once())->method('connect');
    $database->expects($this->once())->method('query')
             ->with('<query string>', 5)
             ->will($this->returnValue(null));
    $dao = new UserDao($log, $database);
    self::assertNull($dao->findUser(5));
}

Вышеуказанный тест завершится неудачей, если findUser() пренебрегает вызовом connect(), передает неправильное значение для $id (5 выше) или возвращает что-либо, кроме null. Красота заключается в том, что никакая база данных не задействована, что делает тест быстрым и надежным, что означает, что он не сбой по причинам, не связанным с тестом, например, сбой сети или данные с плохими образцами. Это позволяет сосредоточиться на том, что действительно имеет значение: функциональность, содержащаяся в findUser().

Ответ 2

Себастьян Бергман согласен с Мишко Хевери и часто цитирует его:

Тестирование модулей требует швов, швы - это то место, где мы предотвращаем выполнение нормального кода, и как мы достигаем изоляции тестируемого класса. Швы работают через полиморфизм, мы переопределяем/реализуем класс/интерфейс, а затем проводя тестируемый класс по-разному, чтобы контролировать поток выполнения. С помощью статических методов переопределить нечего. Да, статические методы легко вызвать, но если статический метод вызывает другой статический метод, нет способа переопределить зависимость вызываемого метода.

Основная проблема со статическими методами заключается в том, что они вводят связь, как правило, путем жесткого кодирования зависимости в вашем потребительском коде, что затрудняет их замену заглушками или макетами в ваших модульных тестах. Это нарушает Открытый/Закрытый принцип и Принцип инверсии зависимостей, два принципа SOLID.

Вы абсолютно правы, что статика считается вредной. Избегайте их.

Проверьте ссылки для получения дополнительной информации.

Обновление: обратите внимание, что, хотя статика по-прежнему считается вредной, возможность заглушения и издевательства статических методов была удалена с PHPUnit 4.0

Ответ 3

Я не вижу никаких проблем при тестировании статических методов (по крайней мере, ни один из них не существует в нестатических методах).

  • Макетные объекты передаются в тестируемые классы с использованием инъекции зависимостей.
  • Моческие статические методы могут быть переданы в тестируемые классы с использованием подходящего автозагрузчика или для управления include_path.
  • Позднее статическое связывание связывается с методами, вызывающими статические методы в одном классе.