AutoFixture.AutoMoq предоставляет известное значение для одного параметра конструктора
я только начал использовать AutoFixture.AutoMoq в моих модульных тестах, и я нахожу его очень полезным для создания объектов, где меня не волнует конкретное значение. В конце концов, анонимное создание объектов-это то, о чем идет речь.
то, с чем я борюсь, - это когда я забочусь об одном или нескольких параметрах конструктора. Взять ExampleComponent
ниже:
public class ExampleComponent
{
public ExampleComponent(IService service, string someValue)
{
}
}
я хочу написать тест, где я предоставляю определенное значение для someValue
но оставить IService
будет создан автоматически AutoFixture.AutoMoq.
Я знаю, как использовать Freeze
на IFixture
чтобы удержать известное значение, которое будет введено в компонент, но я не совсем понимаю, как источник известная моя собственная ценность.
вот что я хотел бы сделать в идеале:
[TestMethod]
public void Create_ExampleComponent_With_Known_SomeValue()
{
// create a fixture that supports automocking
IFixture fixture = new Fixture().Customize(new AutoMoqCustomization());
// supply a known value for someValue (this method doesn't exist)
string knownValue = fixture.Freeze<string>("My known value");
// create an ExampleComponent with my known value injected
// but without bothering about the IService parameter
ExampleComponent component = this.fixture.Create<ExampleComponent>();
// exercise component knowning it has my known value injected
...
}
Я знаю, что могу сделать это, вызвав конструктор напрямую, но это больше не будет анонимным созданием объекта. Есть есть способ использовать AutoFixture.AutoMock как это или мне нужно включить контейнер DI в мои тесты, чтобы иметь возможность делать то, что я хочу?
EDIT:
я, вероятно, должен был быть менее абсракт в моем первоначальном вопросе, так что вот мой конкретный сценарий.
у меня есть ICache
интерфейс, который имеет универсальный TryRead<T>
и Write<T>
методы:
public interface ICache
{
bool TryRead<T>(string key, out T value);
void Write<T>(string key, T value);
// other methods not shown...
}
я реализую CookieCache
здесь ITypeConverter
обрабатывает преобразование объектов в строки и из строк и lifespan
используется для установки даты истечения срока действия файла cookie.
public class CookieCache : ICache
{
public CookieCache(ITypeConverter converter, TimeSpan lifespan)
{
// usual storing of parameters
}
public bool TryRead<T>(string key, out T result)
{
// read the cookie value as string and convert it to the target type
}
public void Write<T>(string key, T value)
{
// write the value to a cookie, converted to a string
// set the expiry date of the cookie using the lifespan
}
// other methods not shown...
}
поэтому при написании теста на срок годности cookie я забочусь о продолжительности жизни, но не столько о конвертере.
5 ответов
вы должны заменить:
string knownValue = fixture.Freeze<string>("My known value");
С:
fixture.Inject("My known value");
вы можете прочитать больше о Inject
здесь.
на самом деле Freeze
метод расширения:
var value = fixture.Create<T>();
fixture.Inject(value);
return value;
что означает, что перегрузка, которую вы использовали в тесте, фактически называется Create<T>
с семенем: мое известное значение в результате "мое известное значение 4d41f94f-1fc9-4115-9f29-e50bc2b4ba5e".
поэтому я уверен, что люди могли бы разработать обобщенную реализацию предложения Марка, но я думал, что опубликую его для комментариев.
Я создал общий ParameterNameSpecimenBuilder
на основе Марка LifeSpanArg
:
public class ParameterNameSpecimenBuilder<T> : ISpecimenBuilder
{
private readonly string name;
private readonly T value;
public ParameterNameSpecimenBuilder(string name, T value)
{
// we don't want a null name but we might want a null value
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentNullException("name");
}
this.name = name;
this.value = value;
}
public object Create(object request, ISpecimenContext context)
{
var pi = request as ParameterInfo;
if (pi == null)
{
return new NoSpecimen(request);
}
if (pi.ParameterType != typeof(T) ||
!string.Equals(
pi.Name,
this.name,
StringComparison.CurrentCultureIgnoreCase))
{
return new NoSpecimen(request);
}
return this.value;
}
}
затем я определил общий FreezeByName
метод расширения на IFixture
, который задает настройки:
public static class FreezeByNameExtension
{
public static void FreezeByName<T>(this IFixture fixture, string name, T value)
{
fixture.Customizations.Add(new ParameterNameSpecimenBuilder<T>(name, value));
}
}
теперь пройдет следующий тест:
[TestMethod]
public void FreezeByName_Sets_Value1_And_Value2_Independently()
{
//// Arrange
IFixture arrangeFixture = new Fixture();
string myValue1 = arrangeFixture.Create<string>();
string myValue2 = arrangeFixture.Create<string>();
IFixture sutFixture = new Fixture();
sutFixture.FreezeByName("value1", myValue1);
sutFixture.FreezeByName("value2", myValue2);
//// Act
TestClass<string> result = sutFixture.Create<TestClass<string>>();
//// Assert
Assert.AreEqual(myValue1, result.Value1);
Assert.AreEqual(myValue2, result.Value2);
}
public class TestClass<T>
{
public TestClass(T value1, T value2)
{
this.Value1 = value1;
this.Value2 = value2;
}
public T Value1 { get; private set; }
public T Value2 { get; private set; }
}
вы мог бы сделайте что-нибудь вроде этого. Представьте, что вы хотите присвоить определенное значение
Я гонорар, как @Nick был почти там. При переопределении аргумента конструктора он должен быть для данного типа и ограничиваться только этим типом.
Сначала мы создаем новый ISpecimenBuilder, который смотрит на " член.DeclaringType", чтобы сохранить правильную область.
public class ConstructorArgumentRelay<TTarget,TValueType> : ISpecimenBuilder
{
private readonly string _paramName;
private readonly TValueType _value;
public ConstructorArgumentRelay(string ParamName, TValueType value)
{
_paramName = ParamName;
_value = value;
}
public object Create(object request, ISpecimenContext context)
{
if (context == null)
throw new ArgumentNullException("context");
ParameterInfo parameter = request as ParameterInfo;
if (parameter == null)
return (object)new NoSpecimen(request);
if (parameter.Member.DeclaringType != typeof(TTarget) ||
parameter.Member.MemberType != MemberTypes.Constructor ||
parameter.ParameterType != typeof(TValueType) ||
parameter.Name != _paramName)
return (object)new NoSpecimen(request);
return _value;
}
}
Далее мы создаем метод расширения, чтобы позволить нам легко подключить его с AutoFixture.
public static class AutoFixtureExtensions
{
public static IFixture ConstructorArgumentFor<TTargetType, TValueType>(
this IFixture fixture,
string paramName,
TValueType value)
{
fixture.Customizations.Add(
new ConstructorArgumentRelay<TTargetType, TValueType>(paramName, value)
);
return fixture;
}
}
теперь мы создаем два аналогичных класса для тестирования с.
public class TestClass<T>
{
public TestClass(T value1, T value2)
{
Value1 = value1;
Value2 = value2;
}
public T Value1 { get; private set; }
public T Value2 { get; private set; }
}
public class SimilarClass<T>
{
public SimilarClass(T value1, T value2)
{
Value1 = value1;
Value2 = value2;
}
public T Value1 { get; private set; }
public T Value2 { get; private set; }
}
наконец, мы тестируем его с расширением исходного теста, чтобы увидеть, что он не будет переопределять аналогично именованные и типизированные аргументы конструктора.
[TestFixture]
public class AutoFixtureTests
{
[Test]
public void Can_Create_Class_With_Specific_Parameter_Value()
{
string wanted = "This is the first string";
string wanted2 = "This is the second string";
Fixture fixture = new Fixture();
fixture.ConstructorArgumentFor<TestClass<string>, string>("value1", wanted)
.ConstructorArgumentFor<TestClass<string>, string>("value2", wanted2);
TestClass<string> t = fixture.Create<TestClass<string>>();
SimilarClass<string> s = fixture.Create<SimilarClass<string>>();
Assert.AreEqual(wanted,t.Value1);
Assert.AreEqual(wanted2,t.Value2);
Assert.AreNotEqual(wanted,s.Value1);
Assert.AreNotEqual(wanted2,s.Value2);
}
}
Это, кажется, наиболее полное решение, установленное здесь. Поэтому я добавлю свое:
первое, что нужно создать ISpecimenBuilder
, который может обрабатывать несколько параметров конструктора
internal sealed class CustomConstructorBuilder<T> : ISpecimenBuilder
{
private readonly Dictionary<string, object> _ctorParameters = new Dictionary<string, object>();
public object Create(object request, ISpecimenContext context)
{
var type = typeof (T);
var sr = request as SeededRequest;
if (sr == null || !sr.Request.Equals(type))
{
return new NoSpecimen(request);
}
var ctor = type.GetConstructors(BindingFlags.Instance | BindingFlags.Public).FirstOrDefault();
if (ctor == null)
{
return new NoSpecimen(request);
}
var values = new List<object>();
foreach (var parameter in ctor.GetParameters())
{
if (_ctorParameters.ContainsKey(parameter.Name))
{
values.Add(_ctorParameters[parameter.Name]);
}
else
{
values.Add(context.Resolve(parameter.ParameterType));
}
}
return ctor.Invoke(BindingFlags.CreateInstance, null, values.ToArray(), CultureInfo.InvariantCulture);
}
public void Addparameter(string paramName, object val)
{
_ctorParameters.Add(paramName, val);
}
}
затем создайте метод расширения, который упрощает использование created builder
public static class AutoFixtureExtensions
{
public static void FreezeActivator<T>(this IFixture fixture, object parameters)
{
var builder = new CustomConstructorBuilder<T>();
foreach (var prop in parameters.GetType().GetProperties())
{
builder.Addparameter(prop.Name, prop.GetValue(parameters));
}
fixture.Customize<T>(x => builder);
}
}
и использование:
var f = new Fixture();
f.FreezeActivator<UserInfo>(new { privateId = 15, parentId = (long?)33 });