我的测试要求我将不可变Response
对象(见下文)上的Rsvp
属性设置为特定值。
public class Rsvp
{
public string Response { get; private set; }
public Rsvp(string response)
{
Response = response;
}
}
我最初尝试使用Build<Rsvp>().With(x => x.Rsvp, "Attending")
执行此操作,但意识到这只支持可写属性。
我用Build<Rsvp>().FromFactory(new Rsvp("Attending"))
替换了它。这可行,但对于更复杂的对象来说很麻烦,因为它们与某些属性无关。
例如,如果Rsvp
对象具有CreatedDate
属性,则此实例化对象的方法将强制我编写Build<Rsvp>().FromFactory(new Rsvp("Attending", fixture.Create<DateTime>()))
。
有没有办法只为不可变对象的含义属性指定值?
答案 0 :(得分:9)
AutoFixture最初是作为测试驱动开发(TDD)的工具而构建的,TDD完全是关于反馈的。本着GOOS的精神,你应该倾听你的测试。如果测试难以编写,则应考虑API设计。 AutoFixture倾向于放大这种反馈。
坦率地说,不可变类型是C#的一个难点,但是如果从F#中获取提示并引入复制和更新语义,则可以更轻松地使用类Rsvp
这样的类。 。如果你像这样修改Rsvp
,整体工作会更容易,因此,作为副产品,也可以用于单元测试:
public class Rsvp
{
public string Response { get; private set; }
public DateTime CreatedDate { get; private set; }
public Rsvp(string response, DateTime createdDate)
{
Response = response;
CreatedDate = createdDate;
}
public Rsvp WithResponse(string newResponse)
{
return new Rsvp(newResponse, this.CreatedDate);
}
public Rsvp WithCreatedDate(DateTime newCreatedDate)
{
return new Rsvp(this.Response, newCreatedDate);
}
}
请注意,我添加了两个WithXyz
方法,这些方法返回一个新实例,其中一个值已更改,但所有其他值保持不变。
这样您就可以创建Rsvp
的实例,用于测试目的:
var fixture = new Fixture();
var seed = fixture.Create<Rsvp>();
var sut = seed.WithResponse("Attending");
或者,作为一个单行:
var sut = new Fixture().Create<Rsvp>().WithResponse("Attending");
如果您无法更改Rsvp
,则可以添加WithXyz
方法作为扩展方法。
一旦你完成了大约十几次,你就厌倦了,现在是时候转移到F#了,所有这些(以及更多)是内置的:
type Rsvp = {
Response : string
CreatedDate : DateTime }
您可以使用AutoFixture创建Rsvp
记录,如下所示:
let fixture = Fixture()
let seed = fixture.Create<Rsvp>()
let sut = { seed with Response = "Attending" }
或者,作为一个单行:
let sut = { Fixture().Create<Rsvp>() with Response = "Attending" }
答案 1 :(得分:3)
只要Response
属性为readonly * ,您就可以为SpecimenBuilder
类型定义自定义Rsvp
:
internal class RsvpBuilder : ISpecimenBuilder
{
public object Create(object request, ISpecimenContext context)
{
var pi = request as ParameterInfo;
if (pi == null)
return new NoSpecimen();
if (pi.ParameterType != typeof(string) || pi.Name != "response")
return new NoSpecimen();
return "Attending";
}
}
以下测试通过:
[Fact]
public void ResponseIsCorrect()
{
var fixture = new Fixture();
fixture.Customizations.Add(new RsvpBuilder());
var sut = fixture.Create<Rsvp>();
var actual = sut.Response;
Assert.Equal("Attending", actual);
}
* 如果由于某种原因Response
属性变为可写,您可以按照此answer中的解决方案进行操作。
答案 2 :(得分:0)
扩展Nikos的答案,我们可以将自定义概括化为可与任何属性一起使用,例如:
"jdbc:sqlserver://localhost;instance=MSSQLSERVER;databaseName=name_of_database;user=sa;password=your_password;"
但是接下来我们需要自定义扩展方法以使其易于使用:
public class OverridePropertyBuilder<T, TProp> : ISpecimenBuilder
{
private readonly PropertyInfo _propertyInfo;
private readonly TProp _value;
public OverridePropertyBuilder(Expression<Func<T, TProp>> expr, TProp value)
{
_propertyInfo = (expr.Body as MemberExpression)?.Member as PropertyInfo ??
throw new InvalidOperationException("invalid property expression");
_value = value;
}
public object Create(object request, ISpecimenContext context)
{
var pi = request as ParameterInfo;
if (pi == null)
return new NoSpecimen();
var camelCase = Regex.Replace(_propertyInfo.Name, @"(\w)(.*)",
m => m.Groups[1].Value.ToLower() + m.Groups[2]);
if (pi.ParameterType != typeof(TProp) || pi.Name != camelCase)
return new NoSpecimen();
return _value;
}
}
然后在您的示例中将其用作:
public class FixtureCustomization<T>
{
public Fixture Fixture { get; }
public FixtureCustomization(Fixture fixture)
{
Fixture = fixture;
}
public FixtureCustomization<T> With<TProp>(Expression<Func<T, TProp>> expr, TProp value)
{
Fixture.Customizations.Add(new OverridePropertyBuilder<T, TProp>(expr, value));
return this;
}
public T Create() => Fixture.Create<T>();
}
public static class CompositionExt
{
public static FixtureCustomization<T> For<T>(this Fixture fixture)
=> new FixtureCustomization<T>(fixture);
}