English 中文(简体)
C#: How do you write test cases for constructors and constructor overloads?
原标题:

Say you have this shell of a class:

public class Number
{
    private int value;

    public Number()
        : this(0) {}

    public Number(int initialValue)
        : this(initialValue, 0, 100) {}

    public Number(int initialValue, int minimumValue, int maximumValue)
    {
        if (minimumValue > maximumValue)
            throw new ArgumentException("Minimum cannot be greater than maximum", "minimumValue");

        MinValue = minimumValue;
        MaxValue = maximumValue;
        Value = initialValue;
    }

    public int MinValue { get; private set; }
    public int MaxValue { get; private set; }

    public int Value
    {
        get { return value; }
        set
        {
            if (value < MinValue)
                value = MinValue;
            if (value > MaxValue)
                value = MaxValue;

            this.value = value;
        }
    }
}

Would you write tests for this class and if so how would you write them?

I m thinking especially about the constructors. Like, would you have one test that created a a Number using the default constructor and checking that the value was 0, minvalue was 0 and maxvalue was 100? Or would that be over specification? Or is it really not, since others could depend on that the default values did not change by accident? Would you write a test for each constructor, or just of the default one since you know it chains through all the others.

问题回答

I have completely switched from the classic approach for TDD to the more modern and logical BDD (Behavior Driven Design). In the case of your Number class, I would write the following BDD Specifications (Note that the syntax below is done with SubSpec, which relies on xUnit.NET):

public void Parameterless_constructor_initializes_all_defaults_properly()
{
    // State
    Number number = null;

    // Context
    "Given a null context".Context(() => {});

    // Concern
    "when creating a new Number with no parameters".Do(() => { number = new Number(); });

    // Observations
    "the Value property should contain the default value 0".Assert(() => Assert.Equal(0, number.value));
    "the MinValue property should be 0".Assert(() => Assert.Equal(0, number.MinValue));
    "the MaxValue property should be 100".Assert(() => Assert.Equal(100, number.MaxValue));
}

public void Single_parameter_constructor_initializes_all_defaults_and_initial_value_properly()
{
    // State
    Number number = null;

    // Context
    "Given a null context".Context(() => {});

    // Concern
    "when creating a new Number with the initial value".Do(() => { number = new Number(10); });

    // Observations
    "the Value property should contain the value 10".Assert(() => Assert.Equal(10, number.value));
    "the MinValue property should be 0".Assert(() => Assert.Equal(0, number.MinValue));
    "the MaxValue property should be 100".Assert(() => Assert.Equal(100, number.MaxValue));
}

public void Full_constructor_initializes_all_values_properly()
{
    // State
    Number number = null;

    // Context
    "Given a null context".Context(() => {});

    // Concern
    "when creating a new Number with the initial, min, and max values".Do(() => { number = new Number(10, 1, 50); });

    // Observations
    "the Value property should contain the value 10".Assert(() => Assert.Equal(10, number.value));
    "the MinValue property should be 1".Assert(() => Assert.Equal(1, number.MinValue));
    "the MaxValue property should be 50".Assert(() => Assert.Equal(50, number.MaxValue));
}

In addition, I noticed that you also have a possible exceptional scenario for your full constructor, when the min value is greater than the max value. You would also want to verify proper behavior in this exceptional case:

public void Full_constructor_throws_proper_exception_when_minvalue_greater_than_maxvalue()
{
    // State
    Number number = null;
    Exception expectedEx = null;

    // Context
    "Given a null context".Context(() => {});

    // Concern
    "when creating a new Number with inverted min and max values".Do(
        () => 
        { 
            try { number = new Number(10, 50, 1); }
            catch (Exception ex) { expectedEx = ex }
        }
     );

    // Observations
    "an exception should be thrown".Assert(() => Assert.NotNull(expectedEx));
    "the exception should be an ArgumentException".Assert(() => Assert.IsType<ArgumentException>(expectedEx));
}

