Конфликт приоритетов TDD и инкапсуляции

Я только начал практиковать TDD в своих проектах. Сейчас я разрабатываю проект, используя для тестирования php / zend / mysql и phpunit / dbunit. Я просто немного отвлекся на идею инкапсуляции и подхода, основанного на тестировании. Моя идея инкапсуляции - скрыть доступ к некоторым функциям объекта. Чтобы сделать его более понятным, частные и защищенные функции не могут быть протестированы напрямую (если вы не создадите общедоступную функцию для ее вызова).

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


person Hanseh    schedule 11.04.2010    source источник


Ответы (3)


В кругах TDD есть довольно стандартный ответ. Если в классе есть функции, которые вы хотите скрыть и напрямую протестировать, вам следует создать класс с этой функциональностью. Это отличный пример того, как TDD улучшает ваш дизайн.

В исходном классе эта посторонняя функциональность исчезла, она была заключена в выросший класс, поэтому дизайн исходного класса проще и лучше соответствует Принцип единой ответственности. В классе sprouted извлеченная функциональность является его raison d'etre, поэтому его следует сделать общедоступным и, следовательно, его можно тестировать без модификаций, предназначенных только для тестирования.

person Carl Manaster    schedule 11.04.2010
comment
Именно то, что мне нужно. Класс sprout для меня новичок (также как и другие шаблоны xunit). Большое спасибо - person Hanseh; 11.04.2010
comment
Хорошая книга для этого: Эффективная работа с устаревшим кодом. - person Gutzofter; 21.04.2010

Что касается прекрасного ответа Карла Манастера, есть некоторые недостатки, которые вы должны хотя бы рассмотреть, прежде чем вступить на путь, предложенный Карлом.

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

Похоже, что Карл предлагает переместить некоторые частные методы из вашего класса в новый класс и сделать эти методы общедоступными (чтобы вы могли их протестировать). (Кстати, почему бы просто не сделать их общедоступными в исходном классе?)

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

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

По этим причинам я не согласен с Карлом в том, что его предложение: «… отличный пример того, как TDD улучшает ваш дизайн».

Кроме того, он заявляет: «В исходном классе эта посторонняя функциональность пропала, заключенная в выросший класс, поэтому дизайн исходного класса проще и лучше соответствует принципу единой ответственности».

Я бы сказал, что перемещаемая функциональность вовсе не «посторонняя». Кроме того, «проще» не совсем четко определено: конечно, может быть случай, что простота класса обратно пропорциональна его размеру, но это не означает, что система простейших-возможных классов будет простейшей возможной системой: если в этом случае все классы содержали бы только один метод, а система имела бы огромное количество классов; Можно было бы утверждать, что удаление этого иерархического слоя множественных методов внутри классов сделало бы систему намного более сложной.

Принцип единой ответственности (SRP), кроме того, заведомо субъективен и полностью зависит от уровня абстракции наблюдателя. Это вовсе не тот случай, когда удаление метода из класса автоматически улучшает его соответствие SRP. Класс Printer с 10 методами несет единственную ответственность за печать на уровне абстракции класса. Одним из его методов может быть checkPrinterConnected (), а другим - checkPaper (); на уровне метода это явно отдельные обязанности, но они не предполагают автоматически, что класс следует разбить на другие классы.

