Могу ли я реализовать серию повторно используемых тестов для проверки реализации интерфейса?

Я пишу серию классов коллекций на C#, каждый из которых реализует похожие пользовательские интерфейсы. Можно ли написать единый набор модульных тестов для интерфейса и автоматически запускать их все в нескольких разных реализациях? Я хотел бы избежать дублирования кода тестирования для каждой реализации.

Я готов изучить любую структуру (NUnit и т. д.) или расширение Visual Studio для достижения этой цели.


Для тех, кто хочет сделать то же самое, я разместил свое конкретное решение, основанное на принятом решении vandeursen, как ответ.


person dlras2    schedule 20.02.2012    source источник
comment
Добавлен тег lsp, поскольку вопрос и ответ применимы к любой иерархии классов, придерживающейся LSP.   -  person avandeursen    schedule 21.02.2012


Ответы (4)


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

Предположим, у вас есть интерфейс Itf с реализацией классов C1 и C2.

Сначала вы создаете тестовый класс для Itf (ItfTest). Чтобы на самом деле выполнить тест, вам нужно создать фиктивную реализацию вашего интерфейса Itf.

Все тесты в этом ItfTest должны проходить на любой реализации Itf (!). Если нет, ваша реализация не соответствует принципу подстановки Лисков ("L" в SOLID принципы объектно-ориентированного проектирования)

Таким образом, чтобы создать тестовый пример для C1, ваш класс C1Test может расширять ItfTest. Ваше расширение должно заменить создание фиктивного объекта созданием объекта C1 (внедряя его или используя GoF заводской метод). Таким образом, все случаи ItfTest применяются к экземплярам типа C1. Кроме того, ваш класс C1Test может содержать дополнительные тестовые примеры, специфичные для C1.

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

Ссылки: биндер шаблон Polymorphic Server Test и PACT — параллельная архитектура для тестирования компонентов.

person avandeursen    schedule 20.02.2012
comment
Чтобы избежать фиктивных реализаций, я просто объявил свой класс ItfTest абстрактным и объявил заглушку функции protected abstract Itf CreateInstance();. (Обратите внимание, что и ItfTest, и C1Test должны иметь атрибут [TestClass].) - person dlras2; 21.02.2012
comment
Да, я также использовал для этого фабричный метод, так как он проще. Я использовал этот тестовый подход в JUnit, где нет атрибута [TestClass], но где аннотации @Test в суперклассах наследуются, что приводит к повторному запуску тестовых случаев суперкласса в подклассах. И: спасибо за редактирование. - person avandeursen; 21.02.2012

Расширяя ответ Джо, вы можете использовать атрибут [TestCaseSource] в NUnit аналогично RowTest MBUnit. Вы можете создать источник тестового примера с именами ваших классов. Затем вы можете украсить каждый тест, который имеет атрибут TestCaseSource. Затем, используя Activator.CreateInstance, вы можете выполнить переход к интерфейсу, и все будет готово.

Что-то вроде этого (примечание — голова скомпилирована)

string[] MyClassNameList = { "Class1", "Class2" };

[TestCaseSource(MyClassNameList)]
public void Test1(string className)
{
    var instance = Activator.CreateInstance(Type.FromName(className)) as IMyInterface;

    ...
}
person Antony Scott    schedule 20.02.2012

Для этого можно использовать атрибуты [RowTest] в MBUnit. В приведенном ниже примере показано, как вы передаете методу строку, указывающую, какой класс реализации интерфейса вы хотите создать, а затем создаете этот класс посредством отражения:

[RowTest]
[Row("Class1")]
[Row("Class2")]
[Row("Class3")]
public void TestMethod(string type)
{
   IMyInterface foo = Activator.CreateInstance(Type.GetType(type)) as IMyInterface;

   //Do tests on foo:
}

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

person Joe Alfano    schedule 20.02.2012
comment
Можете ли вы отредактировать свой ответ, чтобы точно объяснить, как я буду тестировать реализации таким образом? Кажется, вы говорите, что каждому модульному тесту нужен оператор switch для всех реализаций, а это не то, чего я хочу. - person dlras2; 21.02.2012
comment
Вам не нужно использовать оператор switch, если вы этого не хотите. Я упомянул, что вы также можете использовать отражение. См. Ответ Энтони для примера этого. Я также обновил свой пост, включив в него пример использования отражения для создания экземпляров различных конкретных реализаций интерфейса. - person Joe Alfano; 21.02.2012
comment
Я бы, вероятно, написал это, чтобы взять экземпляры IMyInterface вместо только имени, но все же +1. - person dlras2; 21.02.2012
comment
Спасибо Дэн за идею. Мне никогда не удавалось заставить атрибуты тестов строк MbUnit работать с чем-либо, кроме констант. Например, что-то вроде этого не компилируется: [Row(new Class1())]. Я думаю, что это ограничение аргументов атрибутов .net. Я не знаю, можно ли сделать что-то подобное с другими средами тестирования. - person Joe Alfano; 21.02.2012
comment
Хорошая мысль об ограничении констант. Активатор, безусловно, хорошая альтернатива. - person dlras2; 21.02.2012
comment
@JoeAlfano: параметры атрибутов .NET действительно могут быть только константами (подробнее ). - person k.m; 21.02.2012

Это моя конкретная реализация, основанная на ответе avandeursen:

[TestClass]
public abstract class IMyInterfaceTests
{
    protected abstract IMyInterface CreateInstance();

    [TestMethod]
    public void SomeTest()
    {
        IMyInterface instance = CreateInstance();
        // Run the test
    }
}

Затем каждая реализация интерфейса определяет следующий тестовый класс:

[TestClass]
public class MyImplementationTests : IMyInterfaceTests
{
    protected override IMyInterface CreateInstance()
    {
        return new MyImplementation();
    }
}

SomeTest запускается один раз для каждого конкретного TestClass, полученного из IMyInterfaceTests. Используя абстрактный базовый класс, я избегаю необходимости в фиктивных реализациях. Обязательно добавьте TestClassAttribute к обоим классам, иначе это не сработает. Наконец, при желании вы можете добавить в дочерний класс любые тесты, специфичные для реализации (например, конструкторы).

person dlras2    schedule 21.02.2012
comment
вы используете конкретную тестовую среду? Я использую включенные модульные тесты в VS2012, и унаследованные тесты из другого общего тестового проекта (пространства имен) не подхватываются. - person drzaus; 30.04.2013
comment
странно - унаследованные тесты в одном проекте, разные пространства имен отображаются нормально. только когда они находятся в другом проекте, они не регистрируются. что мне не хватает? - person drzaus; 30.04.2013