Как можно избежать больших многошаговых юнит-тестов? - PullRequest
3 голосов
/ 26 февраля 2009

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

public class Foo
{  
    public Foo(IDependency1 dp1, IDependency2 dp2, IDependency3 dp3, IDependency4 dp4)
    {
        ...
    }

    public IEnumerable<int> Frobnicate(IInput input)
    {
        var step1 = _dependency1.DoSomeWork(input);
        var step2 = _dependency2.DoAdditionalWork(step1);
        var step3 = _dependency3.DoEvenMoreWork(step2);
        return _dependency4.DoFinalWork(step3);
    }

    private IDependency1 _dependency1;
    private IDependency2 _dependency2;
    private IDependency3 _dependency3;
    private IDependency4 _dependency4;
}

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

[Test]
public void FrobnicateDoesSomeWorkAndAdditionalWorkAndEvenMoreWorkAndFinalWorkAndReturnsResult()
{
    var fakeInput = ...;
    var step1 = ...;
    var step2 = ...;
    var step3 = ...;
    var fakeOutput = ...;

    MockRepository mocks = new MockRepository();

    var mockDependency1 = mocks.CreateMock<IDependency1>();
    Expect.Call(mockDependency1.DoSomeWork(fakeInput)).Return(step1);

    var mockDependency2 = mocks.CreateMock<IDependency2>();
    Expect.Call(mockDependency2.DoAdditionalWork(step1)).Return(step2);

    var mockDependency3 = mocks.CreateMock<IDependency3>();
    Expect.Call(mockDependency3.DoEvenMoreWork(step2)).Return(step3);

    var mockDependency4 = mocks.CreateMock<IDependency4>();
    Expect.Call(mockDependency4.DoFinalWork(step3)).Return(fakeOutput);

    mocks.ReplayAll();

    Foo foo = new Foo(mockDependency1, mockDependency2, mockDependency3, mockDependency4);
    Assert.AreSame(fakeOutput, foo.Frobnicate(fakeInput));

    mocks.VerifyAll();
}

Это кажется невероятно хрупким. Любое изменение в реализации Frobnicate приводит к сбою этого теста (например, разбиение шага 3 на 2 подэтапа). Это все-в-одном, поэтому попытка использовать несколько небольших тестов не сработает. Он начинает приближаться к коду только для записи для будущих сопровождающих, включая меня в следующем месяце, когда я забыл, как он работает. Там должен быть лучший путь! Верно?

Ответы [ 4 ]

5 голосов
/ 26 февраля 2009

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

Затем выполните интеграционный тест Foo, используя реальные реализации IDependencyX. Тогда вы будете знать, что все отдельные части правильно соединены. Часто достаточно просто протестировать с одним входом, потому что вы тестируете только простой клейкий код.

1 голос
/ 21 мая 2009

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

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

0 голосов
/ 26 февраля 2009

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

Например, если ваш пример кода был программным обеспечением для GPS, вы не тестируете реальные функции, такие как навигация, расчет правильных маршрутов и т. Д., Но вместо этого пользователь может включить его, ввести некоторые данные, отобразить маршруты и выключи его снова. Видишь разницу?

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

0 голосов
/ 26 февраля 2009

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

Пара хороших ссылок:

Проблема в том, что BDD требуется время для освоения.

Быстрый пример, украденный по последней ссылке ( Стив Харман ). Обратите внимание, что для каждого метода теста есть только одно утверждение.

using Skynet.Core

public class when_initializing_core_module
{
    ISkynetMasterController _skynet;

    public void establish_context()
    {
        //we'll stub it...you know...just in case
        _skynet = new MockRepository.GenerateStub<ISkynetMasterController>();
        _skynet.Initialize();
    }

    public void it_should_not_become_self_aware()
    {
        _skynet.AssertWasNotCalled(x => x.InitializeAutonomousExecutionMode());
    }

    public void it_should_default_to_human_friendly_mode()
    {
        _skynet.AssessHumans().ShouldEqual(RelationshipTypes.Friendly);
    }
}

public class when_attempting_to_wage_war_on_humans
{
    ISkynetMasterController _skynet;
    public void establish_context()
    {
        _skynet = new MockRepository.GenerateStub<ISkynetMasterController>();
        _skynet.Stub(x => 
            x.DeployRobotArmy(TargetTypes.Humans)).Throws<OperationInvalidException>();
    }

    public void because()
    {
        _skynet.DeployRobotArmy(TargetTypes.Humans);
    }

    public void it_should_not_allow_the_operation_to_succeed()
    {
        _skynet.AssertWasThrown<OperationInvalidException>();
    }
}
...