如何为自定义数据注释编写单元测试

4
我有以下简单的数据注释类来控制电话号码的区域:
```

我有以下简单的数据注释类来控制电话号码的区域:

```
public class PhoneAreaAttribute : ValidationAttribute, IClientValidatable
{
    public const string ValidInitNumber = "0";
    public const int MinLen = 2;
    public const int MaxLen = 4;

    public override bool IsValid(object value)
    {
        var area = (string)value;
        if (string.IsNullOrWhiteSpace(area))
        {
            return true;
        }

        if (area.StartsWith(PhoneAreaAttribute.ValidInitNumber))
        {
            return false;
        }

        if (!Regex.IsMatch(area, @"^[\d]+$"))
        {
            return false;
        }

        if (!area.LengthBetween(PhoneAreaAttribute.MinLen, PhoneAreaAttribute.MaxLen))
        {
            return false;
        }

        return true;
    }

    public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
    {
        var rule = new ModelClientValidationRule
        {
            ErrorMessage = FormatErrorMessage(metadata.GetDisplayName()),
            ValidationType = "phoneArea",
        };

        yield return rule;
    }
}

我不知道如何为这个类编写正确的单元测试。
谢谢。

你会像对待其他类一样对待它进行单元测试吗?或者你还有其他困难吗? - Styxxy
我个人总是为每个类创建一个单独的单元测试。如果您还没有单元测试项目,那么您应该创建一个!然后,您为这个类创建一个新的单元测试,并编写您的TestMethod等内容。 - Styxxy
@Styxxy,我有一个需要“单元测试”的项目,并且需要覆盖尽可能多的代码进行测试。在这种情况下,我需要为这个类创建一个“单元测试”。 - andres descalzo
没错,你只需要像其他类一样为它创建一个测试。还是不明白你在纠结什么? - Styxxy
让我们在聊天中继续这个讨论:http://chat.stackoverflow.com/rooms/10894/discussion-between-andres-descalzo-and-styxxy - andres descalzo
显示剩余3条评论
2个回答

3

好的,基本上测试属性与测试任何常规类相同。我将您的类简化了一下,以便我可以运行它(您创建了一些扩展方法,我不想重新创建)。下面是这个类的定义。

public class PhoneAreaAttribute : ValidationAttribute
{
    public const string ValidInitNumber = "0";

    public override bool IsValid(object value)
    {
        var area = (string)value;

        if (string.IsNullOrEmpty(area))
        {
            return true;
        }

        if (area.StartsWith(PhoneAreaAttribute.ValidInitNumber))
        {
            return false;
        }

        return true;
    }
}

提前说明:我的单元测试命名约定可能与您使用的不同(有几个约定)。

现在我们将创建一个单元测试。我了解到您已经有一个测试项目,如果您没有,请创建一个。在这个测试项目中,您将创建一个新的单元测试(基本单元测试),让我们把它命名为PhoneAreaAttributeTest

作为良好的实践,我创建了一个测试初始化器来创建所有共享的“资源”,在这种情况下是PhoneAreaAttribute类的一个新实例。是的,您可以像使用“常规”类一样创建一个实例(事实上,“常规”类和属性类之间并没有太大的区别)。

现在我们准备开始编写方法的测试。基本上,您将希望处理所有可能的情况。我将在这里展示两种可能出现在我的(简化后的)IsValid方法中的情况。首先,我将查看给定对象参数是否可以转换为字符串(这是第一个场景/TestMethod)。其次,我将查看“IsNullOrEmpty”的路径是否被正确处理(这是第二个场景/TestMethod)。

正如您所看到的,这只是一个常规的单元测试。这些只是基础知识。如果您仍有疑问,我还建议您阅读一些教程。

这是PhoneAreaAttributeTest测试类:

[TestClass]
public class PhoneAreaAttributeTest
{
    public PhoneAreaAttribute PhoneAreaAttribute { get; set; }

    [TestInitialize]
    public void PhoneAreaAttributeTest_TestInitialise()
    {
        PhoneAreaAttribute = new PhoneAreaAttribute();
    }


    [TestMethod]
    [ExpectedException(typeof(InvalidCastException))]
    public void PhoneAreaAttributeTest_IsValid_ThrowsInvalidCastException()
    {
        object objectToTest = new object();
        PhoneAreaAttribute.IsValid(objectToTest);
    }


