AutoFixture/AutoMoq игнорирует введенный экземпляр / замороженный макет

Короткий вывод теперь, когда решение найдено:

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

Оригинальный вопрос:

Я начал испытывать AutoFixture вчера для моих тестов xUnit.net, которые имеют Moq повсюду. Я надеялся заменить некоторые вещи из Moq или сделать их более удобными для чтения, и я особенно заинтересован в использовании AutoFixture в качестве SUT Factory.

Я вооружился несколькими постами Марка Симанна в блоге по AutoMocking и попытался работать оттуда, но я не продвинулся далеко.

Вот как выглядел мой тест без AutoFixture:

[Fact]
public void GetXml_ReturnsCorrectXElement()
{
    // Arrange
    string xmlString = @"
        <mappings>
            <mapping source='gcnm_loan_amount_min' target='gcnm_loan_amount_min_usd' />
            <mapping source='gcnm_loan_amount_max' target='gcnm_loan_amount_max_usd' />
        </mappings>";

    string settingKey = "gcCreditApplicationUsdFieldMappings";

    Mock<ISettings> settingsMock = new Mock<ISettings>();
    settingsMock.Setup(s => s.Get(settingKey)).Returns(xmlString);
    ISettings settings = settingsMock.Object;

    ITracingService tracing = new Mock<ITracingService>().Object;

    XElement expectedXml = XElement.Parse(xmlString);

    IMappingXml sut = new SettingMappingXml(settings, tracing);

    // Act
    XElement actualXml = sut.GetXml();

    // Assert
    Assert.True(XNode.DeepEquals(expectedXml, actualXml));
}

История здесь достаточно проста - убедитесь, что SettingMappingXml запрашивает ISettings зависимость с правильным ключом (который жестко запрограммирован / введено свойство) и возвращает результат в виде XElement, ITracingService актуально только в случае ошибки.

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

Введите AutoFixture - первая попытка:

[Fact]
public void GetXml_ReturnsCorrectXElement()
{
    // Arrange
    IFixture fixture = new Fixture();
    fixture.Customize(new AutoMoqCustomization());

    string xmlString = @"
        <mappings>
            <mapping source='gcnm_loan_amount_min' target='gcnm_loan_amount_min_usd' />
            <mapping source='gcnm_loan_amount_max' target='gcnm_loan_amount_max_usd' />
        </mappings>";

    string settingKey = "gcCreditApplicationUsdFieldMappings";

    Mock<ISettings> settingsMock = new Mock<ISettings>();
    settingsMock.Setup(s => s.Get(settingKey)).Returns(xmlString);
    ISettings settings = settingsMock.Object;
    fixture.Inject(settings);

    XElement expectedXml = XElement.Parse(xmlString);

    IMappingXml sut = fixture.CreateAnonymous<SettingMappingXml>();

    // Act
    XElement actualXml = sut.GetXml();

    // Assert
    Assert.True(XNode.DeepEquals(expectedXml, actualXml));
}

Я бы ожидал CreateAnonymous<SettingMappingXml>()при обнаружении ISettings параметр конструктора, чтобы заметить, что для этого интерфейса был зарегистрирован конкретный экземпляр, и внедрить его, однако он этого не делает, а создает новую анонимную реализацию.

Это особенно запутанно, так как fixture.CreateAnonymous<ISettings>() действительно возвращает мой экземпляр -

IMappingXml sut = new SettingMappingXml(fixture.CreateAnonymous<ISettings>(), fixture.CreateAnonymous<ITracingService>());

делает тест совершенно зеленым, и эта строка - именно то, что я ожидал, что AutoFixture выполнит внутренне при создании экземпляра SettingMappingXml,

Тогда есть концепция замораживания компонента, так что я пошел дальше, просто заморозив макет в приборе вместо того, чтобы получить смоделированный объект:

fixture.Freeze<Mock<ISettings>>(f => f.Do(m => m.Setup(s => s.Get(settingKey)).Returns(xmlString)));

