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