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

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

AutoFixture просто возвращает замороженный макет; у меня сут, который также был сгенерирован 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 - это просто автоматическое свойство.


person TeaDrivenDev    schedule 21.11.2012    source источник
comment
Я не могу воспроизвести то, о чем вы сообщаете. Вышеупомянутый тест (с AutoFixture) на моей машине прошел успешно. Как выглядит конструктор SettingMappingXml?   -  person Mark Seemann    schedule 21.11.2012


Ответы (2)


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

public string SettingKey { get; set; }

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

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

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

Это печатает примерно так:

Клавиша настройки 83b75965-2886-4308-bcc4-eb0f8e63de09

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

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

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

Правильный способ решить эту проблему - использовать модульный тест и AutoFixture в качестве механизма обратной связи. Это один из ключевых моментов в GOOS: проблемы с модульными тестами часто являются симптомом недостатка дизайна, а не ошибкой модульного теста. (или AutoFixture).

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

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

public string SettingKey { get; private set; }

С этим изменением моя репродукция проходит.

Опустить ключ настройки

Если вы не можете (или не хотите) изменить свой дизайн, вы можете указать 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 в Customization.

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, настолько идиоматичен, что имеет ярлык под названием Заморозить:

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));
}

Другие варианты

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

fixture.OmitAutoProperties = true;
person Mark Seemann    schedule 22.11.2012
comment
Спасибо за очень обстоятельный ответ. Так что, по крайней мере, я был прав в своем ответе Никосу - ошибка не очевидная, но глупая. Назначение общедоступного свойства состояло в том, чтобы позволить мне в какой-то момент использовать класс с другим значением, хотя сейчас мне нужно было только значение по умолчанию, установленное в конструкторе. Срезая углы и по дороге стреляя себе в ногу ... - person TeaDrivenDev; 22.11.2012

В первом тесте вы можете создать экземпляр класса 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, поэтому уже созданные макеты теперь поставляются автоматически.

person Nikos Baxevanis    schedule 21.11.2012
comment
Спасибо за ответ. Шаг 2 - это то, что мне совсем не нужно делать - меня не волнует экземпляр ITracingService, и я просто хочу, чтобы AutoFixture его обработала. Что касается экземпляра ISetting - CreateAnonymous<SettingMappingXml>() есть один, но не тот, который я заморозил. Однако CreateAnonymous<ISetting>() действительно возвращает замороженный экземпляр. С ответами Марка и вашими ответами у меня начинает складываться впечатление, что должно быть какое-то место, где я сделал что-то не очевидное, но довольно глупое. - person TeaDrivenDev; 22.11.2012
comment
Вы можете пропустить шаг 2, и тогда экземпляр с автоматическим издевательством будет предоставлен классом Fixture. Из исходного кода конструктора после выполнения шага 3 вы должны получить экземпляр SettingMappingXml с имитируемым замороженным ISetting экземпляром. - person Nikos Baxevanis; 22.11.2012