Trying to get an understanding of proper unit testing, I've read up quite a bit and found myself writing tests like the ones that follow. Based on "best practices" etc., how am I doing as far as naming the tests, ensuring that each test is correctly limited in what it does, and providing good test coverage? Following those points:
- I've been naming the tests rather verbosely based on the suggestion that a random person who doesn't know this code (myself in six months) should be able to figure out what each test does from just its name -- note each test has
Struct
in its name because there are two methods with this signature, and the other (with a nearly-identical set of tests) takes classes. - I've written a few more single-line tests than I'd like and will be refactoring most of this soon, but to keep the tests limited, I'm trying hard to make sure that every test only verifies a single aspect of the interface.
- And last, the
IsWithinRange
method having been written to verify something is ... well .. "within a range"... I've gone out of my way to test the minimum and maximum accepted values, a value in between, and values just past each end, as well as checking each type of exception I expect the method to throw. Is there anything else I'm not testing and should?
The full source for the project is on GitHub if anyone wants to look for more information, but the basic idea is that this Validate
class is used to check values at the beginning of a method, and all exceptions for invalid data are returned as a single ValidationException
. The full concept came from an article by Rick Brewster, and I've just run with it for a bit and written up more ways to check values as I found need for them.
The unit tests currently in question:
[TestMethod]
public void PassingValidToStruct_IsWithinRange_Passes()
{
Validate.Begin().IsWithinRange((Int32?)5, "value", 0, 10).Check();
}
[TestMethod]
public void PassingMinToStruct_IsWithinRange_Passes()
{
Validate.Begin().IsWithinRange((Int32?)0, "value", 0, 10).Check();
}
[TestMethod]
public void PassingMaxToStruct_IsWithinRange_Passes()
{
Validate.Begin().IsWithinRange((Int32?)10, "value", 0, 10).Check();
}
[TestMethod]
public void MinMaxValueSameToStruct_IsWithinRange_Passes()
{
Validate.Begin().IsWithinRange((Int32?)5, "value", 5, 5).Check();
}
[TestMethod]
public void PassingTooSmallToStruct_IsWithinRange_Fails()
{
var valid = Validate.Begin().IsWithinRange((Int32?)-1, "value", 0, 10);
ExceptionAssert.Throws<ValidationException>(() => valid.Check());
}
[TestMethod]
public void PassingTooLargeToStruct_IsWithinRange_Fails()
{
var valid = Validate.Begin().IsWithinRange((Int32?)11, "value", 0, 10);
ExceptionAssert.Throws<ValidationException>(() => valid.Check());
}
[TestMethod]
public void PassingNullToStruct_IsWithinRange_Fails()
{
var valid = Validate.Begin().IsWithinRange((Int32?)null, "value", 0, 10);
ExceptionAssert.Throws<ValidationException>(() => valid.Check());
}
[TestMethod]
public void PassingNullNameToStruct_IsWithinRange_Throws()
{
ExceptionAssert.Throws<ValidationException>(() => Validate.Begin().IsWithinRange((Int32?)5, null, 0, 10));
}
[TestMethod]
public void PassingEmptyNameToStruct_IsWithinRange_Throws()
{
ExceptionAssert.Throws<ValidationException>(() => Validate.Begin().IsWithinRange((Int32?)5, "", 0, 10));
}
[TestMethod]
public void PassingNullMinToStruct_IsWithinRange_Throws()
{
ExceptionAssert.Throws<ValidationException>(() => Validate.Begin().IsWithinRange((Int32?)5, "value", null, 10));
}
[TestMethod]
public void PassingNullMaxToStruct_IsWithinRange_Throws()
{
ExceptionAssert.Throws<ValidationException>(() => Validate.Begin().IsWithinRange((Int32?)5, "value", 0, null));
}
[TestMethod]
public void PassingMinMaxBackwardsToStruct_IsWithinRange_Throws()
{
ExceptionAssert.Throws<ArgumentException>(() => Validate.Begin().IsWithinRange((Int32?)5, "value", 10, 0));
}
And the method in question:
internal const String ArgumentOutOfRange = "{0}'s value of \"{1}\" is out of range; must be a value from \"{2}\" to \"{3}\"";
/// <summary>
/// Checks a comparable value to ensure that it is within the specified range
/// </summary>
/// <typeparam name="T">Any object type which is a struct, and which implements IComparable</typeparam>
/// <param name="validation">The validation instance to carry forward</param>
/// <param name="value">An instance of T? to check</param>
/// <param name="name">The name of the value</param>
/// <param name="min">An instance of T? which acts as the minimum accepted value</param>
/// <param name="max">An instance of T? which acts as the maximum accepted value</param>
/// <exception cref="ArgumentException">Thrown if min is greater than max</exception>
/// <exception cref="ValidationException">Thrown if min or max is null, or the name is an empty string</exception>
/// <exception cref="ArgumentOutOfRange">Passed on in the return when the value is not within the expected range</exception>
/// <exception cref="ArgumentNullException">Passed on in the return when the value is null</exception>
public static Validation IsWithinRange<T>(this Validation validation, T? value, String name, T? min, T? max) where T : struct, IComparable
{
Validate.Begin()
.IsNotNullOrEmpty(name, "name")
.IsNotNull(min, "min")
.IsNotNull(max, "max")
.Check();
if (min.Value.CompareTo(max.Value) > 0)
throw new ArgumentException("min must be equal to or less than max");
if (!value.HasValue)
return validation.GetInstance().AddException(new ArgumentNullException(name, String.Format(ArgumentNull, name)));
if (min.Value.CompareTo(value.Value) > 0 || max.Value.CompareTo(value.Value) < 0)
return validation.GetInstance().AddException(new ArgumentOutOfRangeException(name, String.Format(ArgumentOutOfRange, name, value.Value, min.Value, max.Value)));
return validation;
}