Как вы единичный тест частных методов?


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

Как правильно это сделать?

30 454

30 ответов:

Если вы используете .net, вы должны использовать InternalsVisibleToAttribute.

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

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

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

Microsoft предоставляет два механизма для этого:

аксессоры

  • перейти к исходному коду определения класса
  • щелкните правой кнопкой мыши на имени класса
  • Выберите " Создать Личное Accessor"
  • выберите проект, в котором должен быть создан метод доступа => Вы будете в конечном итоге с нового класса с 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();

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

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

Ну вы можете модульный тест частный метод в двух направлениях

  1. вы можете создать экземпляр PrivateObject класс синтаксис выглядит следующим образом

    PrivateObject obj= new PrivateObject(PrivateClass);
    //now with this obj you can call the private method of PrivateCalss.
    obj.PrivateMethod("Parameters");
    
  2. вы можете использовать отражение.

    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 });
    

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;
    }

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

  1. Модульное Тестирование Статических Частных Методов
  2. Модульное Тестирование Нестатических Частных Методов

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

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

вы можете найти статью здесь:

http://www.codeproject.com/KB/cs/testnonpublicmembers.aspx

объявить их internal, а затем использовать InternalsVisibleToAttribute чтобы ваша модульная тестовая сборка могла их видеть.

иногда, это может быть хорошо, чтобы проверить частные объявления. По сути, компилятор имеет только один открытый метод: Compile (string outputFileName, params string[] sourceSFileNames ). Я уверен, вы понимаете, что было бы трудно протестировать такой метод без проверки каждого "скрытого" объявления!

вот почему мы создали Visual T#: чтобы упростить тесты. Это бесплатно .Net языков программирования (C# версии 2.0 совместимый).

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

взгляните на наш веб-сайт: скачать это бесплатно.

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

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

вы должны по этой причине избегать использования InternalsVisibleToAtrribute.

вот большой разговор Яна Купера, который охватывает эту тему:Ян Купер: TDD, где все пошло не так

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

Reflector dogReflector = new Reflector(new Dog());
dogReflector.Invoke("DreamAbout", DogDream.Food);

вы также можете установить и получить значения из свойства

dogReflector.GetProperty("Age");

Что касается" test private", я согласен с этим.. в идеальном мире. нет смысла проводить частные модульные тесты. Но в реальном мире вы можете захотеть написать частные тесты вместо рефакторинга кода.

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

кроме того, на языке сценариев (с возможностями OO, такими как Python, Ruby и PHP) вы можете сделать сам тест файла при запуске. Хороший быстрый способ убедиться, что ваши изменения ничего не сломали. Это, очевидно, делает масштабируемое решение для тестирования всех классов: просто запустите их все. (вы также можете сделать это на других языках с помощью void main, который всегда выполняет свои тесты).

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

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

  /**
   *
   * @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', array(param1, param2, param3));

просто выдайте параметры в том порядке, в котором они появляются в исходной частной функции

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

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");
CC -Dprivate=public

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

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

Class target = new Class();
PrivateObject obj = new PrivateObject(target);
var retVal = obj.Invoke("PrivateMethod");
Assert.AreEqual(retVal);

можно создать тестовый метод для частного метода из Visual studio 2008. При создании модульного теста для частного метода в тестовый проект добавляется папка "ссылки на тесты" и в эту папку добавляется метод доступа. Метод доступа также упоминается в логике метода модульного тестирования. Этот метод доступа позволяет модульному тесту вызывать частные методы в тестируемом коде. Для деталей имейте взгляд в

http://msdn.microsoft.com/en-us/library/bb385974.aspx

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

вот пример, сначала подпись метода:

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);
}

способ сделать это-иметь свой метод protected и написать тестовое приспособление, которое наследует ваш класс для тестирования. Таким образом, вы ни поворачиваете свой метод public, но вы включаете тестирование.

1) Если у вас есть устаревший код, то единственный способ тестировать приватные методы-это отражение.

2) Если это новый код, то у вас есть следующие варианты:

  • использовать отражение (сложных)
  • написать модульный тест в том же классе (делает производственный код уродливым имея тестовый код также в нем)
  • рефакторинг и сделать метод общедоступным в каком-то классе util
  • используйте аннотацию @VisibleForTesting и удалите рядовой

Я предпочитаю метод аннотации, самый простой и наименее сложный. Единственная проблема заключается в том, что мы увеличили видимость, которая, я думаю, не является большой проблемой. Мы всегда должны кодировать интерфейс, поэтому если у нас есть интерфейс MyService и реализация MyServiceImpl, то мы можем иметь соответствующие тестовые классы, которые являются MyServiceTest (методы тестового интерфейса) и MyServiceImplTest (частные методы тестирования). Все клиенты должны в любом случае использовать интерфейс так в некотором смысле, даже если видимость частного метода была увеличена, это не должно иметь большого значения.

вы также можете объявить его как public или internal (с InternalsVisibleToAttribute) при построении в режиме отладки:

    /// <summary>
    /// This Method is private.
    /// </summary>
#if DEBUG
    public
#else
    private
#endif
    static string MyPrivateMethod()
    {
        return "false";
    }

это раздувает код, но это будет private в сборке выпуска.