我想验证(断言)我的DTO对象上的某些属性已设置。 我试图用Fluent Assertions来做,但以下代码似乎不起作用:
mapped.ShouldHave().Properties(
x => x.Description,
...more
x => x.Id)
.Should().NotBeNull();
是否可以通过Fluent Assertions或其他工具实现这一目标? Fluent断言具有ShouldBeEquivalentTo,但实际上我只关心那些是不是空/空,所以我无法利用它。
当然我可以在每个属性级别上做一个Assert,但对一些更优雅的方式感兴趣。
答案 0 :(得分:5)
实际上,Properties
方法返回PropertiesAssertion
,它只有EqualTo
方法进行相等比较。 否 NotEqualTo
方法或NotNull
。在您的测试中,您的预期PropertiesAssertion
不是null
,这就是为什么它总是会通过。
您可以实现AssertionHelper
静态类并传递Func
的数组,您将使用它来验证对象。这是非常天真的实现,你不会得到很好的错误报告,但我只是展示了一般的想法
public static void CheckAllPropertiesAreNotNull<T>(this T objectToInspect,
params Func<T, object>[] getters)
{
if (getters.Any(f => f(objectToInspect) == null))
Assert.Fail("some of the properties are null");
}
现在,此测试将失败并显示some of the properties are null
消息
var myDto = new MyDto();
myDto.CheckAllPropertiesAreNotNull(x => x.Description,
x => x.Id);
该解决方案有两个问题:
Id
属性属于值类型,则getter(objectToInspect) == null
始终为false
要解决第一点,您可以:
CheckAllPropertiesAreNotNull
创建一个重载,每个都有不同数量的Generic Func<TInput, TFirstOutput> firstGetter
,然后您将每个getter的返回值与相应的default(TFirstOutput)
Activator
创建默认实例并调用Equals
我会告诉你第二个案例。您可以创建一个IsDefault
方法,该方法接受object
类型的参数(注意,这可能是一个盒装的int):
private static bool IsDefault(this object value)
{
if (value == null)
return true;
if (!value.GetType().IsValueType) //if a reference type and not null
return false;
//all value types should have a parameterless constructor
var defaultValue = Activator.CreateInstance(value.GetType());
return value.Equals(defaultValue);
}
现在我们的整体代码,处理程序值类型将如下所示:
public static void CheckAllPropertiesAreNotDefault<T>(this T objectToInspect,
params Func<T, object>[] getters)
{
if (getters.Any(f => f(objectToInspect).IsDefault()))
Assert.Fail("some of the properties are not null");
}
要解决第二点,您可以传递Expression<Func<T, object>>[] getters
,其中包含有关被叫属性的信息。创建一个方法GetName
,它接受Expression<Func<T, object>>
并返回被叫属性名称
public static string GetName<T>(Expression<Func<T, object>> exp)
{
var body = exp.Body as MemberExpression;
//return type is an object, so type cast expression will be added to value types
if (body == null)
{
var ubody = (UnaryExpression)exp.Body;
body = ubody.Operand as MemberExpression;
}
return body.Member.Name;
}
现在生成的代码如下:
public static void CheckAllPropertiesAreNotDefault<T>(this T objectToInspect,
params Expression<Func<T, object>>[] getters)
{
var defaultProperties = getters.Where(f => f.Compile()(objectToInspect).IsDefault());
if (defaultProperties.Any())
{
var commaSeparatedPropertiesNames = string.Join(", ", defaultProperties.Select(GetName));
Assert.Fail("expected next properties not to have default values: " + commaSeparatedPropertiesNames);
}
}
现在给我打电话
myDto.CheckAllPropertiesAreNotDefault(x => x.Description,
x => x.Id);
我得到了
预计下一个属性不具有默认值:Description,Id
错误消息。在我的Dto中Description
是string
而Id
是值类型int
。如果我将该属性设置为某些非默认值,我将不会收到任何错误,测试将通过。
答案 1 :(得分:2)
我已经花了一些时间来解决这个问题。 @Dennis提出的解决方案由于多种原因而无法正常工作,这太糟糕了,因为它比以下解决方案更接近,更清晰。 Dennis方法不起作用的主要原因是ReferenceEqualityEquivalencyStep在应用断言规则之前处理空值。第二个原因是通过使用.When(info =&gt; true)我们删除了测试嵌套属性和数组元素的能力。解决这个问题的方法就像是.When(info =&gt;!info.RuntimeType.IsComplexType()&amp;&amp;!(info.RuntimeType属于IEnumerable类型)),但这只应该在值为测试不是空的。问题是ISubjecInfo不允许访问当前主题,因此虽然等效步骤在决定它是否可以处理时可以访问主题,但断言规则却没有。
无论如何,这是我解决问题的方法。我很可能没有想到一切。
namespace FluentAssertions
{
public class SimpleIsNotDefaultEquivalencyStep : IEquivalencyStep
{
public bool CanHandle(EquivalencyValidationContext context, IEquivalencyAssertionOptions config)
{
return true;
}
public virtual bool Handle(EquivalencyValidationContext context, IEquivalencyValidator structuralEqualityValidator, IEquivalencyAssertionOptions config)
{
context.Subject.Should().NotBeDefault( context.Reason, context.ReasonArgs );
return true;
}
}
public static class FluentAssertionsDefaultnessExtensions
{
private static bool IsDefault( object value, bool orValueTypeDefault = false )
{
if( value == null )
{
return true;
}
Type t = value.GetType();
t = orValueTypeDefault ? Nullable.GetUnderlyingType( t ) ?? t : t;
if( t.IsValueType )
{
object defaultValue = Activator.CreateInstance( t );
return value.Equals( defaultValue );
}
else if( value is string )
{
return string.IsNullOrWhiteSpace( value as string );
}
return false;
}
private static bool IsDefaultOrValueTypeDefault( object value )
{
return IsDefault( value, orValueTypeDefault: true );
}
public static AndConstraint<ObjectAssertions> NotBeDefault( this ObjectAssertions assertions, string because = "", params object[] reasonArgs )
{
Execute.Assertion
.BecauseOf( because, reasonArgs )
.ForCondition( !IsDefault( assertions.Subject ) )
.FailWith( "Expected {context:object} to not be default{reason}, but found {0}.", assertions.Subject );
return new AndConstraint<ObjectAssertions>( assertions );
}
public static AndConstraint<StringAssertions> NotBeDefault( this StringAssertions assertions, string because = "", params object[] reasonArgs )
{
Execute.Assertion
.BecauseOf( because, reasonArgs )
.ForCondition( !IsDefault( assertions.Subject ) )
.FailWith( "Expected {context:object} to not be default{reason}, but found {0}.", assertions.Subject );
return new AndConstraint<StringAssertions>( assertions );
}
public static AndConstraint<Numeric.NumericAssertions<T>> NotBeDefault<T>( this Numeric.NumericAssertions<T> assertions, string because = "", params object[] reasonArgs ) where T : struct
{
Execute.Assertion
.BecauseOf( because, reasonArgs )
.ForCondition( !IsDefault( assertions.Subject ) )
.FailWith( "Expected {context:object} to not be default{reason}, but found {0}.", assertions.Subject );
return new AndConstraint<Numeric.NumericAssertions<T>>( assertions );
}
public static AndConstraint<BooleanAssertions> NotBeDefault( this BooleanAssertions assertions, string because = "", params object[] reasonArgs )
{
Execute.Assertion
.BecauseOf( because, reasonArgs )
.ForCondition( !IsDefault( assertions.Subject ) )
.FailWith( "Expected {context:object} to not be default{reason}, but found {0}.", assertions.Subject );
return new AndConstraint<BooleanAssertions>( assertions );
}
public static AndConstraint<GuidAssertions> NotBeDefault( this GuidAssertions assertions, string because = "", params object[] reasonArgs )
{
Execute.Assertion
.BecauseOf( because, reasonArgs )
.ForCondition( !IsDefault( assertions.Subject ) )
.FailWith( "Expected {context:object} to not be default{reason}, but found {0}.", assertions.Subject );
return new AndConstraint<GuidAssertions>( assertions );
}
public static void ShouldNotBeEquivalentToDefault<T>( this T subject, string because = "", params object[] reasonArgs )
{
ShouldNotBeEquivalentToDefault( subject, config => config, because, reasonArgs );
}
public static void ShouldNotBeEquivalentToDefault<T>( this T subject,
Func<EquivalencyAssertionOptions<T>, EquivalencyAssertionOptions<T>> config, string because = "", params object[] reasonArgs )
{
var context = new EquivalencyValidationContext
{
Subject = subject,
Expectation = subject,
CompileTimeType = typeof( T ),
Reason = because,
ReasonArgs = reasonArgs
};
var validator = new EquivalencyValidator(
config( EquivalencyAssertionOptions<T>.Default()
.Using<string>( ctx => ctx.Subject.Should().NotBeDefault() ).WhenTypeIs<string>() )
.WithStrictOrdering()
);
validator.Steps.Remove( validator.Steps.Single( _ => typeof( TryConversionEquivalencyStep ) == _.GetType() ) );
validator.Steps.Remove( validator.Steps.Single( _ => typeof( ReferenceEqualityEquivalencyStep ) == _.GetType() ) );
validator.Steps.Remove( validator.Steps.Single( _ => typeof( SimpleEqualityEquivalencyStep ) == _.GetType() ) );
validator.Steps.Add( new SimpleIsNotDefaultEquivalencyStep() );
validator.AssertEquality( context );
}
}
}
以下是对它的测试:
[TestMethod]
[TestCategory( TestCategory2 )]
public void Test_NotBeDefault()
{
((Action)(() => ((int?)null).Should().NotBeDefault())).ShouldThrow<AssertFailedException>( "because null is default for int?" );
((Action)(() => ((int?)0).Should().NotBeDefault())).ShouldThrow<AssertFailedException>( "because 0 is value type default for int?" );
((Action)(() => 0.Should().NotBeDefault())).ShouldThrow<AssertFailedException>( "because null is value type default for int" );
((Action)(() => ((int?)1).Should().NotBeDefault())).ShouldNotThrow( "because 1 is not default for int?" );
((Action)(() => 1.Should().NotBeDefault())).ShouldNotThrow( "because 1 is not default for int" );
((Action)(() => ((object)null).Should().NotBeDefault())).ShouldThrow<AssertFailedException>( "because null is default for object" );
((Action)(() => ((object)new object()).Should().NotBeDefault())).ShouldNotThrow( "because not null is not default for object" );
((Action)(() => ((string)null).Should().NotBeDefault())).ShouldThrow<AssertFailedException>( "because null is default for string" );
((Action)(() => ((string)"").Should().NotBeDefault())).ShouldThrow<AssertFailedException>( "because empty string is default for string" );
((Action)(() => ((string)"hi").Should().NotBeDefault())).ShouldNotThrow( "because \"hi\" is not default for string" );
((Action)(() => ((bool?)null).Should().NotBeDefault())).ShouldThrow<AssertFailedException>( "because null is default for bool?" );
((Action)(() => ((bool?)false).Should().NotBeDefault())).ShouldThrow<AssertFailedException>( "because false is default for bool?" );
((Action)(() => false.Should().NotBeDefault())).ShouldThrow<AssertFailedException>( "because false is default for bool" );
((Action)(() => ((bool?)true).Should().NotBeDefault())).ShouldNotThrow( "because true is not default for bool?" );
((Action)(() => true.Should().NotBeDefault())).ShouldNotThrow( "because true is not default for bool" );
var actual = new
{
i1 = (int?)null,
i2 = (int?)0,
i3 = 0,
i4 = (int?)1,
i5 = 1,
s1 = (string)null,
s2 = (string)"",
s3 = (string)"hi",
b1 = (bool?)null,
b2 = (bool?)false,
b3 = false,
b4 = (bool?)true,
b5 = true,
n1 = (PlainClass)null,
n2 = new PlainClass(),
n3 = new PlainClass
{
Key = 10,
NestedProperty = new object()
},
a1 = (PlainClass[])null,
a2 = new [] { "", "hi", null },
a3 = new [] { 0, 11 },
a4 = new [] { new PlainClass { Key = 42 } },
g1 = (Guid?)null,
g2 = (Guid)Guid.Empty,
g3 = Guid.NewGuid()
};
((Action)(() => actual.ShouldNotBeEquivalentToDefault())).ShouldThrow<AssertFailedException>().WithMessage(
@"Expected property i1 to not be default, but found <null>.
Expected property i2 to not be default, but found 0.
Expected property i3 to not be default, but found 0.
Expected property s1 to not be default, but found <null>.
Expected property s2 to not be default, but found """".
Expected property b1 to not be default, but found <null>.
Expected property b2 to not be default, but found False.
Expected property b3 to not be default, but found False.
Expected property n1 to not be default, but found <null>.
Expected property n2.Key to not be default, but found 0.
Expected property n2.NestedProperty to not be default, but found <null>.
Expected property a1 to not be default, but found <null>.
Expected property a2[0] to not be default, but found """".
Expected property a2[2] to not be default, but found <null>.
Expected property a3[0] to not be default, but found 0.
Expected property a4[0].NestedProperty to not be default, but found <null>.
Expected property g1 to not be default, but found <null>.
Expected property g2 to not be default, but found {00000000-0000-0000-0000-000000000000}.
With configuration:
- Select all declared properties
- Match property by name (or throw)
- Invoke Action<String> when info.RuntimeType.IsSameOrInherits(System.String)
- Invoke Action<String> when info.RuntimeType.IsSameOrInherits(System.String)
" );
}
答案 2 :(得分:0)
您可以使用`ShouldBeEquivalentTo'API在所有类型的属性上添加覆盖,如下所示:
mapped.ShouldBeEquivalentTo(mapped, options => options
.ExcludingNestedObjects()
.Using<object>(ctx => ctx.Subject.Should().NotBeNull())
.When(info => true));
然后当谓词确定哪些属性应该使用此自定义断言。
答案 3 :(得分:0)
尝试一下:
var mySelectedProperties = new string[] { "Description", "Id", "other" };
Assert.IsTrue(mapped.GetType().GetProperties().ToList().
Where(p => mySelectedProperties.Contains(p.Name)).
Where(p => p.GetValue(mapped) == null).
Count() == 0);