AutoFixture.AutoMoq fournit une valeur connue pour un paramètre de constructeur
je viens de commencer à utiliser AutoFixture.AutoMoq dans mes tests unitaires et je le trouve très utile pour créer des objets où je ne me soucie pas de la valeur spécifique. Après tout, la création anonyme d'objets est ce dont il s'agit.
ce qui me pose problème, c'est quand je me soucie d'un ou plusieurs paramètres du constructeur. Prendre ExampleComponent
ci-dessous:
public class ExampleComponent
{
public ExampleComponent(IService service, string someValue)
{
}
}
je veux écrire un essai où je fournir une valeur spécifique pour someValue
mais laissez IService
pour être créé automatiquement par AutoFixture.AutoMoq.
je sais comment utiliser Freeze
sur mon IFixture
conserver une valeur connue, qui sera injecté dans un composant, mais je n'arrive pas à voir comment alimentation une valeur connue de mon propre.
voici ce que j'aimerais idéalement faire:
[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
...
}
je sais que je pourrais le faire en appelant directement le constructeur mais ce ne serait plus de la création d'objet anonyme. Est-il un moyen pour utiliser AutoFixture.AutoMock comme ceci ou Ai-je besoin d'incorporer un conteneur DI dans mes tests pour être en mesure de faire ce que je veux?
EDIT:
j'aurais probablement dû faire moins d'absract dans ma question initiale donc voici mon scénario spécifique.
j'ai un ICache
interface génériques TryRead<T>
et Write<T>
méthodes:
public interface ICache
{
bool TryRead<T>(string key, out T value);
void Write<T>(string key, T value);
// other methods not shown...
}
je suis à la mise en œuvre d'un CookieCache
où ITypeConverter
gère la conversion des objets vers et à partir de chaînes et lifespan
est utilisé pour définir la date d'expiration d'un 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...
}
donc en écrivant un test pour la date d'expiration d'un cookie, je me soucie de la durée de vie mais pas tellement du convertisseur.
5 réponses
vous devez remplacer:
string knownValue = fixture.Freeze<string>("My known value");
avec:
fixture.Inject("My known value");
vous pouvez en savoir plus sur Inject
ici.
en Fait, le Freeze
la méthode d'extension fait:
var value = fixture.Create<T>();
fixture.Inject(value);
return value;
ce qui signifie que la surcharge que vous avez utilisée dans le test a en fait appelé Create<T>
avec une graine: Ma valeur connue entraînant "Mon connu value4d41f94f-1fc9-4115-9f29-e50bc2b4ba5e".
donc je suis sûr que les gens pourraient travailler sur la mise en œuvre généralisée de la suggestion de Mark, mais j'ai pensé que je voudrais le poster pour des commentaires.
j'ai créé un générique ParameterNameSpecimenBuilder
basé sur Mark's 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;
}
}
j'ai alors défini unFreezeByName
méthode d'extension sur IFixture
qui définit la personnalisation:
public static class FreezeByNameExtension
{
public static void FreezeByName<T>(this IFixture fixture, string name, T value)
{
fixture.Customizations.Add(new ParameterNameSpecimenBuilder<T>(name, value));
}
}
Le test suivant, maintenant pass:
[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; }
}
Vous faire quelque chose comme cela. Imaginez que vous voulez attribuer une valeur particulière à TimeSpan
argument appelé 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;
}
}
Impérativement, il pourrait être utilisé comme ceci:
var fixture = new Fixture();
fixture.Customizations.Add(new LifespanArg(mySpecialLifespanValue));
var sut = fixture.Create<CookieCache>();
cette approche peut être généralisée dans une certaine mesure, mais en fin de compte, nous sommes limités par l'absence d'une manière fortement typée d'extraire un ParameterInfo d'un argument de constructeur ou de méthode particulier.
j'ai l'impression que @ Nick était presque là. Quand on supplante l'argument du constructeur, il faut qu'il soit pour le type donné et qu'il soit limité à ce type seulement.
nous créons D'abord un nouveau ISpecimenBuilder qui regarde le "membre".DeclaringType" pour garder la bonne portée.
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;
}
}
ensuite, nous créons une méthode d'extension pour nous permettre de la raccorder facilement avec 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;
}
}
maintenant nous créons deux classes similaires pour tester avec.
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; }
}
finalement, nous le testons avec une extension du test original pour voir qu'il ne supplantera pas les arguments du constructeur nommés et dactylographiés de la même manière.
[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);
}
}
Cela semble être la solution la plus complète définie ici. Je vais donc ajouter le mien:
La première chose à créer ISpecimenBuilder
qui peut gérer plusieurs paramètres du constructeur
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);
}
}
puis créer une méthode d'extension qui simplifie l'utilisation de 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);
}
}
Et d'utilisation:
var f = new Fixture();
f.FreezeActivator<UserInfo>(new { privateId = 15, parentId = (long?)33 });