Карл заканчивает: «В классе sprouted извлеченная функциональность является его смыслом существования, поэтому его уместно сделать общедоступным и, следовательно, его можно тестировать без модификаций, предназначенных только для тестирования». Важность функциональности (это смысл существования) не является основанием для обоснованности ее публичности. Основанием для приемлемости публичности функциональности является минимизация интерфейса, предоставляемого клиенту, так что функциональность класса доступна для использования, в то время как независимость клиента от реализации функциональности максимальна. Конечно, если вы переносите только один метод в класс sprouted, он должен быть общедоступным. Однако, если вы перемещаете более одного метода, вы должны сделать те методы общедоступными, которые необходимы для успешного использования класса клиентом: эти общедоступные методы могут быть гораздо менее важны, чем некоторые частные методы, от которых вы хотите защитить свой класс. клиент. (В любом случае, я не фанат этой фразы «Raison-d'etre», поскольку важность метода также не совсем четко определена.)

Альтернативный подход к предложению Карла зависит от того, насколько большой вы планируете расти в своей системе. Если он вырастет до менее чем нескольких тысяч классов, то вы можете подумать о том, чтобы создать сценарий для копирования исходного кода в новый каталог, изменить все случаи с «private» на «public» в этом скопированном источнике, а затем написать свой тесты против скопированного источника. У этого есть обратная сторона - время, необходимое для копирования кода, но преимущество сохранения инкапсуляции исходного исходного кода, но при этом возможность тестирования всех методов в скопированной версии.

Ниже приведен сценарий, который я использую для этой цели.

С уважением,

Эд Кирван

! / bin / bash

rm -rf код-копия

echo Создание копии кода ...

mkdir код-копия

cp -r ../www code-copy /

для i в find code-copy -name "*php" -follow; делать

sed -i 's/private/public/g' $i

Выполнено

php run_tests.php

person Ed Kirwan    schedule 12.04.2010
comment
Спасибо .. Это еще одна хорошая вещь, которую стоит учесть. Я буду ждать больше информации и провожу собственное исследование по этой теме. - person Hanseh; 13.04.2010
comment
Не замедлит ли это тестирование? - person Hanseh; 13.04.2010

Я только что прочитал отличную статью о том, как позволить макетам управлять дизайном:

http://www.mockobjects.com/files/usingmocksandtests.pdf

Когда Карл говорит, что «вы должны создать класс с этой функциональностью», автор этой статьи объясняет, как ваши тесты могут помочь вам с помощью фиктивных объектов, как вы можете спроектировать свой класс, чтобы вам 1) не нужно было беспокоиться о невозможности тестировать частные части и, что более важно, 2) как это улучшит ваш дизайн за счет (я перефразирую цитату Карла) выявления соавторов и ролей с правильной ответственностью.

Автор шаг за шагом покажет вам пример, чтобы прояснить свою точку зрения.

Вот еще одна статья с таким же подходом:

http://www.methodsandtools.com/archive/archive.php?id=90

Цитата:

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

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

Группа пионеров гибкой разработки программного обеспечения из Соединенного Королевства также боролась с этим еще в 1999 году. Им пришлось добавить дополнительные методы получения для проверки состояния объектов. Их менеджеру не понравилось все это нарушение инкапсуляции, и он заявил: мне не нужны геттеры в коде! (Mackinnon et al., 2000 и Freeman et al., 2004)

Команде пришла в голову идея сосредоточиться на взаимодействиях, а не на состоянии. Они создали специальный объект для замены соавторов тестируемых объектов. Эти специальные объекты содержат спецификации для ожидаемых вызовов методов. Они называли эти объекты mock-объектами, или для краткости mocks. Исходные идеи были усовершенствованы, что привело к созданию нескольких фреймворков имитирующих объектов для всех распространенных языков программирования: Java (jMock, EasyMock, Mockito), .NET (NMock, RhinoMocks), Python (PythonMock, Mock.py, Ruby (Mocha, RSpec)). , C ++ (mockpp, amop). Дополнительные сведения и ссылки см. На сайте www.mockobjects.com.

person koen    schedule 13.04.2010
comment
Отличная цитата. Так начали сравнивать TDD в лондонской школе или в стиле «извне внутрь» или в стиле «mockist» с классическим TDD, основанным на проверке на основе состояния. Для меня недостатком стиля mockist является то, что он иногда имеет хрупкие тесты, а недостатком классического стиля является то, что он нарушает инкапсуляцию, чтобы иметь возможность проверить тест. - person dragan.stepanovic; 26.04.2015