Конечно, это прекрасно работает - пока я называю SettingMappingXml конструктор явно и не полагаться на CreateAnonymous(),



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

Это, в свою очередь, означает, что я должен упустить что-то довольно элементарное. Как я могу создать свою SUT, созданную AutoFixture, с предварительно сконфигурированным смоделированным объектом в качестве зависимости? Единственное, в чем я сейчас уверен, это то, что мне нужно AutoMoqCustomization так что мне не нужно ничего настраивать для ITracingService,

Пакеты AutoFixture/AutoMoq - 2.14.1, Moq - 3.1.416.3, все из NuGet. Версия.NET 4.5 (установлена ​​с VS2012), поведение одинаково в VS2012 и 2010.

При написании этого поста я обнаружил, что у некоторых людей возникают проблемы с Moq 4.0 и перенаправлениями привязки сборки, поэтому я тщательно очистил свое решение от любых экземпляров Moq 4 и установил Moq 3.1, установив AutoFixture.AutoMoq в "чистые" проекты. Тем не менее, поведение моего теста остается неизменным.

Спасибо за любые указатели и объяснения.

Обновление: вот код конструктора, который попросил Марк:

public SettingMappingXml(ISettings settingSource, ITracingService tracing)
{
    this._settingSource = settingSource;
    this._tracing = tracing;

    this.SettingKey = "gcCreditApplicationUsdFieldMappings";
}

А для полноты GetXml() Метод выглядит так:

public XElement GetXml()
{
    int errorCode = 10600;

    try
    {
        string mappingSetting = this._settingSource.Get(this.SettingKey);
        errorCode++;

        XElement mappingXml = XElement.Parse(mappingSetting);
        errorCode++;

        return mappingXml;
    }
    catch (Exception e)
    {
        this._tracing.Trace(errorCode, e.Message);
        throw;
    }
}

SettingKey это просто автоматическое свойство.

2 ответа

Решение

Предполагая, что SettingKey свойство определяется следующим образом, теперь я могу воспроизвести проблему:

public string SettingKey { get; set; }

Что происходит, так это то, что Test Doubles, внедренные в экземпляр SettingMappingXml, прекрасно работают, но потому что SettingKey доступно для записи, функция авто-свойств AutoFixture запускает и изменяет значение.

Рассмотрим этот код:

var fixture = new Fixture().Customize(new AutoMoqCustomization());
var sut = fixture.CreateAnonymous<SettingMappingXml>();
Console.WriteLine(sut.SettingKey);

Это печатает что-то вроде этого:

SettingKey83b75965-2886-4308-bcc4-eb0f8e63de09

Несмотря на то, что все тестовые пары правильно введены, ожидание в Setup метод не встречен.

Есть много способов решить эту проблему.

Защитить инварианты

Надлежащим способом решения этой проблемы является использование модульного теста и автофиксации в качестве механизма обратной связи. Это один из ключевых моментов в ГСНО: проблемы с модульными тестами часто являются признаком недостатка проекта, а не ошибкой самого модульного теста (или автофиксации).

В этом случае это указывает на то, что дизайн недостаточно надежен. Действительно ли уместно, что клиент может манипулировать SettingKey по желанию?

Как минимум, я бы рекомендовал альтернативную реализацию, например:

public string SettingKey { get; private set; }

С этим изменением мое репро проходит.

Опустить SettingKey

Если вы не можете (или не хотите) изменить свой дизайн, вы можете указать AutoFixture пропустить установку SettingKey имущество:

IMappingXml sut = fixture
    .Build<SettingMappingXml>()
    .Without(s => s.SettingKey)
    .CreateAnonymous();

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

fixture.Customize<SettingMappingXml>(
    c => c.Without(s => s.SettingKey));
IMappingXml sut = fixture.CreateAnonymous<SettingMappingXml>();

Чтобы пойти дальше, вы можете заключить, что Customize вызов метода в настройке.

