FluentAssertions Should().BeEquivalentTo无法比较EF动态代理的运行时派生类型。

3

我正在使用FluentAssertions来比较两个对象,其中一个对象是EF动态代理,使用Should().BeEquivalentTo()。然而,似乎在5.0.0版本中将ShouldBeEquivalentToShouldAllBeEquivalentTo合并(#593)后,在使用RespectingRuntimeTypes时功能已经被破坏了。除非我为对象图中的每个类型显式添加ComparingByMembers,否则不再比较所声明类型的派生类型的属性成员。是否有其他设置可以解决这个问题?


1
这些类型是否覆盖了“Equals”? - Dennis Doomen
@DennisDoomen 不。 - Neo
1
你读过这个吗?http://www.continuousimprover.com/2018/02/fluent-assertions-50-best-unit-test.html?m=1 - Dennis Doomen
1
@DennisDoomen 我确实阅读了“迈向统一API”,然后直接跳到“升级提示”(将其视为“TLDR”部分),期望这将突出任何破坏性更改。今天早上我发现基类确实错误地实现了Equals,删除它解决了问题。感谢您的回复,但我可以建议您在“升级提示”部分(在“更改BeEquivalentTo”下)添加一个附加的项目符号,以确保任何Equals实现符合它应该遵循的引用类型值语义原则吗? - Neo
好主意。已将此添加到原始博客中。 - Dennis Doomen
我也遇到了这个问题。与@Neo不同的是,层次结构中没有一个类实现equals方法,所以这对我来说不是问题。使用IncludingAllRuntimePropertiesRespectingRuntimeTypes解决了我的问题。 - Samuel
2个回答

2

我写了下面这个扩展方法,试图解决这个问题,但是似乎只是为了在动态代理的运行时修复派生类型的问题而显得很麻烦:

public static class FluentAssertionsExtensions
{
    /// <summary>
    /// Extends the functionality of <see cref="EquivalencyAssertionOptions{TExpectation}" />.ComparingByMembers by recursing into the entire object graph
    /// of the T or passed object and marks all property reference types as types that should be compared by its members even though it may override the
    /// System.Object.Equals(System.Object) method. T should be used in conjunction with RespectingDeclaredTypes. The passed object should be used in
    /// conjunction with RespectingRuntimeTypes.
    /// </summary>
    public static EquivalencyAssertionOptions<T> ComparingByMembersRecursive<T>(this EquivalencyAssertionOptions<T> options, object obj = null)
    {
        var handledTypes = new HashSet<Type>();
        var items = new Stack<(object obj, Type type)>(new[] { (obj, obj?.GetType() ?? typeof(T)) });

        while (items.Any())
        {
            (object obj, Type type) item = items.Pop();
            Type type = item.obj?.GetType() ?? item.type;

            if (!handledTypes.Contains(type))
            {
                handledTypes.Add(type);

                foreach (PropertyInfo pi in type.GetProperties())
                {
                    object nextObject = item.obj != null ? pi.GetValue(item.obj) : null;
                    Type nextType = nextObject?.GetType() ?? pi.PropertyType;

                    // Skip string as it is essentially an array of chars, and needn't be processed.
                    if (nextType != typeof(string))
                    {
                        if (nextType.GetInterface(nameof(IEnumerable)) != null)
                        {
                            nextType = nextType.HasElementType ? nextType.GetElementType() : nextType.GetGenericArguments().First();

                            if (nextObject != null)
                            {
                                // Look at all objects in a collection in case any derive from the collection element type.
                                foreach (object enumObj in (IEnumerable)nextObject)
                                {
                                    items.Push((enumObj, nextType));
                                }

                                continue;
                            }
                        }

                        items.Push((nextObject, nextType));
                    }
                }

                if (type.IsClass && type != typeof(string))
                {
                    // ReSharper disable once PossibleNullReferenceException
                    options = (EquivalencyAssertionOptions<T>)options
                        .GetType()
                        .GetMethod(nameof(EquivalencyAssertionOptions<T>.ComparingByMembers))
                        .MakeGenericMethod(type).Invoke(options, null);
                }
            }
        }

        return options;
    }
}

这应该像这样被调用:

foo.Should().BeEquivalentTo(bar, o => o
    .RespectingRuntimeTypes()
    .ComparingByMembersRecursive(foo)
    .ExcludingMissingMembers());

1

我最近在使用Microsoft.EntityFrameworkCore.Proxies时遇到了同样的问题。在我的情况下,我必须比较持久属性并忽略比较其他导航属性。

解决方案是实现接口FluentAssertions.Equivalency.IMemberSelectionRule来排除不必要的属性。

public class PersistentPropertiesSelectionRule<TEntity> : IMemberSelectionRule 
    where TEntity : class
{
    public PersistentPropertiesSelectionRule(DbContext dbContext) => 
        this.dbContext = dbContext;

    public bool IncludesMembers => false;

    public IEnumerable<SelectedMemberInfo> SelectMembers(
        IEnumerable<SelectedMemberInfo> selectedMembers, 
        IMemberInfo context, 
        IEquivalencyAssertionOptions config)
    {
        var dbPropertyNames = dbContext.Model
            .FindEntityType(typeof(TEntity))
            .GetProperties()
            .Select(p => p.Name)
            .ToArray();

        return selectedMembers.Where(x => dbPropertyNames.Contains(x.Name));
    }

    public override string ToString() => "Include only persistent properties";

    readonly DbContext dbContext;
}

然后编写一个扩展方法可以帮助方便使用,也可以提高可读性。扩展方法可以是以下代码段中的内容。

public static class FluentAssertionExtensions
{
    public static EquivalencyAssertionOptions<TEntity> IncludingPersistentProperties<TEntity>(this EquivalencyAssertionOptions<TEntity> options, DbContext dbContext) 
        where TEntity : class
    {
        return options.Using(new PersistentPropertiesSelectionRule<TEntity>(dbContext));
    }
}

最后你可以像下面这段代码一样在测试中调用扩展方法。

// Assert something
using (var context = DbContextFactory.Create())
{
    var myEntitySet = context.MyEntities.ToArray();
    myEntitySet.Should().BeEquivalentTo(expectedEntities, options => options
        .IncludingPersistentProperties(context)
        .Excluding(r => r.MyPrimaryKey));
}

这个实现解决了我的问题,代码看起来整洁清晰。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接