Каковы принципы проектирования, которые продвигают тестируемый код? (разработка тестируемого кода против проектирования вождения с помощью тестов)


54

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

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

Я читал, что есть нечто, известное как ТВЕРДЫЙ. Я хочу понять, приводит ли косвенное следование принципам SOLID к коду, который легко тестируется? Если нет, есть ли какие-то четко определенные принципы проектирования, которые продвигают тестируемый код?

Я знаю, что есть что-то, известное как Test Driven Development. Хотя я больше заинтересован в разработке кода, ориентируясь на тестирование на самой стадии разработки, а не на разработку тестов. Я надеюсь это имеет смысл.

Еще один вопрос, связанный с этой темой: можно ли повторно анализировать существующий продукт / проект и вносить изменения в код и дизайн с целью написания модульного теста для каждого модуля?


3
Посмотрите на это: googletesting.blogspot.in/2008/08/...
VS1

Спасибо. Я только начал читать статью, и это уже имеет смысл.

1
Это один из моих вопросов на собеседовании («Как вы разрабатываете код, чтобы его можно было легко тестировать модулем?»). Он показывает мне один раз, понимают ли они модульное тестирование, насмешку / заглушку, OOD и, возможно, TDD. К сожалению, ответы, как правило, что-то вроде «Сделать тестовую базу данных».
Крис Питман

Ответы:


57

Да, SOLID - это очень хороший способ разработки кода, который можно легко протестировать. Как краткий учебник:

S - Принцип единой ответственности: объект должен делать ровно одну вещь и должен быть единственным объектом в кодовой базе, который делает эту одну вещь. Например, возьмите класс домена, скажем, Счет. Класс Invoice должен представлять структуру данных и бизнес-правила счета-фактуры, используемые в системе. Это должен быть единственный класс, который представляет счет в базе кода. Это может быть далее разбито, чтобы сказать, что метод должен иметь одну цель и должен быть единственным методом в кодовой базе, который удовлетворяет эту потребность.

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

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

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

L - Принцип замещения Лискова: класс A, зависящий от класса B, должен иметь возможность использовать любой X: B, не зная разницы. В основном это означает, что все, что вы используете в качестве зависимости, должно иметь поведение, аналогичное тому, которое видит зависимый класс. В качестве короткого примера, скажем, у вас есть интерфейс IWriter, который предоставляет запись (строку), которая реализована ConsoleWriter. Теперь вместо этого вам нужно записать файл, поэтому вы создаете FileWriter. При этом вы должны убедиться, что FileWriter может использоваться так же, как ConsoleWriter (это означает, что единственный способ, которым зависимый может взаимодействовать с ним, - это вызов Write (строка)), и, таким образом, дополнительная информация, которая может понадобиться FileWriter для этого. задание (например, путь и файл для записи) должно быть предоставлено откуда-то еще, кроме зависимого.

Это огромно для написания тестируемого кода, потому что дизайн, который соответствует LSP, может иметь «поддельный» объект, заменяющий реальную вещь в любой точке без изменения ожидаемого поведения, позволяя тестировать небольшие куски кода изолированно с уверенностью что система будет работать с подключенными реальными объектами.

I - Принцип сегрегации интерфейса: Интерфейс должен иметь как можно меньше методов, чтобы обеспечить функциональность роли, определенной интерфейсом . Проще говоря, чем меньше интерфейс, тем лучше интерфейс. Это связано с тем, что большой интерфейс имеет больше причин для изменения и вызывает больше изменений в других местах кодовой базы, которые могут не потребоваться.

Приверженность ISP улучшает тестируемость, уменьшая сложность тестируемых систем и зависимостей этих SUT. Если объект, который вы тестируете, зависит от интерфейса IDoThreeThings, который предоставляет DoOne (), DoTwo () и DoThree (), вы должны смоделировать объект, который реализует все три метода, даже если объект использует только метод DoTwo. Но, если объект зависит только от IDoTwo (который предоставляет только DoTwo), вы можете легко смоделировать объект, у которого есть этот единственный метод.

