2013-05-29 2 views
20

Я только что начал использовать AutoFixture.AutoMoq в своих модульных тестах, и я нахожу его очень полезным для создания объектов, где мне все равно, стоимость. В конце концов, анонимное создание объекта - вот что это такое.AutoFixture.AutoMoq дает известное значение для одного параметра конструктора

То, с чем я борюсь, - это когда я забочусь о одном или нескольких параметрах конструктора. Возьмите ниже:

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

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... 
} 

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

+3

Зачем вы хотите это сделать? Что такое сценарий? IME, подобные сценарии пахнут, как смешанные проблемы в «ExampleComponent». Есть причина, по которой AutoFixture не поддерживает это из коробки. –

+0

@MarkSeemann, что вы думаете о моем сценарии в отредактированном вопросе? Я не думаю, что это можно интерпретировать как смешанные проблемы. –

+3

Ну, мне довольно сложно сказать, потому что я не вижу, как вы собираетесь использовать «продолжительность жизни». Разве «продолжительность жизни» не должна взаимодействовать с текущим временем? Когда вы начнете думать о таких вопросах, как, возможно, абстракция все еще возникает. В прошлый раз, когда я сделал что-то подобное, я пришел вместо интерфейса ILease, что сделало логику кеша более гибкой, поскольку теперь я могу поддерживать: Абсолютный срок действия, Раздвижные окна, истечение срока действия LRU и множество других опций. –

ответ

13

Вы должны заменить:

string knownValue = fixture.Freeze<string>("My known value"); 

с:

fixture.Inject("My known value"); 

Вы можете прочитать больше о Injecthere.


На самом деле метод Freeze расширение делает:

var value = fixture.Create<T>(); 
fixture.Inject(value); 
return value; 

Это означает, что перегрузка вы использовали в тесте на самом деле называется Create<T> с семенем: Мои известное значение в результате «Мой известный value4d41f94f -1fc9-4115-9f29-e50bc2b4ba5e ".

+4

FWIW, хотя этот ответ является хорошим объяснением того, как работают 'Freeze' и' Inject', предлагаемое решение приведет к тому, что * все * строки получат значение «My known value» , что может и не быть тем, чего хочет OP. –

+0

Спасибо @NikosBaxevanis, который ответил на мой вопрос отлично. –

+0

@MarkSeemann - есть ли способ, которым я могу использовать AutoFixture для настройки различных известных значений одного и того же типа в конструкторе? –

10

Вы можете сделать что-то подобное. Представьте, что вы хотите присвоить конкретное значение аргументу TimeSpan под названием lifespan.

public class LifespanArg : ISpecimenBuilder 
{ 
    private readonly TimeSpan lifespan; 

    public LifespanArg(TimeSpan lifespan) 
    { 
     this.lifespan = lifespan; 
    } 

    public object Create(object request, ISpecimenContext context) 
    { 
     var pi = request as ParameterInfo; 
     if (pi == null) 
      return new NoSpecimen(request); 

     if (pi.ParameterType != typeof(TimeSpan) || 
      pi.Name != "lifespan") 
      return new NoSpecimen(request); 

     return this.lifespan; 
    } 
} 

Настоятельно, он может быть использован, как это:

var fixture = new Fixture(); 
fixture.Customizations.Add(new LifespanArg(mySpecialLifespanValue)); 

var sut = fixture.Create<CookieCache>(); 

Этот подход можно обобщить до некоторой степени, но в конце концов, мы ограничены отсутствием строго типизированных пути извлеките ParameterInfo из конкретного аргумента конструктора или метода.

+0

спасибо за это. Я думаю, что я добавлю общий метод расширения, основанный на этом, для использования в будущих тестах. –

19

Таким образом, я уверен, что люди могли бы разработать обобщенную реализацию предложения Марка, но я думал, что опубликую его для комментариев.

Я создал общий 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; } 
} 
+5

Работал как шарм. Сделал небольшую гибридную перегрузку, где замороженное значение все еще автогенерируется: 'public static T FreezeByName (это приспособление IFixture, название строки) { var value = fixture.Create (); fixture.Customizations.Add (новый параметр ParameterNameSpecimenBuilder (имя, значение)); возвращаемое значение; } ' – Holstebroe

+0

@Holstebroe +1 хорошая идея. –

+0

Это потрясающе! Благодарю. – Sam

8

Я плачу как @ Ник был почти там. При переопределении аргумента конструктора он должен быть для данного типа и ограничиваться только этим типом.

Сначала мы создаем новый ISpecimenBuilder, который смотрит на «Member.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); 
    }   
} 
+0

Работал отлично для меня, спасибо! – Lukie

5

Это, как представляется, наиболее полное решение.Так что я собираюсь добавить мое:

Первое, что нужно создать 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); 
    } 
} 

Затем создать метод расширения, что упрощает использование созданного застройщиком

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

Хорошее и чистое решение. В наших проектах иногда у нас есть несколько зависимостей для одного и того же типа, и мы хотим только протестировать один из них, этот именованный параметр очень подходит для наших нужд. Спасибо, что поделился. –

Смежные вопросы