有没有一种测试类似属性的方法?

3

假设我有一个类具有一些相似的属性:

public string First { get; set; }
public string Second { get; set; }
public string Third { get; set; }

我希望在我的测试中以相同的方式来测试它们... 因此我写了以下代码:

[Test]
public void TestFirst()
{
    // Asserting strings here
}

有没有一种方法可以避免创建三个测试(一个用于First,一个用于Second和一个用于Third)?
我正在寻找类似[Values(First,Second,Third)]的东西,这样我就可以编写一个测试来迭代这些属性。
谢谢!

如果您有三个(或更多)属性都表现出完全相同的行为,那么这会让我怀疑类设计是否正确。 - Damien_The_Unbeliever
可能会同意,但是你得到的代码并不总是可以随心所欲地更改 :) - Noctis
7个回答

2
这个怎么样:
[TestFixture]
public class Tests
{
    [Test]
    public void Test()
    {
        var obj = new MyClass();

        obj.First = "some value";
        obj.Second = "some value";
        obj.Third = "some value";

        AssertPropertyValues(obj, "some value", x => x.First, x => x.Second, x => x.Third);
    }

    private void AssertPropertyValues<T, TProp>(T obj, TProp expectedValue, params Func<T, TProp>[] properties)
    {
        foreach (var property in properties)
        {
            TProp actualValue = property(obj);
            Assert.AreEqual(expectedValue, actualValue);
        }
    }
}

使用这种方法,如果其中一个属性出现问题,您将收到消息:期望值为“某些值”,但实际值为“另一个值” - 没有属性名称。类似的问题是:如果两个或更多的属性包含意外的值,则您只会收到关于第一个属性的消息。 - Akim

1

您应该能够使用表达式树来实现此目的。使用MSDN文档中的Expression.Property方法,我已经创建了以下辅助方法,用于从任意对象obj中获取名为propertyName的类型T属性:

public T InvokePropertyExpression<T>(object obj, string propertyName)
{
    return Expression.Lambda<Func<T>>(Expression.Property(
               Expression.Constant(obj), propertyName)).Compile()();
}

使用这个辅助方法在我的单元测试中,我现在可以根据属性名称访问相关属性,例如像这样:
[Test, Sequential]
public void Tests([Values("First", "Second", "Third")] string propertyName,
                  [Values("hello", "again", "you")] string expected)
{
    var obj = new SomeClass 
        { First = "hello", Second = "again", Third = "you" };
    var actual = InvokePropertyExpression<string>(obj, propertyName);
    Assert.AreEqual(expected, actual);
}

1
嗯...这看起来很有趣。明天早上我会试一下并在这里更新。 (与此同时,我可以去了解一下表达式树... :)) - Noctis
不错的例子,但使用NUnit.Framework.Constraints.Constraint构建约束条件有更简单的方法。 - Akim

1

使用NUnit.Framework.Constraints.Constraint有很多表达这种断言的可能性。此外,您可以使用ValuesSoueceAttributeTestCaseSourceAttribute来描述测试的更多输入,而不是使用ValuesAttributeTestCaseAttribute

描述测试输入

让我们使用TestCaseSourceAttribute定义预期属性名称及其值

public IEnumerable TestCasesSourcesAllProperties
{
    get
    {
        yield return new TestCaseData(
            new Tuple<string, string>[] { 
                Tuple.Create("First", "foo"), 
                Tuple.Create("Second", "bar"), 
                Tuple.Create("Third", "boo") } as object)
                    .SetDescription("Test all properties using Constraint expression");
    }
}

在单个测试中构建约束

现在我们可以在单个测试中为所有三个属性构建约束。

// get test parameters from TestCasesSourcesAllProperties
[TestCaseSource("TestCasesSourcesAllProperties")]
public void ClassUnderTest_CheckAllProperty_ExpectValues(Tuple<string, string>[] propertiesNamesWithValues)
{
    // Arrange
    ClassUnderTest cut = null;

    // Act: perform actual test, here is only assignment
    cut = new ClassUnderTest { First =  "foo", Second = "bar",  Third  = "boo" };

    // Assert
    // check that class-under-test is not null
    NUnit.Framework.Constraints.Constraint expression = Is.Not.Null;

    foreach(var property in propertiesNamesWithValues)
    {
        // add constraint for every property one by one
        expression = expression.And.Property(property.Item1).EqualTo(property.Item2);
    }

    Assert.That(cut, expression);
}

这里是一个完整的例子

缺点

条件逻辑,即测试逻辑内部的foreach


1

这很容易做到,但我质疑它是否值得。

如何操作 - 上面的许多答案都可以,但是假设您正在测试一个新创建的对象,这似乎是最简单的方法...