public class SettingMappingXmlCustomization : ICustomization
{
    public void Customize(IFixture fixture)
    {
        fixture.Customize<SettingMappingXml>(
            c => c.Without(s => s.SettingKey));
    }
}

Это требует от вас создать Fixture Экземпляр с этой настройкой:

IFixture fixture = new Fixture()
    .Customize(new SettingMappingXmlCustomization())
    .Customize(new AutoMoqCustomization());

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

public class TestConventions : CompositeCustomization
{
    public TestConventions()
        : base(
            new SettingMappingXmlCustomization(),
            new AutoMoqCustomization())
    {
    }
}

Это позволяет вам всегда создавать Fixture например, как это:

IFixture fixture = new Fixture().Customize(new TestConventions());

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

Наконец, поскольку выглядит так, как будто вы используете xUnit.net, вы можете использовать интеграцию AutoFixture с xUnit.net, но перед этим вам нужно будет использовать менее обязательный стиль манипулирования Fixture, Оказывается, что код, который создает, настраивает и внедряет ISettings Test Double настолько идиоматичен, что у него есть ярлык Freeze:

fixture.Freeze<Mock<ISettings>>()
    .Setup(s => s.Get(settingKey)).Returns(xmlString);

После этого следующим шагом будет определение пользовательского атрибута AutoDataAttribute:

public class AutoConventionDataAttribute : AutoDataAttribute
{
    public AutoConventionDataAttribute()
        : base(new Fixture().Customize(new TestConventions()))
    {
    }
}

Теперь вы можете свести тест к базовым элементам, избавившись от всех шумов, что позволяет тесту кратко выразить только то, что имеет значение:

[Theory, AutoConventionData]
public void ReducedTheory(
    [Frozen]Mock<ISettings> settingsStub,
    SettingMappingXml sut)
{
    string xmlString = @"
        <mappings>
            <mapping source='gcnm_loan_amount_min' target='gcnm_loan_amount_min_usd' />
            <mapping source='gcnm_loan_amount_max' target='gcnm_loan_amount_max_usd' />
        </mappings>";
    string settingKey = "gcCreditApplicationUsdFieldMappings";
    settingsStub.Setup(s => s.Get(settingKey)).Returns(xmlString);

    XElement actualXml = sut.GetXml();

    XElement expectedXml = XElement.Parse(xmlString);
    Assert.True(XNode.DeepEquals(expectedXml, actualXml));
}

Другие опции

Для первоначального прохождения теста вы также можете полностью отключить Auto-свойства:

fixture.OmitAutoProperties = true;

В первом тесте вы можете создать экземпляр Fixture класс с AutoMoqCustomization применяется:

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

Тогда единственные изменения:

Шаг 1

// The following line:
Mock<ISettings> settingsMock = new Mock<ISettings>();
// Becomes:
Mock<ISettings> settingsMock = fixture.Freeze<Mock<ISettings>>();

Шаг 2

// The following line:
ITracingService tracing = new Mock<ITracingService>().Object;
// Becomes:
ITracingService tracing = fixture.Freeze<Mock<ITracingService>>().Object;

Шаг 3

// The following line:
IMappingXml sut = new SettingMappingXml(settings, tracing);
// Becomes:
IMappingXml sut = fixture.CreateAnonymous<SettingMappingXml>();

Это оно!


Вот как это работает:

Внутренне Freeze создает экземпляр запрошенного типа (например, Mock<ITracingService>), а затем внедряет его, чтобы он всегда возвращал этот экземпляр при повторном запросе.

Это то, что мы делаем в Step 1 а также Step 2,

В Step 3 мы запрашиваем экземпляр SettingMappingXml тип, который зависит от ISettings а также ITracingService, Так как мы используем Auto Mocking, Fixture класс будет предоставлять макеты для этих интерфейсов. Тем не менее, мы ранее вводили их с Freeze поэтому уже созданные макеты теперь автоматически добавляются.

Другие вопросы по тегам