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

C# 如何对自定义数据注释执行单元测试,c#,unit-testing,data-annotations,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(obje

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

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;
    }
}
公共类PhoneAreaAttribute:ValidationAttribute,IClientValidable
{
公用常量字符串ValidInitNumber=“0”;
公共常数int MinLen=2;
公共常数int MaxLen=4;
公共覆盖布尔值有效(对象值)
{
变量区域=(字符串)值;
if(string.IsNullOrWhiteSpace(区域))
{
返回true;
}
if(area.StartWith(PhoneAreaAttribute.ValidInitNumber))
{
返回false;
}
如果(!Regex.IsMatch(区域,@“^[\d]+$”)
{
返回false;
}
如果(!area.LengthBetween(PhoneAreaAttribute.MinLen,PhoneAreaAttribute.MaxLen))
{
返回false;
}
返回true;
}
公共IEnumerable GetClientValidationRules(ModelMetadata元数据、ControllerContext上下文)
{
var规则=新ModelClientValidationRule
{
ErrorMessage=FormatErrorMessage(metadata.GetDisplayName()),
ValidationType=“phoneArea”,
};
收益率-收益率规则;
}
}
我不知道这将是一个正确的单元测试这个类


谢谢。

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

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”的路径(这是第二个场景/测试方法)

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

以下是
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.");
    }
}

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

  • 的(CC)是有效的
    是5
  • 该方法依赖于另外两种方法
    IsNullOrWhiteSpace
    LengthBetween
    。我相信这两个都有一个额外的CC 2
  • 有可能抛出一个
    InvalidCastException
    。这代表了另一个潜在的测试用例
总的来说,您可能需要测试8个案例。使用and*(您也可以在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();
    }
}
公共类PhoneAreaAttributeTests
{
[理论]
[在线数据(“123”,真实)]
[InlineData(“,true)]
[InlineData(null,true)]
public void有效,输入正确返回(
对象值(预期为布尔值)
{
//设置
var phoneAreaAttribute=CreatePhoneAreaAttribute();
//练习
var-actual=phoneAreaAttribute.IsValid(值);
//核实
actual.Should().Be(应为“{0}应为有效输入”,值);
//拆卸
}
[理论]
[InlineData(“012”,假)]
[在线数据(“A12”,假)]
[在线数据(“1”,假)]
[在线数据(“12345”,假)]
public void有效,输入不正确,返回值无效(
对象值(预期为布尔值)
{
//设置
var phoneAreaAttribute=CreatePhoneAreaAttribute();
//练习
var-actual=phoneAreaAttribute.IsValid(值);
//核实
actual.Should().Be(应为“{0}应为无效输入”值);
//拆卸
}
[事实]
public void有效\u使用非字符串输入\u throwException()
{
//设置
var phoneAreaAttribute=CreatePhoneAreaAttribute();
常量int输入=123;
//练习
//核实
断言。抛出(
()=>phoneAreaAttribute.IsValid(输入));
//拆卸
}
//简单的工厂方法,如果我们改变
//构造函数,我们不必改变所有的
//依赖于此对象的测试。
公共PhoneAreaAttribute CreatePhoneAreaAttribute()
{
返回新的PhoneAreaAttribute();
}
}

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

像对其他类进行单元测试一样对其进行单元测试?或者你还有其他问题吗?@Styxxy我只需要为这个类创建一个“单元测试”。我个人总是为每个类创建一个单独的单元测试。如果没有单元测试