Частный код модульного тестирования

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

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

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

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

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

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

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

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

Ни у кого нет мыслей по этому поводу?

Ура, Джейсон

РЕДАКТИРОВАТЬ: Я не думаю, что был достаточно ясен в своем исходном вопросе - я могу тестировать частные методы с помощью аксессоров и имитировать вызовы / методы с помощью TypeMock. Проблема не в этом. Проблема заключается в тестировании вещей, которые не должны быть общедоступными или не могут быть общедоступными.

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

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

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

Ура, Джейсон


person Jason    schedule 01.10.2009    source источник
comment
какую изоляционную структуру вы будете использовать?   -  person Russ Cam    schedule 02.10.2009
comment
Я использую Typemock Isolator. Имитировать вызовы методов легко - мне просто не нравится это делать, так как это меняет поведение тестируемого класса!   -  person Jason    schedule 03.10.2009
comment
Все это заставляет меня желать, чтобы я мог написать свой собственный фреймворк с нуля, чтобы я мог спроектировать эти проблемы с самого начала !!!   -  person Jason    schedule 03.10.2009


Ответы (12)


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

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

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

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
person Matt    schedule 01.10.2009
comment
+1, это довольно мило. Я никогда не знал, что отражение может сделать это. - person Chris; 02.10.2009
comment
Вы также можете сделать это, используя частный аксессор и (необязательно) передав частный объект, который обертывает ваш реальный экземпляр, что намного чище. Я думаю, что это также предотвращает любые проблемы с отражением, когда вы приходите подписывать свои классы (не цитируйте меня по этому поводу - мне еще никогда не приходилось этого делать !!) - person Jason; 03.10.2009

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

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

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

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

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

Классы - это черные ящики. Испытайте их таким образом.

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

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

person TrueWill    schedule 02.10.2009
comment
Спасибо за ответ - я должен был упомянуть, что я использую TypeMock Isolator, и насмешка классов, классов для методов не является проблемой. И справиться с частным кодом тоже не проблема. Проблема в том, что (возьмем пример 1), если я имитирую класс, который выполняет возврат данных, я не могу заставить его вызвать обратный вызов. (Пример 2) Я не могу тестировать обработчики частных событий, не копаясь в частных методах класса. - person Jason; 03.10.2009

На самом деле вам нужно рассмотреть только два случая:

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

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

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

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

person Jörg W Mittag    schedule 02.10.2009
comment
Я не верю, что это правда, когда вы рассматриваете первый или второй приведенный мною пример. Вы утверждаете, что частный код может существовать только как прямой результат рефакторинга общедоступного кода, но это не объясняет, как тестировать код для обработчиков событий или асинхронных обратных вызовов. Это две настоящие проблемы, с которыми я не могу разобраться! - person Jason; 03.10.2009

На протяжении всего моего модульного тестирования я никогда не утруждал себя тестированием private функций. Обычно я просто тестировал public функции. Это согласуется с методологией тестирования черного ящика.

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

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

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

person Chris    schedule 01.10.2009
comment
ааа, но вы действительно можете протестировать частные классы :). Должен быть заполнен, но я должен ?, но на самом деле это возможно. И были случаи, когда я считал это полезным. - person Matt; 02.10.2009

Несколько замечаний от специалиста по TDD, который копался в C #:

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

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

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

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

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

person Tim Ottinger    schedule 02.10.2009

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

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

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

person Carl Manaster    schedule 03.10.2009

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

person Samantha Branham    schedule 02.10.2009

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

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

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

Я знаю, это некрасиво, но я думаю, что это хорошо работает.

person Frank Schwieterman    schedule 02.10.2009
comment
Что касается вашего первого предложения - я бы не хотел этого делать, поскольку с помощью простого приведения типов вы можете ввести огромную защиту безопасности в свое приложение. Ваше предложение по обработке обратных вызовов примерно столько, сколько я получил с тестированием асинхронных методов ... Я фактически закончил тем, что изменил код обратно на частный и изменил свои тесты, чтобы протестировать частную реализацию с использованием аксессоров в конце, поскольку я не хотел код публикуется. Это просто немного "пахнет" :( - person Jason; 03.10.2009
comment
Можете ли вы расширить брешь в безопасности, связанную с кастингом? - person Frank Schwieterman; 05.10.2009
comment
Привет, Фрэнк! Используя немного рефлексии, нетрудно определить фактический тип интерфейса. Тогда можно было бы вернуть объект, реализующий интерфейс, к его исходному типу и получить доступ к общедоступным методам, к которым у вас не было бы доступа через интерфейс. Насколько я понимаю, (и в данном случае) суть интерфейса не в безопасности, а в том, чтобы указать разработчику правильное использование класса (или полиморфизма). - person Jason; 06.10.2009
comment
Используя отражение, можно было бы вызывать частные методы напрямую, независимо от того, находится ли класс за интерфейсом или нет. Если кто-то может внедрить код в ваш процесс и сделать что-то подобное, это проблема безопасности, а не то, что они будут делать после этого. Это скорее проблема защитного кодирования, чем проблема безопасности как таковая. Защитное кодирование может предотвратить только разумное неправильное использование, и я думаю, что это достигается, если вызывающие абоненты обращаются к компоненту через интерфейс. - person Frank Schwieterman; 06.10.2009

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

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

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

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

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

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

person Kathy Van Stone    schedule 01.10.2009

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

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

person Virgil Dupras    schedule 02.10.2009

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

SRP - wikipedia / pdf

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

person Ty.    schedule 02.10.2009
comment
Как бы вы отнеслись к примеру 1 в этом случае? - person Frank Schwieterman; 03.10.2009
comment
Мне нужно было бы увидеть код, но сразу я бы сказал, что при извлечении файла самому не нужно знать, что он был асинхронным. Вы должны реализовать эту функцию отдельно и ее расширение, поддерживающее асинхронную передачу. В этом случае я предполагаю, что вы полностью используете асинхронные запросы и обрабатываете результаты в обратном вызове. Оба они уже четко разделены. Один отправляет, другой получает. Тестовая отправка. Получение теста. Испытайте оба вместе. Больше информации станет достоянием общественности, но не в вашем существующем контексте. - person Ty.; 03.10.2009
comment
Вот пример того, как я пытаюсь реализовать ваше предложение (поправьте меня, если я ошибаюсь!) У меня есть класс (C1), который может получать данные асинхронно. Мой вызывающий класс (C2) вызывает класс получения данных. Вызывающий класс не знает, сколько времени занимает асинхронный вызов (как по определению в другом потоке). Класс получения данных сигнализирует о том, что он получил данные. Затем C2 извлекает данные из C1. Итак, как я могу протестировать это полностью, не вводя условие гонки? Тестирование отправки и тестирования приема - это нормально, но как мне протестировать и то, и другое вместе? - person Jason; 03.10.2009

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

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

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

person jlo-gmail    schedule 29.12.2016
comment
В следующий раз отметьте вопрос как повторяющийся, а не отправляйте несколько одинаковых ответов. Спасибо! - person Rob; 29.12.2016