The above specifications should give you 100% test coverage. They also produce a very nice, human readable, logical report when executed with xunit.net and output the default report.

I guess you have several constructors for a reason - try to test scenario and not that the class was initialized according to some rule.
For example if you use the default constructor to create a class for on the fly calculation test that and not the fact that the default constructor has some value set.

My point is that you should not have overloads you do not use (unless you re developing API) so why not test the use case instead of the constructor.

Use nunit.

Create a test that creates an object for each constructor. Use the Assert.AreEqual to make sure all the objects are equal (You should override Equals for classes like this). To be extra sure, negative assert the Assert.AreSame assertion.

Then test each property for correct value.

If your class was more complicated and you wanted to be even more careful, you could then set all the values to unique random numbers and assert that the properties are initialized correctly from a random dataset.

I would write a unit test for each constructor, checking that the minimum and maximum values are set properly. I would do this to make sure that if I change the code of one of the constructors later, my tests tell me what changed where.
I would also extract the default min and max into a constant, probably, so that the test would look like Assert.AreEqual(DefaultMinimum, myNumber.MinValue).
I would have a test checking that an invalid min/max throws an exception.
And I would rename that class "BoundedNumber" or something along these lines :)

OK, I m really answering the question right below the code listing (rather than the one in the headline)...

I think the main value of this class is in its (pun not intended) Value property. Therefore, it is that property rather than the constructors that should be the focus of unit tests.

If you write unit tests after you ve written the three constructors and make those tests too restrictive (over-specification), then you risk ending up with a suite of brittle, hard-to-maintain tests.

All tests for constructors are similar, because things that constructors do are similar by definition. So I wrote a simple testing library which helps write declarative tests for the constructors: How to Easily Test Validation Logic in Constructors in C#

Here is an example in which I am trying seven test cases on a constructor of one class:

[TestMethod]
public void Constructor_FullTest()
{

    IDrawingContext context = new Mock<IDrawingContext>().Object; 

    ConstructorTests<Frame>
        .For(typeof(int), typeof(int), typeof(IDrawingContext))
        .Fail(new object[] { -3, 5, context }, typeof(ArgumentException), "Negative  length")
        .Fail(new object[] { 0, 5, context }, typeof(ArgumentException), "Zero length")
        .Fail(new object[] { 5, -3, context }, typeof(ArgumentException), "Negative width")
        .Fail(new object[] { 5, 0, context }, typeof(ArgumentException), "Zero width")
        .Fail(new object[] { 5, 5, null }, typeof(ArgumentNullException), "Null drawing context")
        .Succeed(new object[] { 1, 1, context }, "Small positive length and width")
        .Succeed(new object[] { 3, 4, context }, "Larger positive length and width")
        .Assert();

}




相关问题
Anyone feel like passing it forward?

I m the only developer in my company, and am getting along well as an autodidact, but I know I m missing out on the education one gets from working with and having code reviewed by more senior devs. ...

NSArray s, Primitive types and Boxing Oh My!

I m pretty new to the Objective-C world and I have a long history with .net/C# so naturally I m inclined to use my C# wits. Now here s the question: I feel really inclined to create some type of ...

C# Marshal / Pinvoke CBitmap?

I cannot figure out how to marshal a C++ CBitmap to a C# Bitmap or Image class. My import looks like this: [DllImport(@"test.dll", CharSet = CharSet.Unicode)] public static extern IntPtr ...

How to Use Ghostscript DLL to convert PDF to PDF/A

How to user GhostScript DLL to convert PDF to PDF/A. I know I kind of have to call the exported function of gsdll32.dll whose name is gsapi_init_with_args, but how do i pass the right arguments? BTW, ...

Linqy no matchy

Maybe it s something I m doing wrong. I m just learning Linq because I m bored. And so far so good. I made a little program and it basically just outputs all matches (foreach) into a label control. ...

热门标签