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

我不知道这将如何成为此类的正确单元测试。

谢谢。

4

2 回答 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;
    }
}

事先注意:我的一些单元测试命名约定可能与您使用的不同(那里有一些)。

现在我们将创建一个单元测试。我知道您已经有一个Test Project,如果您没有,只需创建一个。在这个测试项目中,您将创建一个新的单元测试(Basic Unit Test),我们将其命名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.");
    }
}
于 2012-05-04T17:29:54.463 回答
1

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

  • 圈复杂度(CC)IsValid为 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();
    }
}

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

于 2012-05-04T17:30:26.677 回答