Как вы тестируете частные методы?
Я строю библиотеку классов, которая будет иметь несколько открытых и закрытых методов. Я хочу иметь возможность тестировать приватные методы модульно (в основном при разработке, но также это может быть полезно для будущего рефакторинга).
Как правильно это сделать?
33 ответа
Если вы используете.net, вы должны использовать InternalsVisibleToAttribute.
Если вы хотите выполнить юнит-тест частного метода, возможно, что-то не так. Модульные тесты (вообще говоря) предназначены для тестирования интерфейса класса, то есть его открытых (и защищенных) методов. Конечно, вы можете "взломать" решение этой проблемы (даже если просто сделаете методы общедоступными), но вы также можете рассмотреть:
- Если метод, который вы хотите протестировать, действительно стоит протестировать, возможно, стоит перенести его в свой собственный класс.
- Добавьте дополнительные тесты к публичным методам, которые вызывают приватный метод, тестируя функциональность приватного метода. (Как отметили комментаторы, вы должны делать это только в том случае, если функциональность этих частных методов действительно является частью открытого интерфейса. Если они фактически выполняют функции, скрытые от пользователя (то есть модульное тестирование), это, вероятно, плохо).
Это может быть не полезно для тестирования частных методов. Однако мне также иногда нравится вызывать закрытые методы из тестовых методов. Большую часть времени, чтобы предотвратить дублирование кода для генерации тестовых данных...
Microsoft предоставляет два механизма для этого:
Accessors
- Перейти к исходному коду определения класса
- Щелкните правой кнопкой мыши на названии класса
- Выберите "Создать частный доступ"
- Выберите проект, в котором должен быть создан метод доступа => Вы получите новый класс с именем foo_accessor. Этот класс будет динамически генерироваться во время компиляции и будет доступен всем участникам.
Однако этот механизм иногда немного усложняется, когда дело доходит до изменений интерфейса исходного класса. Поэтому в большинстве случаев я избегаю этого.
Класс PrivateObject Другой способ - использовать Microsoft.VisualStudio.TestTools.UnitTesting.PrivateObject.
// Wrap an already existing instance
PrivateObject accessor = new PrivateObject( objectInstanceToBeWrapped );
// Retrieve a private field
MyReturnType accessiblePrivateField = (MyReturnType) accessor.GetField( "privateFieldName" );
// Call a private method
accessor.Invoke( "PrivateMethodName", new Object[] {/* ... */} );
Я не согласен с философией "вас должно интересовать только тестирование внешнего интерфейса". Это все равно, что сказать, что в автосервисе должны быть только тесты, чтобы проверить, вращаются ли колеса. Да, в конечном счете, я заинтересован во внешнем поведении, но мне нравятся мои собственные, частные, внутренние тесты, чтобы быть немного более конкретным и конкретным. Да, если я проведу рефакторинг, мне, возможно, придется изменить некоторые тесты, но если это не масштабный рефакторинг, мне придется изменить только некоторые из них, и тот факт, что другие (неизменные) внутренние тесты все еще работают, является отличным показателем того, что рефакторинг прошел успешно.
Вы можете попытаться охватить все внутренние случаи, используя только общедоступный интерфейс, и теоретически возможно полностью протестировать каждый внутренний метод (или, по крайней мере, каждый существенный), используя общедоступный интерфейс, но вам, возможно, придется стоять на голове, чтобы достичь это и связь между тестовыми примерами, выполняемыми через открытый интерфейс, и внутренней частью решения, которое они предназначены для тестирования, могут быть трудными или невозможными для распознавания. Указав, что отдельные тесты, которые гарантируют, что внутренний механизм работает должным образом, стоят тех незначительных изменений, которые произошли с рефакторингом - по крайней мере, это был мой опыт. Если вам нужно вносить огромные изменения в свои тесты для каждого рефакторинга, то, возможно, это не имеет смысла, но в этом случае, возможно, вам следует полностью переосмыслить свой дизайн. Хороший дизайн должен быть достаточно гибким, чтобы можно было вносить большинство изменений без значительных изменений.
В тех редких случаях, когда я хотел протестировать закрытые функции, я обычно модифицировал их для защиты, и я написал подкласс с функцией публичной оболочки.
Класс:
...
protected void APrivateFunction()
{
...
}
...
Подкласс для тестирования:
...
[Test]
public void TestAPrivateFunction()
{
APrivateFunction();
//or whatever testing code you want here
}
...
Я думаю, что следует задать более фундаментальный вопрос: почему вы пытаетесь сначала протестировать приватный метод? Это запах кода, когда вы пытаетесь протестировать закрытый метод через открытый интерфейс этого класса, в то время как этот метод является частным по причине, так как это деталь реализации. Нужно заботиться только о поведении открытого интерфейса, а не о том, как он реализован под прикрытием.
Если я хочу проверить поведение закрытого метода, используя обычные рефакторинги, я могу извлечь его код в другой класс (возможно, с видимостью на уровне пакета, поэтому убедитесь, что он не является частью общедоступного API). Затем я могу проверить его поведение в изоляции.
Продукт рефакторинга означает, что закрытый метод теперь является отдельным классом, который стал соавтором исходного класса. Его поведение станет понятным благодаря собственным модульным тестам.
Затем я могу посмеяться над его поведением при попытке проверить исходный класс, чтобы затем сосредоточиться на тестировании поведения открытого интерфейса этого класса, а не на проверке комбинаторного взрыва открытого интерфейса и поведения всех его закрытых методов.,
Я вижу это аналогично вождению автомобиля. Когда я веду машину, я не еду с поднятым капотом, поэтому я вижу, что двигатель работает. Я полагаюсь на интерфейс, который обеспечивает машина, а именно на счетчик оборотов и спидометр, чтобы знать, что двигатель работает. Я полагаюсь на то, что машина действительно движется, когда я нажимаю педаль газа. Если я хочу проверить двигатель, я могу проверить его изолированно.:D
Конечно, непосредственное тестирование частных методов может быть последним средством, если у вас есть устаревшее приложение, но я бы предпочел, чтобы унаследованный код был реорганизован для обеспечения лучшего тестирования. Майкл Фезерс написал отличную книгу на эту тему. http://www.amazon.co.uk/Working-Effectively-Legacy-Robert-Martin/dp/0131177052
Частные типы, внутренние и частные члены таковы по какой-то причине, и часто вы не хотите связываться с ними напрямую. И если вы это сделаете, есть вероятность, что вы сломаетесь позже, потому что нет никакой гарантии, что парни, создавшие эти сборки, сохранят частные / внутренние реализации как таковые.
Но иногда, когда я выполнял некоторые хаки / исследования скомпилированных или сторонних сборок, я сам заканчивал тем, что хотел инициализировать частный класс или класс с закрытым или внутренним конструктором. Или иногда, когда я имею дело с предварительно скомпилированными устаревшими библиотеками, которые я не могу изменить - я заканчиваю тем, что пишу несколько тестов для частного метода.
Так родился AccessPrivateWrapper - http://amazedsaint.blogspot.com/2010/05/accessprivatewrapper-c-40-dynamic.html - это быстрый класс-обертка, который облегчит работу с помощью динамических функций и отражения C# 4.0.
Вы можете создавать внутренние / частные типы, такие как
//Note that the wrapper is dynamic
dynamic wrapper = AccessPrivateWrapper.FromType
(typeof(SomeKnownClass).Assembly,"ClassWithPrivateConstructor");
//Access the private members
wrapper.PrivateMethodInPrivateClass();
Ну, вы можете тестировать приватный метод двумя способами
Вы можете создать экземпляр
PrivateObject
Класс синтаксис выглядит следующим образомPrivateObject obj= new PrivateObject(PrivateClass); //now with this obj you can call the private method of PrivateCalss. obj.PrivateMethod("Parameters");
Вы можете использовать отражение.
PrivateClass obj = new PrivateClass(); // Class containing private obj Type t = typeof(PrivateClass); var x = t.InvokeMember("PrivateFunc", BindingFlags.InvokeMethod | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance, null, obj, new object[] { 5 });
Я также использовал метод InternalsVisibleToAttribute. Стоит также упомянуть, что, если вы чувствуете себя некомфортно, когда вы используете внутренние методы для достижения этой цели, то, возможно, в любом случае они не должны быть предметом прямых модульных тестов.
В конце концов, вы тестируете поведение своего класса, а не его конкретную реализацию - вы можете изменить последний, не изменяя первый, и ваши тесты все равно должны пройти.
Есть 2 типа частных методов. Статические частные методы и нестатические частные методы (методы экземпляра). Следующие 2 статьи объясняют, как выполнить модульное тестирование частных методов с примерами.
MS Test имеет хорошую встроенную функцию, которая делает приватные члены и методы доступными в проекте, создав файл с именем VSCodeGenAccessors
[System.Diagnostics.DebuggerStepThrough()]
[System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.TestTools.UnitTestGeneration", "1.0.0.0")]
internal class BaseAccessor
{
protected Microsoft.VisualStudio.TestTools.UnitTesting.PrivateObject m_privateObject;
protected BaseAccessor(object target, Microsoft.VisualStudio.TestTools.UnitTesting.PrivateType type)
{
m_privateObject = new Microsoft.VisualStudio.TestTools.UnitTesting.PrivateObject(target, type);
}
protected BaseAccessor(Microsoft.VisualStudio.TestTools.UnitTesting.PrivateType type)
:
this(null, type)
{
}
internal virtual object Target
{
get
{
return m_privateObject.Target;
}
}
public override string ToString()
{
return this.Target.ToString();
}
public override bool Equals(object obj)
{
if (typeof(BaseAccessor).IsInstanceOfType(obj))
{
obj = ((BaseAccessor)(obj)).Target;
}
return this.Target.Equals(obj);
}
public override int GetHashCode()
{
return this.Target.GetHashCode();
}
}
С классами, производными от BaseAccessor
такие как
[System.Diagnostics.DebuggerStepThrough()]
[System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.TestTools.UnitTestGeneration", "1.0.0.0")]
internal class SomeClassAccessor : BaseAccessor
{
protected static Microsoft.VisualStudio.TestTools.UnitTesting.PrivateType m_privateType = new Microsoft.VisualStudio.TestTools.UnitTesting.PrivateType(typeof(global::Namespace.SomeClass));
internal SomeClassAccessor(global::Namespace.Someclass target)
: base(target, m_privateType)
{
}
internal static string STATIC_STRING
{
get
{
string ret = ((string)(m_privateType.GetStaticField("STATIC_STRING")));
return ret;
}
set
{
m_privateType.SetStaticField("STATIC_STRING", value);
}
}
internal int memberVar {
get
{
int ret = ((int)(m_privateObject.GetField("memberVar")));
return ret;
}
set
{
m_privateObject.SetField("memberVar", value);
}
}
internal int PrivateMethodName(int paramName)
{
object[] args = new object[] {
paramName};
int ret = (int)(m_privateObject.Invoke("PrivateMethodName", new System.Type[] {
typeof(int)}, args)));
return ret;
}
На CodeProject есть статья, в которой кратко обсуждаются плюсы и минусы тестирования частных методов. Затем он предоставляет некоторый код отражения для доступа к закрытым методам (аналогично коду, представленному Маркусом выше). Единственная проблема, которую я обнаружил в примере, заключается в том, что код не учитывает перегруженные методы.
Вы можете найти статью здесь:
Я склонен не использовать директивы компилятора, потому что они быстро загромождают вещи. Один из способов смягчить это, если они вам действительно нужны, - поместить их в частичный класс и заставить вашу сборку игнорировать этот файл.cs при создании рабочей версии.
Для тех, кто хочет запускать приватные методы без всякой путаницы. Это работает с любой структурой модульного тестирования, использующей только старый добрый Reflection.
public class ReflectionTools
{
// If the class is non-static
public static Object InvokePrivate(Object objectUnderTest, string method, params object[] args)
{
Type t = objectUnderTest.GetType();
return t.InvokeMember(method,
BindingFlags.InvokeMethod |
BindingFlags.NonPublic |
BindingFlags.Instance |
BindingFlags.Static,
null,
objectUnderTest,
args);
}
// if the class is static
public static Object InvokePrivate(Type typeOfObjectUnderTest, string method, params object[] args)
{
MemberInfo[] members = typeOfObjectUnderTest.GetMembers(BindingFlags.NonPublic | BindingFlags.Static);
foreach(var member in members)
{
if (member.Name == method)
{
return typeOfObjectUnderTest.InvokeMember(method, BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.InvokeMethod, null, typeOfObjectUnderTest, args);
}
}
return null;
}
}
Затем в ваших реальных тестах вы можете сделать что-то вроде этого:
Assert.AreEqual(
ReflectionTools.InvokePrivate(
typeof(StaticClassOfMethod),
"PrivateMethod"),
"Expected Result");
Assert.AreEqual(
ReflectionTools.InvokePrivate(
new ClassOfMethod(),
"PrivateMethod"),
"Expected Result");
Объявить их internal
, а затем используйте InternalsVisibleToAttribute
чтобы позволить вашей сборке тестового модуля увидеть их.
Вы не должны проверять закрытые методы вашего кода в первую очередь. Вы должны тестировать "открытый интерфейс" или API, общедоступные вещи ваших классов. API - это все открытые методы, которые вы предоставляете внешним абонентам.
Причина в том, что, как только вы начинаете тестировать закрытые методы и внутренние компоненты вашего класса, вы связываете реализацию вашего класса (личные вещи) с вашими тестами. Это означает, что когда вы решите изменить детали реализации, вам также придется изменить свои тесты.
По этой причине вам следует избегать использования InternalsVisibleToAtrribute.
Вот большой доклад Иана Купера, который освещает эту тему: Ian Cooper: TDD, где все пошло не так
Я хочу создать четкий пример кода, который вы можете использовать в любом классе, в котором вы хотите протестировать закрытый метод.
В вашем тестовом классе просто включите эти методы, а затем используйте их, как указано.
/**
*
* @var Class_name_of_class_you_want_to_test_private_methods_in
* note: the actual class and the private variable to store the
* class instance in, should at least be different case so that
* they do not get confused in the code. Here the class name is
* is upper case while the private instance variable is all lower
* case
*/
private $class_name_of_class_you_want_to_test_private_methods_in;
/**
* This uses reflection to be able to get private methods to test
* @param $methodName
* @return ReflectionMethod
*/
protected static function getMethod($methodName) {
$class = new ReflectionClass('Class_name_of_class_you_want_to_test_private_methods_in');
$method = $class->getMethod($methodName);
$method->setAccessible(true);
return $method;
}
/**
* Uses reflection class to call private methods and get return values.
* @param $methodName
* @param array $params
* @return mixed
*
* usage: $this->_callMethod('_someFunctionName', array(param1,param2,param3));
* {params are in
* order in which they appear in the function declaration}
*/
protected function _callMethod($methodName, $params=array()) {
$method = self::getMethod($methodName);
return $method->invokeArgs($this->class_name_of_class_you_want_to_test_private_methods_in, $params);
}
$ this -> _ callMethod ('_ someFunctionName', массив (param1,param2,param3));
Просто выдайте параметры в том порядке, в котором они отображаются в оригинальной закрытой функции.
Я удивлен, что никто еще не сказал этого, но решение, которое я использовал, состоит в том, чтобы создать статический метод внутри класса, чтобы проверить себя. Это дает вам доступ ко всему общему и частному для тестирования.
Кроме того, на языке сценариев (с возможностями OO, такими как Python, Ruby и PHP), вы можете сами выполнить проверку файла при запуске. Хороший быстрый способ убедиться, что ваши изменения ничего не сломали. Это, очевидно, делает масштабируемое решение для тестирования всех ваших классов: просто запустите их все. (Вы также можете сделать это на других языках с помощью void main, которая также всегда выполняет свои тесты).
Иногда бывает полезно протестировать частные объявления. По сути, у компилятора есть только один открытый метод: Compile(строка outputFileName, params string[] sourceSFileNames). Я уверен, что вы понимаете, что было бы трудно протестировать такой метод без проверки каждого "скрытого" объявления!
Вот почему мы создали Visual T#: чтобы облегчить тестирование. Это бесплатный язык программирования.NET (совместимый с C# v2.0).
Мы добавили оператор ".-". Это просто ведет себя как "." оператор, за исключением того, что вы также можете получить доступ к любой скрытой декларации из ваших тестов, не меняя ничего в вашем тестируемом проекте.
Взгляните на наш веб-сайт: загрузите его бесплатно.
CC -Dprivate=public
"CC" - это компилятор командной строки в системе, которую я использую. -Dfoo=bar
делает эквивалент #define foo bar
, Таким образом, эта опция компиляции эффективно меняет все личные вещи на публичные.
Вот хорошая статья о модульном тестировании частных методов. Но я не уверен, что лучше сделать приложение специально разработанным для тестирования (это все равно, что создавать тесты только для тестирования) или использовать рефлексию для тестирования. Уверен, большинство из нас выберет второй путь.
По моему мнению, вам следует только протестировать открытый API вашего classe.
Публикация метода для его модульного тестирования нарушает инкапсуляцию, раскрывая детали реализации.
Хороший публичный API решает непосредственную задачу клиентского кода и полностью решает эту задачу.
MbUnit получил хорошую обертку для этого под названием Reflector.
Reflector dogReflector = new Reflector(new Dog());
dogReflector.Invoke("DreamAbout", DogDream.Food);
Вы также можете установить и получить значения из свойств
dogReflector.GetProperty("Age");
Относительно "частного теста" я согласен, что... в идеальном мире. нет смысла делать частные юнит-тесты. Но в реальности вы можете захотеть написать приватные тесты вместо рефакторинга кода.
Я использую класс PrivateObject. Но, как упоминалось ранее, лучше избегать тестирования частных методов.
Class target = new Class();
PrivateObject obj = new PrivateObject(target);
var retVal = obj.Invoke("PrivateMethod");
Assert.AreEqual(retVal);
Вот пример, сначала подпись метода:
private string[] SplitInternal()
{
return Regex.Matches(Format, @"([^/\[\]]|\[[^]]*\])+")
.Cast<Match>()
.Select(m => m.Value)
.Where(s => !string.IsNullOrEmpty(s))
.ToArray();
}
Вот тест:
/// <summary>
///A test for SplitInternal
///</summary>
[TestMethod()]
[DeploymentItem("Git XmlLib vs2008.dll")]
public void SplitInternalTest()
{
string path = "pair[path/to/@Key={0}]/Items/Item[Name={1}]/Date";
object[] values = new object[] { 2, "Martin" };
XPathString xp = new XPathString(path, values);
PrivateObject param0 = new PrivateObject(xp);
XPathString_Accessor target = new XPathString_Accessor(param0);
string[] expected = new string[] {
"pair[path/to/@Key={0}]",
"Items",
"Item[Name={1}]",
"Date"
};
string[] actual;
actual = target.SplitInternal();
CollectionAssert.AreEqual(expected, actual);
}
Вы также можете объявить его как открытый или внутренний (с InternalsVisibleToAttribute) при сборке в режиме отладки:
/// <summary>
/// This Method is private.
/// </summary>
#if DEBUG
public
#else
private
#endif
static string MyPrivateMethod()
{
return "false";
}
Это раздувает код, но это будет private
в сборке релиза.
Это аккуратный способ управления вещами:
http://elegantcode.com/2010/01/28/calling-non-public-methods/
И другой:
http://elegantcode.com/2010/04/16/linfu-duck-typing-part-1-revealing-secrets/
1) Если у вас есть унаследованный код, то единственный способ проверить приватные методы - это рефлексия.
2) Если это новый код, то у вас есть следующие варианты:
- Используйте отражение (для сложных)
- Напишите модульный тест в том же классе (делает рабочий код уродливым, если в нем есть тестовый код)
- Рефакторинг и сделать метод общедоступным в некотором классе утилит
- Используйте аннотацию @VisibleForTesting и удалите приватную
Я предпочитаю метод аннотации, самый простой и наименее сложный. Единственная проблема заключается в том, что мы увеличили видимость, что, я думаю, не является большой проблемой. Мы всегда должны кодировать интерфейс, поэтому, если у нас есть интерфейс MyService и реализация MyServiceImpl, то у нас могут быть соответствующие тестовые классы MyServiceTest (методы интерфейса тестирования) и MyServiceImplTest (тестировать частные методы). В любом случае все клиенты должны использовать интерфейс таким образом, что, хотя видимость приватного метода была увеличена, это не должно иметь большого значения.
Способ сделать это, чтобы ваш метод protected
и напишите тестовое устройство, которое наследует ваш класс для тестирования. Таким образом, вы не поворачиваете свой метод public
, но вы включаете тестирование.
В С# вы можете использовать код, который я привожу ниже. Хотя я думаю, что модульное тестирование частных методов следует проводить только в случае крайней необходимости. Я столкнулся с несколькими случаями, когда я чувствовал, что это было оправдано. Вот несколько методов C#, которые я создал вUnitTestBase
класс, от которого я наследую свои классы UnitTest (вы также можете поместить его в статический «вспомогательный» класс). ХТН
protected internal static TResult? InvokePrivateInstanceMethod<TType, TResult>(string methodName, object?[]? methodArguments = null, params object?[]? constructorArguments)
{
var classType = typeof(TType);
var instance = Activator.CreateInstance(classType, constructorArguments);
var privateMethodInfo = classType.GetMethods(BindingFlags.NonPublic | BindingFlags.Instance)
.FirstOrDefault(m => m.IsPrivate &&
m.Name.Equals(methodName, StringComparison.Ordinal) &&
m.ReturnType.Equals(typeof(TResult)));
if (privateMethodInfo is null)
{
throw new MissingMethodException(classType.FullName, methodName);
}
var methodResult = privateMethodInfo.Invoke(instance, methodArguments);
if (methodResult is not null)
{
return (TResult)methodResult;
}
return default;
}
protected internal static async Task<TResult?> InvokePrivateInstanceMethodAsync<TType, TResult>(string methodName, object?[]? methodArguments = null, params object?[]? constructorArguments)
{
var classType = typeof(TType);
var instance = Activator.CreateInstance(classType, constructorArguments);
var privateMethodInfo = classType.GetMethods(BindingFlags.NonPublic | BindingFlags.Instance)
.FirstOrDefault(m => m.IsPrivate &&
m.Name.Equals(methodName, StringComparison.Ordinal) &&
m.ReturnType.Equals(typeof(Task<TResult>)));
if (privateMethodInfo is null)
{
throw new MissingMethodException(classType.FullName, methodName);
}
var methodResult = privateMethodInfo.Invoke(instance, methodArguments);
if (methodResult is not null)
{
return await (Task<TResult>)methodResult;
}
return default;
}