Модульное тестирование и статические методы
Чтение и сбор данных на модульном тестировании, пытаясь понять следующее сообщение, в котором объясняются трудности статических вызовов функций.
Я не понимаю этого. Я всегда считал, что статические функции были хорошим способом округления функций полезности в классе. Например, я часто использую вызовы статических функций для инициализации, то есть:
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, которые заменяют реальные зависимости. При процедурной программировании ничего не нужно "прокладывать", поскольку нет объектов, код и данные являются отдельными.
Теперь, я понимаю из сообщения, что статические методы создают зависимости, но не понимают интуитивно, почему невозможно проверить возвращаемое значение статического метода так же легко, как обычный метод?
Я буду избегать статических методов, но мне понравилось бы иметь представление о КОГДА статические методы полезны, если вообще. Похоже, что этот пост статические методы почти так же злы, как и глобальные переменные, и их следует избегать как можно больше.
Приветствуется любая дополнительная информация или ссылки на эту тему.
Ответы
Ответ 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
.
- Позднее статическое связывание связывается с методами, вызывающими статические методы в одном классе.