测试集合不包含多个成员

Test collection does not contain several members

考虑这段代码:

const string user8 = "user8";
const string user9 = "user9";
string[] users = { "user1", "user2", "user3", "user4", user8 };

我想测试 users 不包含 user8 或 user9。我以前用

Assert.That(users, Is.Not.SupersetOf(new[] {user8, user9 }));

不幸的是,它通过了测试(这不是预期的)。我可以使用

Assert.That(users, Does.Not.Contains(user8).And.Not.Contains(user9));

但是如果我打算再次测试超过 2 个成员的集合,那将是有问题的。有没有更好的语法?我正在使用 NUnit 3.4。

注意:目标不仅仅是测试结果,还应该是正确的断言,这样每当测试失败时,我们都可以从错误消息中更快地确定。这是上一个示例中的错误消息示例(使用 Does.Not.Contains

"Expected: not collection containing "user8" 且不包含 "user9" 但是是:< "user1"、"user2"、"user3"、"user4"、"user8" >"

尝试使用 CollectionAssert.IsNotSubsetOf()

CollectionAssert.IsNotSubsetOf(new[] {user8Name, user9Name }), users);

更新:

好吧,您始终可以使用基本循环。

Array.ForEach(new[] { user8, user9 }, u => Assert.That(users, Has.No.Member(u)));

这将通过循环检查 users 是否包含 new[] { user8, user9 } 数组中的任何实例。

错误消息将是这样的:

Expected: not collection containing "user8" But was: < "user1", "user8", "user2", "user3" >

尝试检查针对用户的排除列表

const string user8 = "user8";
const string user9 = "user9";
string[] users = { "user1", "user2", "user3", "user4", user8 };

Assert.Multiple(() => {
    var exclude = new[] { user8, user9 }; 
    foreach(var user in exclude) {
        Assert.That(users, Has.No.Member(user));
    }
}

此测试应该会失败,因为用户列表确实包含 user8

NUnit 文档 - CollectionContainsConstraint

在深入了解创建自定义约束并下载 NUnit 源代码之后,我决定创建自定义 CollectionContainsConstraint

/// <summary>
/// CollectionContainsAnyConstraint is used to test whether a collection
/// contains any member in expected collection.
/// </summary>
/// <typeparam name="T"></typeparam>
public class CollectionContainsAnyConstraint<T> : CollectionContainsConstraint
{
    public CollectionContainsAnyConstraint(IEnumerable<T> expected) : base(expected)
    {
    }

    public override string Description
        => Regex.Replace(base.Description, @"^\s*collection containing", "collection containing any of");

    /// <summary>
    /// Test whether any member in expected collection is available in actual collection
    /// </summary>
    /// <param name="actual">Actual collection</param>
    /// <returns></returns>
    protected override bool Matches(IEnumerable actual)
    {
        var convertedExpected = (IEnumerable<T>)Expected;
        var convertedActual = EnsureHasSameGenericType(actual, typeof(T));
        return convertedActual.Any(x => convertedExpected.Contains(x));
    }

    private IEnumerable<T> EnsureHasSameGenericType(IEnumerable actual, Type expectedType)
    {
        var sourceType = actual.GetType();
        var sourceGeneric = sourceType.IsArray
            ? sourceType.GetElementType()
            : sourceType.GetGenericArguments().FirstOrDefault();
        if (sourceGeneric == null)
            throw new ArgumentException("The actual collection must contain valid generic argument");
        if (!sourceGeneric.Equals(expectedType))
            throw new ArgumentException($"The actual is collection of {sourceGeneric.Name} but the expected is collection of {expectedType.Name}");
        return (IEnumerable<T>)actual;
    }
}

public static class ConstraintExtensions
{
    /// <summary>
    /// Returns a new <see cref="CollectionContainsAnyConstraint{T}"/> checking for the
    /// presence of any object of the <see cref="expected"/> collection against actual collection.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="expression"></param>
    /// <param name="expected">A collection where one of its member is available in actual collection</param>
    /// <returns></returns>
    public static CollectionContainsAnyConstraint<T> ContainsAny<T>(this ConstraintExpression expression,
        params T[] expected)
    {
        var constraint = new CollectionContainsAnyConstraint<T>(expected);
        expression.Append(constraint);
        return constraint;
    }
}

用法如下所示

[TestFixture]
class GivenCustomCollectionContainsTest
{
    const string User8 = "user8";
    const string User9 = "user9";
    private readonly List<string> users = new List<string> { "user1", "user2", "user3", "user4", "user5" };

    [Test]
    public void WhenActualContainsOneOfExpectedAndPreceededByNotOperatorThenItShouldFail()
    {
        var actual = users.ToList();
        actual.Add(User8);

        var assert = Assert.Throws<AssertionException>(() => Assert.That(actual, Does.Not.ContainsAny(User8, User9)));
        Assert.That(assert.ResultState.Status, Is.EqualTo(TestStatus.Failed));
        Assert.That(assert.Message, Does.Contain("not collection containing any of").And.Contain($"\"{User8}\""));
    }

    [Test]
    public void WhenActualContainsAllOfExpectedAndPreceededByNotOperatorThenItShouldFail()
    {
        var actual = users.ToList();
        actual.Add(User8);
        actual.Add(User9);

        var assert = Assert.Throws<AssertionException>(() => Assert.That(actual, Does.Not.ContainsAny(User8, User9)));
        Assert.That(assert.ResultState.Status, Is.EqualTo(TestStatus.Failed));
        Assert.That(assert.Message, Does.Contain("not collection containing any of").And.Contain($"\"{User8}\", \"{User9}\""));
    }
}

请注意,这仅用于负面测试。幸运的是,我现在不需要 "any member of a collection is available in another collection" 的测试。大多数情况下,它测试一个集合是另一个集合的一部分,因此,我可以使用 Is.SupersetOf.