Comments 17
s/пишите/пишете/g+
Слишком уж частая ошибка, не понимаю, как ее все еще допускают.
Слишком уж частая ошибка, не понимаю, как ее все еще допускают.
0
Ну у этой медали есть оборотная сторона: вам при написании теста надо обязательно знать скрытые детали устройства того, что вы тестируете. Например, замОчили вы User::save, а потом кто-то поменял класс и добавил туда еще какой-нибудь UserSettings::save. И все, вы в тесте пропустили этот вызов.
В то же время, DI позволяет явно контролировать, что от чего зависит (в том числе в тестах), так что вышеописанной ситуации там произойти не должно. К тому же, если вы пишете в стиле TDD, то у вас автоматом получается хорошая архитектура, и вам не надо ломать голову, как же покрыть код тестами.
Я вообще в последнее время на основе многих наблюдений склоняюсь к мысли, что юнит-тесты и TDD не могут существовать друг без друга в мало-мальски больших системах. Если вы пишете не в стиле TDD, но при этом делаете тетсы, то вы только называете их «юнит-тестами», на самом же деле это получаются тесты функциональные. В функциональных (и регрессионных, «дымовых» и т.д.) ничего плохого нет — я сам предпочитаю мелкие библиотечки покрывать большим количеством грязных «дымовых» тестов (подчас лучше написать 20 хрупких теста за полчаса, охватывающих мыслимые комбинации, чем пару юнит-тестов за полдня). Но такая техника годтся только для узкого круга задач.
В то же время, DI позволяет явно контролировать, что от чего зависит (в том числе в тестах), так что вышеописанной ситуации там произойти не должно. К тому же, если вы пишете в стиле TDD, то у вас автоматом получается хорошая архитектура, и вам не надо ломать голову, как же покрыть код тестами.
Я вообще в последнее время на основе многих наблюдений склоняюсь к мысли, что юнит-тесты и TDD не могут существовать друг без друга в мало-мальски больших системах. Если вы пишете не в стиле TDD, но при этом делаете тетсы, то вы только называете их «юнит-тестами», на самом же деле это получаются тесты функциональные. В функциональных (и регрессионных, «дымовых» и т.д.) ничего плохого нет — я сам предпочитаю мелкие библиотечки покрывать большим количеством грязных «дымовых» тестов (подчас лучше написать 20 хрупких теста за полчаса, охватывающих мыслимые комбинации, чем пару юнит-тестов за полдня). Но такая техника годтся только для узкого круга задач.
+6
>если вы пишете в стиле TDD, то у вас автоматом получается хорошая архитектура
Эк вы, батенька, загнули:)
Эк вы, батенька, загнули:)
+7
Да все правильно. TDD не позволяет тащить сильные зависимости, это в больших системах и является проблемой. Если же мозгов вообще нет, то тут ни TDD ни DI не помогут.
0
Имхо, тестирование не при чем. Достаточно соблюдать правило Деметры. То есть, объект должен вызывать методы максимум на один уровень ниже (либо свои собственные методы, либо методы у своих прямых зависимостей):
Соблюдая это простое правило, всегда можно любому классу подсунуть нужные моки и протестировать независимо от всего остального кода. DI-контейнеры немного помогают соблюдать это правило, собирая зависимости в один уровень.
class Test
{
function goodMethod() {
$this->dependency->method1();
$this->another->method2();
}
function badMethodInTheClass() {
$this->dependency->anotherMethod()->method3(); // Very bad!
}
}
Соблюдая это простое правило, всегда можно любому классу подсунуть нужные моки и протестировать независимо от всего остального кода. DI-контейнеры немного помогают соблюдать это правило, собирая зависимости в один уровень.
+3
Например, замОчили вы User::save, а потом кто-то поменял класс и добавил туда еще какой-нибудь UserSettings::save. И все, вы в тесте пропустили этот вызов.
С использованием АОП можно замочить все вызовы ->save() у всех сущностей, что может быть полезно для реализации и тестирования всевозможных ActiveRecord-ов, активно занимающихся работой с БД.
0
Да, я понимаю про обратную сторону. И не имею цели переубеждать адептов TDD. Просто хотел бы обладать возможностями Ruby/JavaScript для тестирования. Как ни крути, а в этих сообществах к тестированию относятся более серьезно. Пусть порой и забивают как на законы Деметры, так и на использование DI. Главное, чтобы код тестировался, а кто уже и как, чуть менее важно.
Доктор, я не могу работать по TDD хотя бы потому, что мне нужно сначала вписать код, проверить синтаксис, посмотреть как этот код будет вписываться в остальную архитектуру, перебрать кучу вариантов внедрения и выбрать оптимальный. TDD мне не позволяет перебирать всё эти варианты, только затормаживает необходимостью, включать туда зависимости.
Я лучше попробую разные варианты, посмотрю на возможные косяки в реализации, и найду из них самый простой, короткий и оптимальный.
Тогда покрою его тестом.
Не так всё ужасно. В любом нормальном ORM есть один метод для записи в БД. Это один из родительских классов всех моделей. Если замОчить его, то вызов мы никак не пропустим.
В то же время, DI позволяет явно контролировать, что от чего зависит (в том числе в тестах), так что вышеописанной ситуации там произойти не должно. К тому же, если вы пишете в стиле TDD, то у вас автоматом получается хорошая архитектура, и вам не надо ломать голову, как же покрыть код тестами.
Доктор, я не могу работать по TDD хотя бы потому, что мне нужно сначала вписать код, проверить синтаксис, посмотреть как этот код будет вписываться в остальную архитектуру, перебрать кучу вариантов внедрения и выбрать оптимальный. TDD мне не позволяет перебирать всё эти варианты, только затормаживает необходимостью, включать туда зависимости.
Я лучше попробую разные варианты, посмотрю на возможные косяки в реализации, и найду из них самый простой, короткий и оптимальный.
Тогда покрою его тестом.
Ну у этой медали есть оборотная сторона: вам при написании теста надо обязательно знать скрытые детали устройства того, что вы тестируете. Например, замОчили вы User::save, а потом кто-то поменял класс и добавил туда еще какой-нибудь UserSettings::save. И все, вы в тесте пропустили этот вызов.
Не так всё ужасно. В любом нормальном ORM есть один метод для записи в БД. Это один из родительских классов всех моделей. Если замОчить его, то вызов мы никак не пропустим.
+1
Результаты опросика печалят :(
0
Ура. Без ранкита. Можно перестать уродовать свой код ради тестабилити и отвоёвывать автокомплит у DI.
ИМХО, это очень круто и важная фича для мира PHP.
ИМХО, это очень круто и важная фича для мира PHP.
+1
Добавил в пост ссылку на видео с демонстрацией AspectMock в действии. От редактора Nettuts Premium Jeffrey Way: jeffrey-way.com/blog/2013/07/24/aspectmock-is-pretty-neat/
+1
Насчет тестирования синглтонов. Проблема ту отнюдь не техническая и никакая библиотека ее не решит. Проблема в том что синглтон вносит global scope, то есть поведение методов использующих синглтоны не всегда однозначно, пример:
результат такого метода зависит не только от значения n но и от состояния синглтона Number.
То что это глобальное состояние можно задать наперед это очень хорошо, но такой тест уже не будет юнит тестом, потому что вы тестируете не только сам метод а еще и глобальное состояние программы. Для написания интеграцыонних тестов метод конечно идеален, но его использование еще не значит что можно начать писать синглтоны повсюду.
function add($n){
retrun $n + Number::instance()->m;
}
результат такого метода зависит не только от значения n но и от состояния синглтона Number.
То что это глобальное состояние можно задать наперед это очень хорошо, но такой тест уже не будет юнит тестом, потому что вы тестируете не только сам метод а еще и глобальное состояние программы. Для написания интеграцыонних тестов метод конечно идеален, но его использование еще не значит что можно начать писать синглтоны повсюду.
+1
Конечно не значит что надо писать синглтоны повсюду. Но ограниченное их число для каких-то глобальных обьектов вполне допустимо.
Например, конфигурацию, вполне неплохо держать в синглтоне.
Если мокнуть Number::instance() я никаким образом не буду тестировать глобальное состояние. Наоборот. Я его отключаю.
Мы как раз убираем все внешние зависимости (какими бы они не были) и делаем полноценный юнит тест.
Если бы Number::instance оставался включенным, это был бы интеграционный тест…
То есть, при проходе теста мы ни на строчку не выходим за пределы тестируемого класса, мы не обращаемся к другим классам системы.
Что собственно и является юнит тестированием.
Например, конфигурацию, вполне неплохо держать в синглтоне.
Если мокнуть Number::instance() я никаким образом не буду тестировать глобальное состояние. Наоборот. Я его отключаю.
Мы как раз убираем все внешние зависимости (какими бы они не были) и делаем полноценный юнит тест.
Если бы Number::instance оставался включенным, это был бы интеграционный тест…
То есть, при проходе теста мы ни на строчку не выходим за пределы тестируемого класса, мы не обращаемся к другим классам системы.
Что собственно и является юнит тестированием.
+1
Да, но если вы отключаете глобальное состояние то тест тогда не полный, потому что реальная прога будет использовать это состояние.
Бтв, всегда передаю конфигурация как параметр конструктора ( симфони тоже так делает, если ровно писать) =)
Бтв, всегда передаю конфигурация как параметр конструктора ( симфони тоже так делает, если ровно писать) =)
0
Sign up to leave a comment.
AspectMock — тестируем любой PHP код