我一直在加速解决这个问题。@Dennis 提出的解决方案由于多种原因无法正常工作,这太糟糕了,因为它比以下解决方法如此接近且干净得多。Dennis 方法不起作用的主要原因是 ReferenceEqualityEquivalencyStep 在应用断言规则之前处理空值。第二个原因是通过使用 .When( info => true ) 我们删除了测试嵌套属性和数组元素的能力。解决此问题的方法类似于 .When( info => !info.RuntimeType.IsComplexType() && !(info.RuntimeType is of type IEnumerable) ),但仅当被测试的值不为 null 时才适用. 问题是 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)
" );
}