Как мне выполнить модульное тестирование такого метода с помощью IOC? - PullRequest
0 голосов
/ 21 июля 2011

Я пытаюсь написать модульные тесты для функции, которая выглядит примерно так:

public List<int> Process(int input)
{
    List<int> outputList = new List<int>();

    List<int> list = this.dependency1.GetSomeList(input);
    foreach(int element in list)
    {
        // ... Do some procssing to element

        //Do some more processing
        int processedInt = this.dependency2.DoSomeProcessing(element);

        // ... Do some processing to processedInt

        outputList.Add(processedInt);
    }
    return outputList;
}

Я планировал имитацию зависимостей1 и зависимостей2 в тестовых примерах, но я не уверен, как мне следуетустановить их.Для того чтобы настроить dependency2.DoSomeProcessing, мне нужно знать, каким будет значение «element» при каждом вызове.Чтобы понять это, мне нужно было бы:

  1. Скопировать и вставить часть логики из моего метода Process () в тестовый пример

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

Ни одно из этих решений не кажется мне очень хорошим.Я новичок в МОК и издеваюсь, так что я надеюсь, что есть что-то, чего я совершенно не понимаю.Если нет, то какое из этих решений кажется наименее плохим?

Спасибо

Редактировать: я использую Moq, чтобы высмеивать зависимости.

Ответы [ 4 ]

1 голос
/ 21 июля 2011

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

public IEnumerable<int> Process(int input)
{
    foreach(var element in dependency1.GetSomeList(input))
    {
        yield return dependency2.DoSomeProcessing(element);
    }
}

Итак, теперь перейдем к рассматриваемому вопросу.У вас есть 2 зависимости в методе, которые необходимо смоделировать, чтобы проверить это правильно.Есть несколько хороших способов сделать это, но самый простой - использовать Constructor Dependency Injection.Здесь вы вводите зависимость в ваш класс через конструктор.Вам понадобятся ваши зависимости, чтобы иметь общий базовый тип (интерфейс или абстрактный класс) для выполнения инъекции.

public class Processor
{
    public Processor(IDep1 dependency1, IDep2 dependency2)
    {
        _dependency1 = dependency1;
        _dependency2 = dependency2;
    }

    IDep1 _dependency1;
    IDep2 _dependency2;

    public IEnumerable<int> Process(int input)
    {
        foreach(var element in dependency1.GetSomeList(input))
        {
            yield return dependency2.DoSomeProcessing(element);
        }
    }
}

Теперь создайте макет для внедрения в него для ваших тестов.

public interface IDep1
{
    IEnumerable<int> GetSomeList(int);
}

public interface IDep2
{
    int DoSomeProcessing(int);
}

public class MockDepdency1 : IDep1
{
    public IEnumerable<int> GetSomeList(int val)
    {
        return new int[]{ 1, 2, 3 }.AsEnumerable();
    }
}

public class MockDepdency2 : IDep2
{
    public int DoSomeProcessing(int val)
    {
        return val + 1;
    }
}

...
main()
{
    IDep1 dep1 = new MockDependency1();
    IDep2 dep2 = new MockDependency2();

    var proc = new Processor(dep1, dep2);
    var actual = proc.Process(5);

    Assert.IsTrue(actual.Count() == 3);
}

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

Еще лучшеОдним из способов является использование такой утилиты, как Unity, и не вводить ее через конструктор.Вместо этого вы можете просто сопоставить ваш интерфейс с MockDependency1 и MockDependency2 внутри вашего теста, и при запуске Process (...) он получит вместо этого версии Mock.Если вы хотите знать, как это сделать, дайте мне знать, и я добавлю это ниже.

1 голос
/ 21 июля 2011

Что именно вы тестируете?

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

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

Так что ваш издевательство должно быть примерно таким (рука собрана с Moq)

var dependency1 = new Mock<IDependency1>()
  .Setup(d => d.GetSomeList(It.IsAny<int>())
  .Returns(new List<int>(new [] { 1, 2, 3 });  
var dependency2 = new Mock<IDependency2>()
  .Setup(d => d.DoSomeProcessing(It.IsAny<int>())
  .Returns(x => x * 2); // You can get the input value and use it to determine an output value

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

Затем вы создаете другой тест, который проверяет, работает ли GetSomeList().И еще один, который гарантирует, что DoSomeProcessing() работает.Это DoSomeProcessing() тест, который потребует вычисленных вручную значений.

1 голос
/ 21 июля 2011

Макет обеих зависимостей

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

Вам нужно протестировать только 3 вещи внутри процесса.

  1. Правильный вывод процесса в соответствии с проверенными входами
  2. Behavior => (была зависимость.GetSomeList вызывается как положено и т. Д.)
  3. Исправить вывод Process по всем логическим путям

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

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

Преимущество разделения ваших тестов на большое количество мелкозернистых тестовых случаев состоит в том, что если вы что-то измените внутри Process и запустите свои модульные тесты. Вы точно знаете, в чем проблема, потому что 3/50 тестов теперь не проходят, и вы знаете, где исправить ваши проблемы, потому что все они тестируют 1 конкретную вещь.

Использование Rhino Mocks будет примерно таким

private IDependency1 _dependency1;
private IDependency2 _dependency2;
private ClassToBeTested _classToBeTested;

[SetUp]
private override void SetUp()
{
  base.SetUp();
  _dependency1 = MockRepository.GenerateMock<IDependency1>();
  _dependency2 = MockRepository.GenerateMock<IDependency2>();

  _classToBeTested = new ClassToBeTested(_dependency1, _dependency2);

}

[Test]
public void TestCorrectFunctionOfProcess()
{

  int input = 10000;
  IList<int> returnList = new List<int>() {1,2,3,4};

  // Arrange   
  _dependency1.Expect(d1 => d1.GetSomeList(input)).Return(returnList);
  _dependency2.Expect(d2 => d2.DoSomeProcessing(0))
      .AtLeastOnce().IgnoreArguments().Return(1);

  // Act
  var outputList = _classToBeTested.Process(input);

  // Assert that output is correct for all mocked inputs
  Assert.IsNotNull(outputList, "Output list should not be null")

  // Assert correct behavior was _dependency1.GetSomeList(input) called?
  _dependency1.VerifyAllExpectations();
  _dependency2.VerifyAllExpectations();

}

Обновление

IElementProcessor _elementProcessor;

public List<int> Process(int input)
{
    List<int> outputList = new List<int>();

    List<int> list = this.dependency1.GetSomeList(input);
    foreach(int element in list)
    {
        // ... Do some procssing to element
        _elementProcessor.ProcessElement(element);

        //Do some more processing
        int processedInt = this.dependency2.DoSomeProcessing(element);

        // ... Do some processing to processedInt
        _elementProcessor.ProcessInt(processedInt);

        outputList.Add(processedInt);
    }
    return outputList;
}

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

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

1 голос
/ 21 июля 2011

Изменена ли переменная "element" между началом foreach и методом DoSomeProcessing (element)? Если нет, я бы выбрал вариант № 2. Поскольку у вас есть контроль над зависимостями1 и зависимостями2, вы можете создать для них заглушку, чтобы вернуть некоторые элементы в списке и написать код в зависимости2 для обработки каждого из полученных элементов. из зависимости и вернуть правильное целое число.

Мой второй вариант будет 3. Мне не понравился первый.

Кстати, посмотрите, может ли Pex (http://research.microsoft.com/en-us/projects/pex/)) быть вам полезен в этом случае, так как у вас есть действительный код.

[] s

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...