Как далеко я должен идти с юнит-тестированием? - PullRequest
6 голосов
/ 29 июля 2009

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

У меня есть метод, который генерирует маркер сброса пароля в случае, если пользователь забыл свой пароль. Метод возвращает одну из двух вещей: ничего (ноль), если все работало нормально, или код ошибки, указывающий, что пользователь с указанным именем пользователя не существует.

Если я только тестирую общедоступный интерфейс, как я могу быть уверен, что токен сброса пароля будет в базе данных, если имя пользователя допустимо, и не будет в базе данных, если имя пользователя НЕ допустимо? Должен ли я делать запросы в моих тестах, чтобы проверить это? Или я просто должен предположить, что моя логика верна?

Теперь этот метод очень прост, и это не так уж важно - проблема в том, что эта же ситуация применима ко многим другим методам. Что вы делаете в модульных тестах, ориентированных на базу данных?

Код, для справки, если необходимо:

public function generatePasswordReset($username)
{
    $this->sql='SELECT  id
                FROM    users
                WHERE   username = :username';

    $this->addParam(':username', $username);
    $user=$this->query()->fetch();

    if (!$user)
        return self::$E_USER_DOESNT_EXIST;
    else
    {
        $code=md5(uniqid());
        $this->addParams(array(':uid'        => $user['id'],
                               ':code'       => $code,
                               ':duration'   => 24 //in hours, how long reset is valid
                              ));

        //generate new code, delete old one if present
        $this->sql ='DELETE FROM password_resets WHERE user_id=:uid;';
        $this->sql.="INSERT INTO password_resets (user_id, code, expires)
                     VALUES      (:uid, :code, now() + interval ':duration hours')";

        $this->execute();
    }
}

Ответы [ 8 ]

6 голосов
/ 29 июля 2009

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

//1. get the user from the DB
//2. in a big else, check if user is null
//3. create a array containing the userID, a code, and expiry
//4. delete any existing password resets
//5. create a new password reset

Модульное тестирование также прекрасно, потому что оно помогает выделить зависимости. Этот метод, как показано выше, зависит от БД, а не от объекта, который реализует интерфейс. Этот метод взаимодействует с системами, находящимися вне его области применения, и в действительности его можно протестировать только с помощью интеграционного теста, а не модульного теста. Модульные испытания предназначены для обеспечения работоспособности / правильности единицы работы.

Рассмотрим принцип единой ответственности : " Сделай одну вещь ". Это относится как к методам, так и к классам.

Я бы предложил, чтобы ваш generatePasswordReset метод был изменен на:

  • получить заранее определенный существующий пользовательский объект / идентификатор. Сделайте все эти проверки здравомыслия вне этого метода. Сделай одну вещь.
  • поместите код сброса пароля в свой собственный метод. Это была бы единица работы, которую можно было бы протестировать независимо от SELECT, DELETE и INSERT.
  • Создайте новый метод, который можно назвать OverwriteExistingPwdChangeRequests(), который позаботился бы о DELETE + INSERT.
3 голосов
/ 29 июля 2009

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

Один из способов справиться с обновлениями состояния на удаленных объектах, подобных этому, - создать фиктивный объект, который обеспечивает тот же интерфейс, что и БД (т. Е. Он выглядит идентично с точки зрения вашего кода). Затем в своем тесте вы можете проверить изменения состояния в этом фиктивном объекте и подтвердить, что вы получили то, что должны.

1 голос
/ 30 июля 2009

Юнит-тесты служат для проверки работоспособности юнита. Если вы хотите знать, работает ли устройство или нет, напишите тест. Это так просто. Выбор написания модульного теста или нет не должен основываться на некотором графике или практическом руководстве. Как профессионал, вы несете ответственность за предоставление рабочего кода, и вы не можете знать, работает ли он или нет, если вы не протестируете его.

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

Если вы спрашиваете себя «как я знаю, работает ли эта функция», ответьте «вы не знаете, пока не пройдете повторяющиеся тесты, доказывающие, что она работает».

1 голос
/ 29 июля 2009

Вы можете разбить его еще немного, эта функция делает много, что делает тестирование немного сложным, не невозможным, но сложным. Если, с другой стороны, вы вытащили несколько меньших дополнительных функций (getUserByUsername, deletePasswordByUserID, addPasswordByUserId и т. Д. Затем вы можете достаточно легко протестировать их один раз и знать, что они работают, поэтому вам не нужно тестировать их снова. Таким образом, вы тестируете нижние Отключение вызовов, чтобы убедиться, что они здоровы, поэтому вам не нужно беспокоиться о них дальше по цепочке. Тогда для этой функции все, что вам нужно сделать, это выбросить пользователя, которого не существует, и убедиться, что он возвращается с ошибкой USER_DOESNT_EXIST затем тот, где пользователь существует (здесь вы тестируете БД). Внутренние работы уже были выполнены где-то еще (надеюсь).

0 голосов
/ 30 июля 2009

Если ваши юнит-тесты имеют побочные эффекты (например, изменение базы данных), они становятся интеграционными тестами . В интеграционных тестах нет ничего плохого; Любое автоматическое тестирование хорошо для качества вашего продукта. Но интеграционные тесты имеют более высокую стоимость обслуживания , потому что они более сложные и их легче сломать.

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

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

См. Документацию SimpleTest (фреймворк php) для примера.

0 голосов
/ 30 июля 2009

Базы данных являются глобальными переменными. Глобальные переменные - это открытые интерфейсы для каждого модуля, который их использует. Поэтому ваши тесты должны варьировать входные данные не только для параметра функции, но и для входных данных базы данных.

0 голосов
/ 29 июля 2009

Тестирование открытого интерфейса необходимо, но не достаточно. Есть много философий о том, сколько тестов требуется, и я могу только высказать свое собственное мнение. Проверьте все. В прямом смысле. У вас должен быть тест, который проверяет, что каждая строка кода была выполнена набором тестов. (Я говорю «каждая строка», потому что я думаю о C и gcov, а gcov обеспечивает детализацию на уровне строк. Если у вас есть инструмент с более высоким разрешением, используйте его.) Если вы можете добавить кусок кода в свой код базы без добавления теста, набор тестов должен завершиться ошибкой.

0 голосов
/ 29 июля 2009

В общем случае можно «насмехаться» над вызываемым объектом, проверяя, получает ли он ожидаемые запросы.

В этом случае я не уверен, насколько это полезно, вы в конечном итоге пишете одну и ту же логику дважды ... мы думали, что отправили «УДАЛИТЬ из пароля» и т. Д. О, смотри, мы сделали!

Хммм, что мы на самом деле проверили. Если строка была плохо сформирована, мы не узнаем!

Возможно, это противоречит букве закона модульного тестирования, но я бы вместо этого проверил эти побочные эффекты, выполнив отдельные запросы к базе данных.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...