Pull to refresh

Comments 17

s/пишите/пишете/g+
Слишком уж частая ошибка, не понимаю, как ее все еще допускают.
Напишите об этом автору в ЛС. Писать об этом в комментариях — слишком уж частая ошибка, не понимаю, как ее все еще допускают.
Ну у этой медали есть оборотная сторона: вам при написании теста надо обязательно знать скрытые детали устройства того, что вы тестируете. Например, замОчили вы User::save, а потом кто-то поменял класс и добавил туда еще какой-нибудь UserSettings::save. И все, вы в тесте пропустили этот вызов.

В то же время, DI позволяет явно контролировать, что от чего зависит (в том числе в тестах), так что вышеописанной ситуации там произойти не должно. К тому же, если вы пишете в стиле TDD, то у вас автоматом получается хорошая архитектура, и вам не надо ломать голову, как же покрыть код тестами.

Я вообще в последнее время на основе многих наблюдений склоняюсь к мысли, что юнит-тесты и TDD не могут существовать друг без друга в мало-мальски больших системах. Если вы пишете не в стиле TDD, но при этом делаете тетсы, то вы только называете их «юнит-тестами», на самом же деле это получаются тесты функциональные. В функциональных (и регрессионных, «дымовых» и т.д.) ничего плохого нет — я сам предпочитаю мелкие библиотечки покрывать большим количеством грязных «дымовых» тестов (подчас лучше написать 20 хрупких теста за полчаса, охватывающих мыслимые комбинации, чем пару юнит-тестов за полдня). Но такая техника годтся только для узкого круга задач.
>если вы пишете в стиле TDD, то у вас автоматом получается хорошая архитектура
Эк вы, батенька, загнули:)
Да все правильно. TDD не позволяет тащить сильные зависимости, это в больших системах и является проблемой. Если же мозгов вообще нет, то тут ни TDD ни DI не помогут.
Имхо, тестирование не при чем. Достаточно соблюдать правило Деметры. То есть, объект должен вызывать методы максимум на один уровень ниже (либо свои собственные методы, либо методы у своих прямых зависимостей):

class Test
{
    function goodMethod() {
        $this->dependency->method1();
        $this->another->method2();
    }

    function badMethodInTheClass() {
        $this->dependency->anotherMethod()->method3(); // Very bad!
    }
}


Соблюдая это простое правило, всегда можно любому классу подсунуть нужные моки и протестировать независимо от всего остального кода. DI-контейнеры немного помогают соблюдать это правило, собирая зависимости в один уровень.
Например, замОчили вы User::save, а потом кто-то поменял класс и добавил туда еще какой-нибудь UserSettings::save. И все, вы в тесте пропустили этот вызов.

С использованием АОП можно замочить все вызовы ->save() у всех сущностей, что может быть полезно для реализации и тестирования всевозможных ActiveRecord-ов, активно занимающихся работой с БД.
Да, я понимаю про обратную сторону. И не имею цели переубеждать адептов TDD. Просто хотел бы обладать возможностями Ruby/JavaScript для тестирования. Как ни крути, а в этих сообществах к тестированию относятся более серьезно. Пусть порой и забивают как на законы Деметры, так и на использование DI. Главное, чтобы код тестировался, а кто уже и как, чуть менее важно.

В то же время, DI позволяет явно контролировать, что от чего зависит (в том числе в тестах), так что вышеописанной ситуации там произойти не должно. К тому же, если вы пишете в стиле TDD, то у вас автоматом получается хорошая архитектура, и вам не надо ломать голову, как же покрыть код тестами.


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

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

Ну у этой медали есть оборотная сторона: вам при написании теста надо обязательно знать скрытые детали устройства того, что вы тестируете. Например, замОчили вы User::save, а потом кто-то поменял класс и добавил туда еще какой-нибудь UserSettings::save. И все, вы в тесте пропустили этот вызов.

Не так всё ужасно. В любом нормальном ORM есть один метод для записи в БД. Это один из родительских классов всех моделей. Если замОчить его, то вызов мы никак не пропустим.
Ура. Без ранкита. Можно перестать уродовать свой код ради тестабилити и отвоёвывать автокомплит у DI.
ИМХО, это очень круто и важная фича для мира PHP.
Да, единственная фича доступная только ранкиту это переопределение функций самого PHP. Впрочем, без этого можно жить.
В следующей версии Go! AOP можно будет мочить и системные функции самого PHP ) Заготовочки уже работают, так что скоро ждите новостей.
Насчет тестирования синглтонов. Проблема ту отнюдь не техническая и никакая библиотека ее не решит. Проблема в том что синглтон вносит global scope, то есть поведение методов использующих синглтоны не всегда однозначно, пример:
function add($n){
     retrun $n + Number::instance()->m;
}


результат такого метода зависит не только от значения n но и от состояния синглтона Number.

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

Если мокнуть Number::instance() я никаким образом не буду тестировать глобальное состояние. Наоборот. Я его отключаю.
Мы как раз убираем все внешние зависимости (какими бы они не были) и делаем полноценный юнит тест.
Если бы Number::instance оставался включенным, это был бы интеграционный тест…

То есть, при проходе теста мы ни на строчку не выходим за пределы тестируемого класса, мы не обращаемся к другим классам системы.
Что собственно и является юнит тестированием.
Да, но если вы отключаете глобальное состояние то тест тогда не полный, потому что реальная прога будет использовать это состояние.
Бтв, всегда передаю конфигурация как параметр конструктора ( симфони тоже так делает, если ровно писать) =)
Да, но если вы отключаете глобальное состояние то тест тогда не полный, потому что реальная прога будет использовать это состояние.


Вы определитесь, вам юнит тесты или интеграционные нужны :)
Если вы говорите про «реальную прогу» это уже никак не тест отдельного компонента.
Sign up to leave a comment.

Articles