Статьи

Никогда не смешивайте публичные и частные юнит-тесты!

Мне кажется, что разработчики часто не проводят различий между различными типами модульных тестов, которые они должны писать, и, таким образом, смешивают вещи, которые не следует смешивать, что приводит к сложному сопровождению и сложному развитию тестового кода. Размер категоризации модульных тестов. Я чувствую, что здесь особенно важен уровень связи с тестируемым модулем и его внутренними компонентами. Мы должны постоянно знать, какого рода тест мы пишем, чего мы пытаемся достичь с помощью теста, и, следовательно, что означает, что оно оправдано или, наоборот, не подходит для такого теста. Другими словами, мы всегда должны знать, пишем ли мы публичный тест или приватный тести никогда не смешивать два. Почему нет и что на самом деле представляют собой эти два вида тестов? Читать дальше! (И если вы согласны или не согласны, не стесняйтесь поделиться своим мнением.)

мотивация

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

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

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

Публичный юнит тест (юнит = класс)

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

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

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

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

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

Общедоступные юнит-тесты также известны как тесты в оба конца, и они воплощают принцип тестирования. Сначала используйте парадную дверь .

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

Пример:

public class ArrayBasedStackTest {
   ...
   @Test public void pop_returns_pushed_in_reverse_order() {
      stack.push(1);
      stack.push(2);

      assertEquals(2, stack.pop());
      assertEquals(1, stack.pop());
   }
}

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

Частный (вспомогательный) юнит тест (юнит = метод)

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

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

Если вы разделяете публичные и частные тесты, то вы можете хранить частные тесты только в том случае, если их стоимость превышает их стоимость, и отбрасывать их, когда они становятся слишком обременительными. Вы можете позволить себе удалить их, потому что знаете, что контракт класса — что действительно важно в долгосрочной перспективе — охватывается публичными тестами. В зависимости от вашей ситуации, вы можете выбросить приватный тест, как только будет разработана соответствующая функциональность (в этом случае они служат вам только для разработки [даже Кент Бек делает это иногда — см. Эпизод 3 ]), или вы можете оставить это до первого большого рефакторинга, который изменяет внутреннее устройство класса таким образом, что тесно связанный тест не может выжить.

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

Пример:

public class ArrayBasedStackPrivateTest {
   ...
   @Test public void test_growIfNecessary() {
      ArrayBasedStack zeroSizedStack = new ArrayBasedStack(0);
      // it has the package-private fields int[] content; int topIdx = 0

      int originalStackSize = zeroSizedStack.content.length;
      assertEquals(0, originalStackSize);
      assertEquals(0, zeroSizedStack.topIdx);

      zeroSizedStack.growIfNecessary();

      assertTrue("The stack hasn't grown; size: " + originalStackSize
            , zeroSizedStack.content.length > originalStackSize);
   }
}

Этот приватный тест помогает мне проверить «приватный» метод стека, не имея дело с его pop (). Ему не нужно устанавливать какое-либо частное состояние (как конструктор делает это для меня), но он использует свой привилегированный доступ для проверки «частных» свойств, представляющих состояние, таким образом предоставляя мне гораздо лучшее понимание потенциального сбоя. (На самом деле я сначала написал общедоступный модульный тест, но он не удался, потому что я увеличивал стек, удваивая его размер — который на самом деле не работает с нулем. Уже написав утверждения в этом частном тесте, я понял это.) с тестом публичного и частного юнитов, и доступ к внутреннему состоянию может быть не столь убедительным в этом простом случае, но вы, конечно, можете подумать о реальном случае из вашего опыта, где он был бы более очевидным.

Примечание по терминологии

Резюме: договор <=> публичный, детали реализации <=> частный, единица <=> изолирован

Термины «общедоступный» и «частный», которые я здесь использую, отражают концептуальное различие между контрактом, который класс имеет с остальной частью системы — который, таким образом, является его «общедоступным» API — и подробностями его реализации, которые являются «частными». к классу в смысле принципа инкапсуляции старого доброго ООП. Они не имеют прямого отношения к ключевым словам «public» и «private», используемым в Java, хотя контракт обычно представлен открытыми методами, в то время как детали реализации часто скрыты в частных методах (хотя, чтобы сделать их тестируемыми, обычно лучше сделать их пакетными). -приватный, если вы не пишете тесты в Groovy ).

Термин «единица» используется довольно свободно. Согласно недавнему твиту Кента Бека, вы можете распознать модульное тестирование по тому факту, что в случае его неудачи вы точно знаете, в чем проблема, и какую часть кода, возможно, даже какую строку проверить. Если тест не пройден, и вы не можете сказать, почему, тогда это не модульный тест. Отсюда следует, что многие тесты на уровне методов не являются на самом деле модульными тестами в этом строгом смысле, и особенно публичные тесты, как правило, являются фактически низкоуровневыми интеграционными тестами, где единицей интеграции является класс (и его непубличные методы). Размер тестируемой «единицы» является еще одним аспектом категоризации теста. В менее строгом смысле можно сказать, что тест — это своего рода модульный тест (определенного уровня), если он проверяет модуль изолированно.В этом смысле, основанном на изоляции, я нарисовал примерное равенство между Public Test и классом (его целью является проверка контракта класса) и Private Test и методом. Тестовый метод в Public Test обычно вызывает один или несколько открытых методов для своего целевого объекта и иногда для некоторых других объектов для проверки егокосвенные (но все же «публичные») результаты . Тестовый метод в Private Test будет использовать «закрытые» поля и установщики для настройки целевого объекта, выполнения непубличного тестируемого метода и проверки его вывода и, возможно, некоторых (общедоступных или непубличных) полей / получателей.

Резюме

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

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

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

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

Обновление 8/11: недавний комментарий в DZone показывает, что я недостаточно ясно дал понять. Поэтому я хотел бы подчеркнуть, что публичные и частные юнит-тесты являются моими собственными терминами и не имеют ничего общего с интеграционным тестированием «белый ящик» и «черный ящик». Как частные, так и публичные UT тестируют один класс изолированно — они отличаются только тем, насколько они зависят от внутренней организации класса. Общедоступные UT проверяют только общедоступные методы и, таким образом, обычно являются более грубыми, частные UT проверяют непубличные методы, как правило, настолько изолированно, насколько это возможно, даже если это требует знания и изменения внутреннего, частного состояния тестируемого объекта.

Ресурсы

  1. Book xUnit Test Patterns: рефакторинг тестового кода (2007)

 

От http://theholyjava.wordpress.com/2011/10/20/never-mix-public-and-private-unit-tests/