Модульные тесты C # проверяют неабстрактный метод в классе, созданном Activator - PullRequest
0 голосов
/ 18 сентября 2018

Сначала позвольте представить вам мой проект.

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

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

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

Вот пример кода:

public Program CreateProgram(Type type)
    {
        Program program = Activator.CreateInstance(type) as Program;
        program.Directory = ProgramsPath;

        int nbChannels = 2; //not 2 but a long line searching the correct number where it is.

        for (int i = 1; i <= nbChannels; i++)
        {
            program.AddNewChannel(i);
        }

        program.Save();
        return program;
    }

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

В качестве примера, вот одна из моих тестовых функций (для метода Save) с ее init. Я храню типы, необходимые для тестирования, в файле XML.

    [TestInitialize]
    public void TestInitialize()
    {
        if (!TestContext.TestName.StartsWith("GetKnownTypes"))
            type = UnitTestsInitialization.applicationHMIAssembly.GetType((string)TestContext.DataRow["Data"]);
    }


    [TestMethod]
    [DataSource("Microsoft.VisualStudio.TestTools.DataSource.XML",
               "|DataDirectory|\\" + DATA_FILE, "Row",
                DataAccessMethod.Sequential)]
    public void SavedProgramCreatesFile()
    {
        Program program = Activator.CreateInstance(type) as Program;
        program.Name = "SavedProgramCreatesFile";
        program.Directory = DIRECTORY;

        program.Save();

        string savedProgramFileName = program.GetFilePath();

        bool result = File.Exists(savedProgramFileName);

        Assert.IsTrue(result);
    }

Все мои конкретные классы Программы были проверены отдельно.

Таким образом, я хотел бы проверить, вызваны ли следующие методы program.AddNewChannel и program.Save.

Я посмотрел на Moq, но первая проблема в том, что метод Save не является абстрактным.

Кроме того, использование Активатора не позволяет мне сделать Mock<Program>.

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

    [TestMethod]
    [DataSource("Microsoft.VisualStudio.TestTools.DataSource.XML",
               "|DataDirectory|\\" + DATA_FILE, "Row",
                DataAccessMethod.Sequential)]
    public void CreateProgram_CallsProgramSaveMethod()
    {
        Mock<Program> mock = new Mock<Program>();
        mock.Setup(p => p.AddNewChannel(It.IsAny<int>()));

        Program program = pm.CreateProgram(mock.Object.GetType());
        mock.Verify(p => p.Save());
        mock.Verify(p => p.GetFilePath(It.IsAny<string>()));
        mock.Verify(p => p.AddNewChannel(It.IsAny<int>()), Times.Exactly(ProgramManager.NB_MACHINE_CHANNELS));
        Assert.IsNotNull(program);
        program.DeleteFile();
    }

Что было вдохновлено этим вопросом: Как издеваться над абстрактным базовым классом

И работает, пока не достигнет строки program.AddNewChannel(i); в цикле for. Ошибка следующая:

System.NotImplementedException: «Это ошибка DynamicProxy2: перехватчик попытался« продолжить »для метода« Void AddNewChannel (Int32) », который является абстрактным. При вызове абстрактного метода нет реализации, к которой следует «переходить», и перехватчик обязан имитировать реализацию (установить возвращаемое значение, аргументы и т. Д.) '

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

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

Может кто-нибудь предложить мне какой-либо способ проверки этих вызовов методов? (Даже если мне нужно изменить мой метод CreateProgram)

Я посмотрел здесь: Как смоделировать не виртуальные методы? , но я не уверен, что это применимо к моей проблеме.

Я использую MSTests для своих юнит-тестов.

ВНИМАНИЕ

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

Заранее спасибо.

Ответы [ 2 ]

0 голосов
/ 18 сентября 2018

Создайте интерфейс-оболочку и класс вокруг Activator, затем передайте тип этому:

public interface IActivatorWrapper
{
    object CreateInstance(Type type);
}

public class ActivatorWrapper : IActivatorWrapper
{
    public object CreateInstance(Type type)
    {
        return Activator.CreateInstance(type);
    }
}

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


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

0 голосов
/ 18 сентября 2018

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


Таким образом, я бы хотел проверить, не вызваны ли следующие методы program.AddNewChannel и program.Save.

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

То, что вы описываете, является (очень элементарным) интеграционным тестом, а не модульный тест.


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

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

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

Предположим следующее:

public abstract class Program 
{
    public bool BaseMethod() {}
}

public class Foo : Program 
{
    public bool CustomFooMethod() {}
}

public class Bar : Program 
{
    public bool CustomBarMethod() {}
}

Создание абстрактного класса тестированияМетод:

[TestFixture]
[Ignore]
public class ProgramTests
{
     public virtual Program GetConcrete()
     {
         throw new NotImplementedException();
     }

    [Test]
    public void BaseMethodTestReturnsFalse()
    {
        var result = GetConcrete().BaseMethod();

        Assert.IsFalse(result);
    }
}

[Ignore] гарантирует, что класс ProgramTests не будет проверен сам по себе.

Затем вы наследуете от этого класса, где будут проверяться конкретные классы: *Аналогичным образом реализовано 1039 *

[TestFixture]
public class FooTests
{
    private readonly Foo Foo;

    public FooTests()  
    {
        this.Foo = new Foo();
    }

    public overrides Program GetConcrete()
    {
        return this.Foo;
    }

    [Test]
    public void CustomFooMethodTestReturnsFalse()
    {
        var result = this.Foo.CustomFooMethod();

        Assert.IsFalse(result);
    }
}

BarTests.

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

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


Я также заметил это:

Mock<Program> mock = new Mock<Program>();
mock.Setup(p => p.AddNewChannel(It.IsAny<int>()));
Program program = pm.CreateProgram(mock.Object.GetType());

Я не понимаюцель этого кода.Чем он отличается от простого:

Program program = pm.CreateProgram(typeof(Program).GetType());

Насколько я понимаю, и макет, и его настройки не имеют значения, поскольку вы смотрите только на тип , а затемCreateProgram() создайте новый объект для вас в любом случае.

Во-вторых, и это относится к моему примеру тестирования конкретных классов, вы не должны тестировать непосредственно с Program, вы должны тестировать свойклассы производных программ (Foo и Bar).

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

...