D - Принцип обращения зависимостей: Конкреции и абстракции никогда не должны зависеть от других конкреций, а от абстракций . Этот принцип непосредственно навязывает принцип слабой связи. Объект никогда не должен знать, что такое объект; вместо этого его должно волновать, что делает объект. Таким образом, использование интерфейсов и / или абстрактных базовых классов всегда предпочтительнее, чем использование конкретных реализаций при определении свойств и параметров объекта или метода. Это позволяет вам менять одну реализацию на другую без необходимости изменения использования (если вы также следуете LSP, который идет рука об руку с DIP).

Опять же, это огромно для тестируемости, так как позволяет вам еще раз внедрить фиктивную реализацию зависимости вместо «производственной» реализации в тестируемый объект, при этом все еще тестируя объект в точной форме, которую он будет иметь, пока в производстве. Это ключ к модульному тестированию "в изоляции".


16

Я читал, что есть нечто, известное как ТВЕРДЫЙ. Я хочу понять, приводит ли косвенное следование принципам SOLID к коду, который легко тестируется?

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

Исходя из моего опыта, два принципа SOLID играют важную роль в разработке тестируемого кода:

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

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

(...) можно ли повторно анализировать существующий продукт / проект и вносить изменения в код и дизайн с целью написания модульного теста для каждого модуля?

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

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

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


Я имею высокую когезию и низкую связь
JK.

8

ВАШ ПЕРВЫЙ ВОПРОС:

ТВЕРДЫЙ действительно путь. Я считаю, что двумя наиболее важными аспектами аббревиатуры SOLID, когда речь идет о тестируемости, являются S (единая ответственность) и D (внедрение зависимости).

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

Внедрение зависимостей (DI). Это дает вам контроль над средой тестирования. Вместо создания внешних объектов внутри вашего кода, вы внедряете его через конструктор класса или вызов метода. При тестировании юнитов вы просто заменяете реальные классы заглушками или макетами, которые вы полностью контролируете.

ВАШ ВТОРОЙ ВОПРОС: В идеале вы пишете тесты, которые документируют функционирование вашего кода перед его рефакторингом. Таким образом, вы можете задокументировать, что ваш рефакторинг воспроизводит те же результаты, что и исходный код. Однако ваша проблема в том, что работающий код сложно протестировать. Это классическая ситуация! Мой совет: тщательно продумайте рефакторинг перед модульным тестированием. Если вы можете; написать тесты для рабочего кода, затем выполнить рефакторинг кода, а затем рефакторинг тестов. Я знаю, что это будет стоить часов, но вы будете более уверены, что переработанный код работает так же, как старый. Сказав это, я сдался много раз. Классы могут быть такими уродливыми и грязными, что перезапись - единственный способ сделать их тестируемыми.


4

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

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

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

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

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


3

SOLID - отличное начало, по моему опыту, четыре аспекта SOLID действительно хорошо работают с модульным тестированием.

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

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

public interface ISomeInterface
{
    int GetValue();
}  

public class SomeClass : ISomeInterface
{
    public int GetValue()
    {
         return 1;
    }
}

public interface ISomeOtherInterface
{
    bool IsSuccess();
}

public class SomeOtherClass : ISomeOtherInterface
{
     private ISomeInterface m_SomeInterface;

     public SomeOtherClass(ISomeInterface someInterface)
     {
          m_SomeInterface = someInterface;
     }

     public bool IsSuccess()
     {
          return m_SomeInterface.GetValue() == 1;
     }
}

public class SomeFactory
{
     public virtual ISomeInterface GetSomeInterface()
     {
          return new SomeClass();
     }

     public virtual ISomeOtherInterface GetSomeOtherInterface()
     {
          ISomeInterface someInterface = GetSomeInterface();

          return new SomeOtherClass(someInterface);
     }
}

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

Если вы хотите подробнее остановиться на этом, я настоятельно рекомендую прочитать «Искусство модульного тестирования» . Это дает несколько отличных примеров того, как использовать эти принципы, и это довольно быстро читается.


1
Это называется принципом зависимости "инверсии", а не принципом "инъекции".
Матиас Ликкегор Лоренцен
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.