    [TestMethod]
    public void PhoneAreaAttributeTest_IsValid_NullOrEmpty_True()
    {
        string nullToTest = null;
        string emptoToTest = string.Empty;

        var nullTestResult = PhoneAreaAttribute.IsValid(nullToTest);
        var emptyTestResult = PhoneAreaAttribute.IsValid(emptoToTest);

        Assert.IsTrue(nullTestResult, "Null Test should return true.");
        Assert.IsTrue(emptyTestResult, "Empty Test should return true.");
    }
}

如果我可以吹毛求疵的话,虽然在这个例子中看起来是无害的,但是针对单元测试的Setup/Teardown方法,比如TestInitialize,应该小心使用。参见**Why you should not use SetUp and TearDown in NUnit** 和 Setup and teardown are evil for a unit test ;) - Matt
没错。我只是用它来初始化一些在每个方法中都使用的默认值。例如,所有测试都需要的服务接口的模拟(但是你为每个测试创建的存根)。无论如何,你提供的链接都是有趣的文章 :). - Styxxy

1

在考虑如何“正确”测试这个类时,请考虑以下内容:

  • IsValid圈复杂度(CC)为5。
  • 该方法依赖于另外两个方法IsNullOrWhiteSpaceLengthBetween。我认为这两个方法各自有额外的CC 2。
  • 有可能抛出InvalidCastException。这代表着另一个潜在的测试用例。

总共,你有8种潜在的情况需要测试。使用xUnit.netFluent Assertions*(在NUnit中也可以执行类似操作),你可以编写以下单元测试来“正确”测试这个方法:

public class PhoneAreaAttributeTests
{
    [Theory]
    [InlineData("123", true)]
    [InlineData(" ", true)]
    [InlineData(null, true)]
    public void IsValid_WithCorrectInput_ReturnsTrue(
        object value, bool expected)
    {
        // Setup
        var phoneAreaAttribute = CreatePhoneAreaAttribute();

        // Exercise
        var actual = phoneAreaAttribute.IsValid(value);

        // Verify
        actual.Should().Be(expected, "{0} should be valid input", value);

        // Teardown            
    }

    [Theory]
    [InlineData("012", false)]
    [InlineData("A12", false)]
    [InlineData("1", false)]
    [InlineData("12345", false)]
    public void IsValid_WithIncorrectInput_ReturnsFalse(
        object value, bool expected)
    {
        // Setup
        var phoneAreaAttribute = CreatePhoneAreaAttribute();

        // Exercise
        var actual = phoneAreaAttribute.IsValid(value);

        // Verify
        actual.Should().Be(expected, "{0} should be invalid input", value);

        // Teardown      
    }

    [Fact]
    public void IsValid_UsingNonStringInput_ThrowsExcpetion()
    {
        // Setup
        var phoneAreaAttribute = CreatePhoneAreaAttribute();
        const int input = 123;

        // Exercise
        // Verify
        Assert.Throws<InvalidCastException>(
            () => phoneAreaAttribute.IsValid(input));

        // Teardown     
    }

    // Simple factory method so that if we change the
    // constructor, we don't have to change all our 
    // tests reliant on this object.
    public PhoneAreaAttribute CreatePhoneAreaAttribute()
    {
        return new PhoneAreaAttribute();
    }
}

*我喜欢使用流畅的断言,在这种情况下,它有助于我们指定一条消息,让我们知道何时断言失败,哪个是失败的断言。这些数据驱动的测试很好,因为它们可以通过将各种排列组合分组来减少我们需要编写的类似测试方法的数量。当我们这样做时,最好避免断言轮盘,通过自定义消息进行解释。顺便说一句,流畅的断言可以与许多测试框架一起使用。


参数必须是对象类型,因为它由接口定义 ;)。 - Styxxy
@Styxxy,嗯,我已经想到了这一点:)。我想知道为什么不设计成避免这种情况,也许使用泛型(即PhoneAreaAttribute:IClientValidatable<String>IsValid(String value))。这样可以确保类型安全。 - Matt
@Styxxy 啊,我查了一下,IClientValidatableASP.NET MVC验证的一部分。所以OP对此部分没有控制权。更新答案以反映这一点。 - Matt
而ValidationAttribute是System.ComponenModel.DataAnnotations的一部分,它有一个抽象方法IsValid(),您必须实现它(使用TS使用的签名,否则它不会是该方法的_override_或实现)。 - Styxxy
@Styxxy,我们的交流让我受到了教训。我应该更加关注原始代码(我也简化了它以便编写单元测试来验证我的答案)。很酷的是,我学到了一些有关ASP.NET MVC的新知识。 - Matt
太好了。我也从你的代码中学到了一些东西,所以这是双赢。我们现在都变得更聪明了,呵呵。 - Styxxy

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