[TestCase("First", "foo"]
[TestCase("Second", 42]
[TestCase("Third", 3.14]
public void MyTest(string name, object expected)
{
    Assert.That(new MyClass(), Has.Property(name).EqualTo(expected));
}

然而,在测试中放置三个单独的断言似乎更容易阅读...
[Test]
public void MyTest()
{
    var testObject = new MyClass();
    Assert.That(testObject, Has.Property("First").EqualTo("foo"));
    Assert.That(testObject, Has.Property("Second").EqualTo(42));
    Assert.That(testObject, Has.Property("Third").EqualTo(3.14));
}

当然,这假设三个断言都是测试同一件事情的一部分,比如DefaultConstructorInitializesMyClassCorrectly。如果你测试的不是这个,那么三个测试就更有意义了,尽管需要打字更多。确保的一种方法是看看你是否能为测试想出一个合理的名称。
查理

很棒的答案,我之前不知道NUnit中有这个功能 - Anders Gustafsson
就像我得到的额外信息一样 :) 我还在Nunit论坛上回复了您的评论。干杯 - Noctis

0

您可以在测试方法的参数上使用Values属性:

[Test]
public void MyTest([Values("A","B")] string s)
{
    ...
}

然而,这只适用于字符串常量(即不是属性值)。

我猜可以使用反射从给定值中获取属性的值,例如:

[Test]
public void MyTest([Values("A","B")] string propName)
{
    var myClass = new MyClass();
    var value = myClass.GetType().GetProperty(propName).GetValue(myClass, null);

    // test value
}

但这不是最干净的解决方案。也许你可以编写一个测试,调用一个方法来测试每个属性。

[Test]
public void MyTest()
{
    var myClass = new MyClass();
    MyPropertyTest(myClass.First);
    MyPropertyTest(myClass.Second);
    MyPropertyTest(myClass.Third);
}

public void MyPropertyTest(string value)
{
    // Assert on string
}

然而,最好避免使用这种测试方式,因为单元测试应该只测试您代码的一个单元。如果每个测试都命名正确,它可以用来记录您期望的内容,并且可以在将来轻松地添加。


但我无法使用它们来测试字符串... 所以,根据你的例子:some_object.s = "some string"; assert.areequal("some string", some_object.s); - Noctis
OP在他们的问题中提到了“Values”。我认为我们可以假设他们熟悉它。 - Damien_The_Unbeliever
@Noctis - 不确定你的意思是什么。我已经更新了答案,加入了几个选项。看看它们是否有帮助。 - g t
我同意你的看法,它并不是最干净的代码......目前我无法访问代码。明天我会测试一下。谢谢。 - Noctis

0

你可以编写带有参数的测试,并将属性访问器作为参数传递:

看一个例子: 假设你的类有3个属性:

public class MyClass
{
    public string First { get; set; }
    public string Second { get; set; }
    public string Third { get; set; }
}

那么测试可能如下所示:

[TestFixture]
public class MyTest
{
    private TestCaseData[] propertyCases = new[]
        {
            new TestCaseData(
                "First",
                (Func<MyClass, string>) (obj => obj.First),
                (Action<MyClass, string>) ((obj, newVal) => obj.First = newVal)),

            new TestCaseData(
                "Second",
                (Func<MyClass, string>) (obj => obj.Second),
                (Action<MyClass, string>) ((obj, newVal) => obj.Second = newVal)),

            new TestCaseData(
                "Third",
                (Func<MyClass, string>) (obj => obj.Third),
                (Action<MyClass, string>) ((obj, newVal) => obj.Third = newVal))
        };

    [Test]
    [TestCaseSource("propertyCases")]
    public void Test(string description, Func<MyClass, string> getter, Action<MyClass, string> setter)
    {
        var obj = new MyClass();
        setter(obj, "42");

        var actual = getter(obj);

        Assert.That(actual, Is.EqualTo("42"));
    }
}

一些注意事项:
1. 未使用的字符串描述作为第一个参数传递,以区分当它们通过NUnit测试运行器UI或通过Resharper运行时的测试用例。
2. 测试用例是独立的,即使First属性的测试失败,其他两个测试也将被运行。
3. 可以通过NUnit测试运行器UI或通过Resharper仅运行一个测试用例。
所以,您的测试是干净且DRY的 :)

0

感谢大家的答案和帮助。我学到了很多东西。

这是我最终采取的做法。我使用反射来获取所有字符串属性,然后设置为一个值,检查该值是否已设置,将其设置为null,检查它是否返回空字符串(在属性的getter中进行逻辑判断)。

[Test]
public void Test_AllStringProperties()
{
    // Linq query to get a list containing all string properties
    var string_props= (from prop in bkvm.GetType()
                            .GetProperties(BindingFlags.Public | BindingFlags.Instance)
                      where
                        prop.PropertyType == typeof(string) &&
                        prop.CanWrite && prop.CanRead
                      select prop).ToList();

    string_props.ForEach(p =>{
                                 // Set value of property to a different string
                                 string set_val = string.Format("Setting [{0}] to: \"Testing string\".", p.Name);
                                 p.SetValue(bkvm, "Testing string", null);
                                 Debug.WriteLine(set_val);
                                 // Assert it was set correctly
                                 Assert.AreEqual("Testing string", p.GetValue(bkvm, null));

                                 // Set property to null
                                 p.SetValue(bkvm,null,null);
                                 set_val = string.Format("Setting [{0}] to null. Should yield an empty string.", p.Name);
                                 Debug.WriteLine(set_val);
                                 // Assert it returns an empty string.
                                 Assert.AreEqual(string.Empty,p.GetValue(bkvm, null));
                             }
        );
}

这样我就不需要担心有人添加属性了,因为它将自动进行检查,而我不需要更新测试代码(正如您所猜测的,不是每个人都更新或编写测试):)

对此解决方案的任何意见都将受到欢迎。


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