Повторяющийся код в модульных тестах

Мы обнаруживаем, что во многих тестовых сценариях мы повторяем повторяющиеся настройки fixture/mock - как этот случай:

var fixture = new Fixture().Customize(new AutoMoqCustomization());
var encodingMock = fixture.Freeze<Mock<IEncodingWrapper>>();
var httpClientMock = fixture.Freeze<Mock<IHttpWebClientWrapper>>();
var httpResponseMock = fixture.Freeze<Mock<IHttpWebResponseWrapper>>();
var httpHeaderMock = fixture.Freeze<Mock<IHttpHeaderCollectionWrapper>>();
var etag = fixture.CreateAnonymous<string>();
byte[] data = fixture.CreateAnonymous<byte[]>();
Stream stream =  new MemoryStream(data);

encodingMock.Setup(m => m.GetBytes(It.IsAny<string>())).Returns(data);
httpHeaderMock.SetupGet(m => m[It.IsAny<string>()]).Returns(etag).Verifiable();
httpClientMock.Setup(m => m.GetResponse()).Returns(httpResponseMock.Object);
httpResponseMock.Setup(m => m.StatusCode).Returns(HttpStatusCode.OK);
httpResponseMock.SetupGet(m => m.Headers).Returns(httpHeaderMock.Object);
httpResponseMock.Setup(m => m.GetResponseStream()).Returns(stream);

В соответствии с идеей, что тесты должны быть автономными и читаемыми от начала до конца, мы не используем магические методы Setup/Teardown.

Можем ли мы каким-либо образом (настройки AutoFixture, вспомогательные методы) уменьшить "грубую работу" этих тестов?

Ответ 1

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

public class HttpMocksCustomization : CompositeCustomization
{
    public HttpMocksCustomization()
        : base(
            new AutoMoqCustomization(),
            new HttpWebClientWrapperMockCustomization(),
            new HttpWebResponseWrapperMockCustomization()
            // ...
            )
    {
    }
}

Каждая настройка может быть определена следующим образом:

public class HttpWebClientWrapperMockCustomization : ICustomization
{
    public void Customize(IFixture fixture)
    {
        var mock = new Mock<IHttpWebClientWrapper>();
        mock.Setup(m => m.GetResponse()).Returns(httpResponseMock.Object);

        fixture.Inject(mock);
    }
}

public class HttpWebResponseWrapperMockCustomization : ICustomization
{
    public void Customize(IFixture fixture)
    {
        var mock = new Mock<IHttpWebResponseWrapper>();
        mock.Setup(m => m.StatusCode).Returns(HttpStatusCode.OK);

        fixture.Inject(mock);
    }
}

// The rest of the Customizations.

Затем внутри тестового метода вы можете сделать это:

var fixture = new Fixture().Customize(new HttpMocksCustomization());

Таким образом, когда вы запрашиваете экземпляр Mock, вам не нужно повторять шаги настройки. Возвращается тот, который мы настроили ранее:

var httpClientMock = fixture.Freeze<Mock<IHttpWebClientWrapper>>();

Однако , если вы используете xUnit.net, вещи могут быть еще более упрощены.

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

public class AutoHttpMocksDataAttribute : AutoDataAttribute
{
    public AutoHttpMocksDataAttribute()
        : base(new Fixture().Customize(new HttpMocksCustomization()))
    {
    }
}

Затем в вашем методе тестирования вы можете передать Mocks как аргументы:

[Theory, AutoHttpMocksData]
public void MyTestMethod([Freeze]Mock<IHttpWebClientWrapper> httpClientMock, [Freeze]Mock<IHttpWebResponseWrapper> httpResponseMock)
{
    // ...
} 

Ответ 2

Из Растущее объектно-ориентированное программное обеспечение (GOOS) приходит кусочек хорошего совета: если тест трудно писать, он обращается к API системы Under Тест (SUT). Подумайте о перепроектировании SUT. В этом конкретном примере похоже, что SUT имеет по крайней мере четыре зависимости, что может указывать на нарушение принципа Single Responsibility Principle. Возможно ли было бы рефакторинг для служб фасадов?

Еще один замечательный совет GOOS состоит в том, что

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

Ответ 3

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

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