Единичное тестирование личного кода

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

Мой вопрос об модульном тестировании класса:

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

Но... как насчет следующих случаев:

Пример 1. Обработать результат запроса на восстановление async-данных (метод обратного вызова не должен быть общедоступным для тестирования)

Пример 2. Обработчик событий, выполняющий операцию (например, обновление текста метки представления - глупый пример, который я знаю...)

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

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

У кого-нибудь есть мысли об этом?

Cheers, Джейсон

EDIT: Я не думаю, что я был достаточно ясен в своем первоначальном вопросе - я могу тестировать частные методы с помощью аксессуаров и издеваться над вызовами/методами с помощью TypeMock. Это не проблема. Проблема заключается в проверке вещей, которые не должны быть общедоступными или не могут быть общедоступными.

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

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

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

Приветствия, Джейсон

Ответы

Ответ 1

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

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

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

MyClass obj = new MyClass();
MethodInfo methodInfo = obj.GetType().GetMethod("MethodName", BindingFlags.Instance | BindingFlags.NonPublic);
object result = methodInfo.Invoke(obj, new object[] { "asdf", 1, 2 });
// assert your expected result against the one above

Ответ 2

имеем цикломатическое правило, которое утверждает что ни один метод не должен иметь цикломатическая сложность более 5

Мне нравится это правило.

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

Если у вас есть частные методы со сложной логикой, подумайте об их реорганизации в отдельный класс. Это также может помочь снизить циклическую сложность. Другой вариант - сделать метод внутренним и использовать InternalsVisibleTo (упомянутый в одной из ссылок в ответе Криса).

Уловы, как правило, появляются, когда у вас есть внешние зависимости, на которые ссылаются частные методы. В большинстве случаев вы можете использовать такие методы, как Dependency Injection, чтобы разделить ваши классы. Для вашего примера с сторонней структурой это может быть сложно. Сначала я попытаюсь реорганизовать проект для разделения зависимостей сторонних разработчиков. Если это невозможно, рассмотрите возможность использования Typemock Isolator. Я не использовал его, но его ключевой особенностью является возможность "издеваться" над частными, статическими и т.д. Методами.

Классы - это черные ящики. Проверьте их таким образом.

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

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

Ответ 3

Существует только два случая, которые вам нужно учитывать:

  • частный код вызывается прямо или косвенно из открытого кода и
  • частный код не вызывается из открытого кода.

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

Ergo: нет необходимости явно тестировать закрытый код.

Обратите внимание, что при TDD невозможно, чтобы непроверенный личный код существовал даже. Потому что, когда вы делаете TDD, единственный способ, которым может отображаться частный код, - это репликация Extract {Method | Class |...} из общедоступного кода. И Рефакторинги, по определению, сохраняют поведение и, следовательно, сохраняют покрытие. И единственный способ, которым может выглядеть публичный код, - это результат неудачного теста. Если публичный код может отображаться только как уже протестированный код в результате неудачного теста, а частный код может появляться только в результате извлечения из открытого кода с помощью рефакторинга, сохраняющего поведение, из этого следует, что непроверенный личный код никогда не появится.

Ответ 4

Во всех моих модульных тестах я никогда не беспокоился о тестировании функций private. Обычно я просто тестировал функции public. Это согласуется с методологией тестирования Black Box Testing.

Вы правы, что действительно не можете проверить частные функции, если вы не открываете частный класс.

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

РЕДАКТИРОВАТЬ: поиск SO для этой темы, я столкнулся с этим вопросом. Самый проголосовавший ответ подобен моему ответу.

Ответ 5

Несколько очков от парня TDD, который стучал в С#:

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

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

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

4) Вы можете получить класс "Testable" из оригинала, в котором тривиальной задачей является создание нового общедоступного метода, который ничего не делает, кроме вызова старого частного метода. Испытаемый класс является частью тестовой структуры, а не частью производственного кода, поэтому для него очень удобно иметь специальный доступ. Протестируйте его в тестовой структуре, как если бы она была общедоступной.

Все это делает довольно тривиальным проведение тестов методов, которые в настоящее время являются частными вспомогательными методами, без испорчения работы intellisense.

Ответ 6

Здесь есть отличные ответы, и я в основном согласен с повторным советом прорастать новые классы. Однако для вашего примера 3 есть проницательная, простая техника:

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

Скажем, MyClass расширяет FrameworkClass. Пусть MyTestableClass расширяет MyClass, а затем предоставляет общедоступные методы в MyTestableClass, которые предоставляют защищенные методы MyClass, которые вам нужны. Не большая практика - это своего рода средство для плохого дизайна - но полезно по случаю и очень просто.

Ответ 7

Работают ли файлы-помощники? http://msdn.microsoft.com/en-us/library/bb514191.aspx Я никогда не работал с ними напрямую, но я знаю, что сотрудник использовал их для тестирования частных методов в некоторых Windows Forms.

Ответ 8

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

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

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

Я знаю, его не очень, но я думаю, что он работает хорошо.

Ответ 9

Я соглашусь, что, когда недавно пишут тесты для С#, я обнаружил, что многие трюки, которые я знал для Java, действительно не применялись (в моем случае это было тестирование внутренних классов).

Например, 1, если вы можете подделать/высмеять обработчик поиска данных, вы можете получить доступ к обратному вызову через подделку. (Большинство других языков, которые, как я знаю, используют обратные вызовы, также не делают их закрытыми).

Например, я бы посмотрел на запуск события, чтобы проверить обработчик.

Пример 3 - пример шаблона шаблона, который существует на других языках. Я видел два способа сделать это:

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

  • Расширьте класс снова с помощью дополнительных крючков, которые позволяют напрямую обращаться к защищенным методам.

Ответ 10

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

Есть методы (mocks, stub), чтобы выполнить правильное тестирование черного ящика. Посмотрите их.

Ответ 11

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

SRP - wikipedia/pdf

Это также приводит к более надежному и адаптируемому коду, поскольку принцип единой ответственности на самом деле просто говорит о том, что ваш класс должен иметь высокий cohesion.

Ответ 12

В С# вы можете использовать атрибут в AssemblyInfo.cs:

[assembly: InternalsVisibleTo("Worker.Tests")]

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