如何对自定义数据注释执行单元测试

时间:2012-05-04 14:33:58

标签: c# unit-testing data-annotations

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

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;
    }
}

我不知道这个班级的正确单元测试怎么样。

感谢。

2 个答案:

答案 0 :(得分: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.");
    }
}

答案 1 :(得分:1)

在考虑如何“正确”测试此课程时,请考虑以下事项:

  • IsValid的{​​{3}}(CC)为5。
  • 该方法依赖于另外两种方法IsNullOrWhiteSpaceLengthBetween。我相信这两个都有一个额外的CC 2.
  • 有可能抛出InvalidCastException。这代表了另一个潜在的测试用例

总的来说,您可能需要测试8个案例。使用cyclomatic complexityxUnit.net *(您也可以在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();
    }
}

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