Разве определение TestMethod в базовых классах не поддерживается MsTest? - PullRequest
26 голосов
/ 19 августа 2010

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

Допустим, я хочу проверить, что все типы, которые переопределяют Equals(), делают это правильно.Поскольку Equals() определено как виртуальное в System.Object, широкий диапазон типов может изменить это поведение.Каждый тип, который делает это, должен иметь тесты, чтобы убедиться, что новое поведение соответствует неявным ожиданиям вызывающего этого метода.В частности, для Equals(), если вы переопределите этот метод, новая реализация должна убедиться, что два равных объекта также имеют одинаковые хеш-коды, как определено в System.Object.GetHashCode().

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

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

/// <summary>
/// Test fixture base class for testing types that overrides Object.Equals()
/// </summary>
/// <typeparam name="T">The production type under test</typeparam>
public abstract class EqualsFixtureBase<T>
{
    #region Equals tests

    protected static void CompareInstances(T inst1, T inst2, bool expectedEquals)
    {
        Assert.AreEqual(expectedEquals, inst1.Equals((T)inst2));
        Assert.AreEqual(expectedEquals, inst1.Equals((object)inst2));
        if (expectedEquals)
        {
            // equal instances MUST have identical hash codes
            // this is a part of the .NET Equals contract
            Assert.AreEqual(inst1.GetHashCode(), inst2.GetHashCode());
        }
        else
        {
            if (inst2 != null)
            {
                Assert.AreNotEqual(inst1.GetHashCode(), inst2.GetHashCode());
            }
        }
    }

    /// <summary>
    /// Creates version 1 instance of the type under test, not 'Equal' to instance 2.
    /// </summary>
    /// <returns>An instance created with properties 1.</returns>
    protected abstract T CreateInstance1();

    /// <summary>
    /// Creates version 2 instance of the type under test, not 'Equal' to instance 1.
    /// </summary>
    /// <returns>An instance created with properties 2.</returns>
    protected abstract T CreateInstance2();

    /// <summary>
    /// Creates an instance equal to the version 1 instance, but not the identical
    /// same object.
    /// </summary>
    /// <returns>An instance created with properties equal to instance 1.</returns>
    protected abstract T CreateInstanceThatEqualsInstance1();

    [TestMethod]
    public void Equals_NullOrDefaultValueTypeInstance()
    {
        T instance = CreateInstance1();
        CompareInstances(instance, default(T), false);
    }

    [TestMethod]
    public void Equals_InstanceOfAnotherType()
    {
        T instance = CreateInstance1();
        Assert.IsFalse(instance.Equals(new object()));
    }

    [TestMethod]
    public void Equals_SameInstance()
    {
        T slot1 = CreateInstance1();
        CompareInstances(slot1, slot1, true);
    }

    [TestMethod]
    public void Equals_EqualInstances()
    {
        T slot1 = CreateInstance1();
        T slot2 = CreateInstanceThatEqualsInstance1();
        CompareInstances(slot1, slot2, true);
        CompareInstances(slot2, slot1, true);
    }

    [TestMethod]
    public void Equals_NonEqualInstances()
    {
        T slot1 = CreateInstance1();
        T slot2 = CreateInstance2();
        CompareInstances(slot1, slot2, false);
        CompareInstances(slot2, slot1, false);
    }

    #endregion Equals tests
}

Затем я могу повторно использовать эти TestMethods для каждого типа, переопределяющего Equals ().Например, это будет определение класса теста для проверки того, что тип System.String правильно реализует Equals().

[TestClass]
public class ExampleOfAnEqualsTestFixture : EqualsFixtureBase<string>
{
    [TestMethod]
    public void Foo()
    {
        Assert.IsTrue(true);
    }

    protected override string CreateInstance1()
    {
        return "FirstString";
    }

    protected override string CreateInstance2()
    {
        return "SecondString";
    }

    protected override string CreateInstanceThatEqualsInstance1()
    {
        return "FirstString";
    }
}

Это также может быть расширено.Например, для типов, которые перегружают операторы == и! =, Может быть определен второй абстрактный базовый класс тестирования (т. Е. EqualsOperatorsFixtureBase<T> : EqualsFixtureBase<T>), который проверяет, что реализация этих операторов не только правильна, но и соответствует расширенным определениям.Equals() и GetHashCode().

Я могу сделать это с помощью NUnit, но при использовании MsTest у меня возникают проблемы.

a) Visual Studio 2010 обнаруживает только тестовый метод Foo(),не унаследованные методы тестирования, поэтому он не может их запустить.Кажется, что загрузчик тестов Visual Studio не проходит иерархию наследования тестового класса.

b) Когда я проверяю эти типы в TFS, TFS находит абстрактный тип EqualsFixtureBase и думает, что это тестовый класс, который должен бытьзапустить.Но поскольку он не может быть создан, он не может его запустить и помечает тесты этого типа как неокончательные, что не позволяет запустить тест, и, следовательно, сборка (!).

Есть ли способ получитьвокруг этого, или это ограничение MsTest и Visual Studio?

Если это так, исправляет ли это в дорожной карте для VS / TFS ??

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

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

Спасибо

РЕДАКТИРОВАТЬ: Я нашел эту ссылку на один из блогов MSDN, там написано следующее

«В Whidbey отсутствовала поддержка наследования тестовых классов. В Nunit она полностью поддерживается. Это будет исправлено в Orcas.»

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

Ответы [ 4 ]

22 голосов
/ 09 сентября 2010

Использование VS 2010 Я не вижу того же поведения, что и вы.Когда я скопировал ваши 2 класса в тестовый проект и скомпилировал его, я получил вывод:

UTA004: Illegal use of attribute...The TestMethodAttribute can be 
defined only inside a class marked with the TestClass attribute

Итак, я отметил EqualsFixutureBase :

[TestClass]
public abstract class EqualsFixtureBase<T>
{
...
}

Теперь он компилируетсябез предупреждения, и когда я выбираю запускать тесты для ExampleOfAnEqualsTestFixture , он запускает Foo и все 5 унаследованных равных тестов.Также, когда я копирую ExampleOfAnEqualsTestFixture и использую его для int и запускаю тесты для решения, я вижу все 5 унаследованных тестов, выполняющихся (и проходящих) для примера строкового класса и примера intучебный класс.

Вы делаете что-то в дополнение к своему примеру, что может быть причиной вашей проблемы?

10 голосов
/ 12 сентября 2013

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

[TestClass][Ignore]
public abstract class EqualsFixtureBase<T>
{
....
7 голосов
/ 10 сентября 2010

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

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

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

0 голосов
/ 10 декабря 2010

Работает ли это, если вы поместите сборку базового класса в ту же папку, что и производная?Может быть, поэтому их объединение в одну сборку работает;другая сборка не разрешима в том месте, где они этого хотят.Я не уверен, как еще установить правильные пути проб, которые вам могут понадобиться, в то время, когда они необходимы..Testsettings может выражать такие вещи, как база данных приложений и зонды для домена приложения бегуна, возможно, те, которые настроены правильно, помогут ему привязаться к сборке базового класса, если она отличается от сборки модульного теста, производной от корня.

...