Share via


C# unit testing extension methods and validation

Introduction

This article lays out a conceptual operation for validating a date fails into a specific range for an instance of a payment in a factious monthly payment on a loan. The base requirement is that the payment falls in the current month. The payment class has a property RecievedDateTime indicating when the payment was made. If RecievedDateTime is not in the current month then set the property Late to true to calculate penalties. In a real application, this could be handled with IEditableObject Interface but the focus is on is the date within range, not about penalties.

What is important is that when writing code always consider assertion which includes information being used to perform an operation and that when using methods written by use the developer always back them up with test methods. By performing unit test there is more likelihood that these methods will perform as intended rather than writing methods, invoking the methods in production code. Consider injecting a method call into some complex code and things don’t work properly where it may not be apparent at first glance if the issue is in the method or code prior or after the method is called. With unit testing, a developer can first run the test to determine if the method works and plays well with code in production code. The test methods shown in this article are isolated test in that they are a base test that doesn’t interact with production code as to those new to unit testing introducing higher level unit test will make it harder to understand the core basics of writing unit test. One method could have one or two test or a dozen test depending on the scope of use they will play in a solution/project.

Important

System.ValueTuple NuGet package is required for running the solution as the majority of code uses named Tuples. Also language extension methods are dominated in the code which follows and extensively used throughout.

First attempt

Dealing with date range with SQL-Server is simple as there is a BETWEEN clause while C# does not have a between method. Which means either writing logic within code that needs to validate a date is in a range, create a method or create a language extension method such as the one shown below.

public static  bool Between(this DateTime pValue, DateTime pLowerValue, DateTime pUpperValue, bool  pInclusive = false)
{
    return pInclusive
        ? pLowerValue <= pValue && pValue <= pUpperValue
        : pLowerValue < pValue && pValue < pUpperValue;
}

SQL Example for between for comparison to writing a C# between method.

DECLARE @StartDate AS DATETIME = '2014-08-23 00:00:00.000';
DECLARE @EndDate AS DATETIME = '2014-08-30 00:00:00.000';
DECLARE @CustomerId AS INT  = 42;
 
SELECT  O.OrderID ,
 
        C.CompanyName,
        FORMAT(O.OrderDate, 'MM-dd-yyyy',  'en-US') AS OrderDate,
        FORMAT(O.RequiredDate, 'MM-dd-yyyy',  'en-US') AS RequiredDate,
        O.ShipAddress ,
        O.ShipCity ,
        O.ShipPostalCode ,
        O.ShipCountry
FROM    Orders AS O
        INNER JOIN Customers  AS  C ON  O.CustomerIdentifier = C.CustomerIdentifier
WHERE   ( O.ShippedDate BETWEEN @StartDate AND @EndDate )
        AND ( O.CustomerIdentifier = @CustomerId );

Second attempt

Sticking with an extension method which is written and usable in a project or in a class projects other projects can reference. Once written the realization is this extension method would also be useful for other types such as int and double. The following is a rewrite of the method above which works with DateTime, int and double along with other numeric types as this version is a generic method.

public static  bool Between<T>(this T pValue, T pLowerValue, T pUpperValue) =>
    Comparer<T>.Default.Compare(pValue, pLowerValue) >= 0 &&
    Comparer<T>.Default.Compare(pValue, pUpperValue) <= 0;

Many developers don't work much with generics and instead might write a method such as the first version presented which is fine if there is no need for a generic method. What is not good is simply brute force writing a method, get it too work and then have performance issues. For example, the following has to generate the integer range first. LINQ and Lambda are great but must be used knowing how they will work in the wild.

int x = 30;
if (Enumerable.Range(1,100).Contains(x))
    //true

Unit testing

Once the generic method has been written it’s time to validate the method works. This requires unit testing on multiple types, in this case, DateTime, int, and Double. 
Since this started with dates in a specific range this will be the first test.  With that create a new unit test project, create a base class which in this case provide methods to return ranges of dates and numerics (as we need to test these after dates). The following will feed into a unit test for validating a date falls into a date range. 

public (DateTime StartRange, DateTime EndRange, DateTime Value) DateTimeItems()
{
    var now = DateTime.Now;
    var startDate = new  DateTime(now.Year, now.Month, 1);
    var endDate = startDate.AddMonths(1).AddDays(-1).AddSeconds(-1);
 
    return (
        new DateTime(DateTime.Now.Year, DateTime.Now.Month, startDate.Day), 
        new DateTime(DateTime.Now.Year,  DateTime.Now.Month, endDate.Day), 
        new DateTime(DateTime.Now.Year,  DateTime.Now.Month, DateTime.Now.Day)
    );
}

In the test class a test to validate the date range extension method.

[TestMethod]
[TestTraits(Trait.IComparableExtensionMethods)]
public void  BetweenDateRangeFromTestBase()
{
    // arrange 
    var (StartRange, EndRange, Value) = DateTimeItems();
 
    // act assert
    Assert.IsTrue(Value.Between(StartRange, EndRange),
        $"Expected {Value} to be in range {StartRange} to {EndRange}");
}

Run the test method to validate the extension method works as expected. If there are issues now is the time to resolve the issues and perhaps write more test.

Improving test

Can this be improved? Yes, by creating another extension method that will return start and ending dates outside of unit testing as this can be a handy method to have. The following does this, returns a DateTime representing the first day of the month and a DateTime representing the last day of the month. By returning the full DateTime there are no restrictions as in if other details are needed for other date time operations.

using System;
 
namespace Library.LanguageExtensions
{
    public static  class DateTimeExtensions
    {
        /// <summary>
        /// Provides a date for first day and last day of month.
        /// </summary>
        /// <param name="now">Date to find first and last day of month</param>
        /// <returns>
        /// ValueTuple representing first and last dates of month for now.
        /// </returns>
        /// <remarks>
        /// endDate subtracts one second for use with database SQL operations.
        /// </remarks>
        public static  (DateTime FirstDayOfMonth, DateTime LastDayOfMonth) FirstLastDayOfMonth(this DateTime now)
        {
            var startDate = new  DateTime(now.Year, now.Month, 1);
            var endDate = startDate.AddMonths(1).AddDays(-1).AddSeconds(-1);
 
            return (startDate, endDate);
        }
    }
}

As with the range extension method, at least one test method is required to ensure the extension method functions properly.

/// <summary>
/// Validate <see cref="DateTimeExtensions.FirstLastDayOfMonth"></see> extension.
/// </summary>
[TestMethod]
[TestTraits(Trait.DateTimeExtensionMethods)]
public void  FirstLastDayOfMonth()
{
    // arrange
    var today = DateTime.Now;
    var startDate = new  DateTime(today.Year, today.Month, 1);
    var endDate = startDate.AddMonths(1).AddDays(-1).AddSeconds(-1);
 
    // act
    var (FirstDayOfMonth, LastDayOfMonth) = DateTime.Now.FirstLastDayOfMonth();
 
    // assert
    Assert.IsTrue(startDate.Equals(FirstDayOfMonth),
        "Expected start dates to match.");
 
    Assert.IsTrue(endDate.Equals(LastDayOfMonth),
        "Expected end dates to match.");
 
}

Generic validation

Validate by running the test. Next write test methods for numeric type for the between extension method.

/// <summary>
/// Validate <see cref="GeneralExtensions.Between"></see> extension 
/// method functions with int where value is between start and end int values
/// </summary>
[TestMethod]
[TestTraits(Trait.IComparableExtensionMethods)]
public void  BetweenIntRange()
{
    // arrange, act assert
    var (StartRange, EndRange, Value) = IntItems();
 
    Assert.IsTrue(Value.Between(StartRange, EndRange),
        "Expected testValue to be in range");
 
    // arrange
    Value += StartRange;
 
    // assert
    Assert.IsFalse(Value.Between(StartRange, EndRange),
        "Expected testValue to be in range");
 
}
/// <summary>
/// Validate <see cref="GeneralExtensions.Between"></see> extension 
/// method functions with double where value is between start and end double values
/// </summary>
[TestMethod]
[TestTraits(Trait.IComparableExtensionMethods)]
public void  BetweenDoubleRange() 
{
    // arrange, act assert
    var (StartRange, EndRange, Value) = DoubleItems();
 
    Assert.IsTrue(Value.Between(StartRange, EndRange),
        "Expected testValue to be in range");
 
    // arrange
    Value += StartRange;
 
    // assert
    Assert.IsFalse(Value.Between(StartRange, EndRange),
        "Expected testValue to be in range");
 
}
/// <summary>
/// Given a range of -999 to 999 when asserting against 
/// <see cref="GeneralExtensions.Between"></see> if value is in range 
/// then I expect Assert.IsFalse to be true.
/// </summary>
[TestMethod]
[TestTraits(Trait.IComparableExtensionMethods)]
public void  BetweenIntRangeOutOfRange()
{
    // arrange
    int startRange = -999;
    int endRange = 999;
    int value = -1001;
 
    // act
    Assert.IsFalse(value.Between(startRange, endRange),
        "Expected testValue to be out of range");
}

Using a test base class

A test base class, in this case, provides methods to return data which feeds into several of the test methods which permits consistency as the same data is used and can be altered for a test then in the next test is fresh, unaltered. The test base class (TestBase) is marked as abstract as the intent is for the class to only be used for specific purposes and as abstract must be implemented in another class to be used.

Note that in each method the summary are setup to reference the method being tested by using ‘see cref’ attribute for documentation along with the method within is now a hyperlink to the method. Using tools such as SandCastle this enhances a compile help file to be navigated from one topic to another topic in the help file.

using System;
using UnitTestExtensions.Test;
 
namespace UnitTestExtensions.BaseClasses
{
    public abstract  class TestBase
    {
        /// <summary>
        /// Provides values for unit test <see cref="ExtensionsTest.BetweenIntRange"></see>.
        /// </summary>
        /// <returns></returns>
        public (int StartRange, int EndRange, int Value) IntItems()
        {
            return (-999, 999, -999);
        }
        public (double StartRange, Double EndRange, Double Value) DoubleItems()
        {
            return (-999, 999, -999);
        }
        /// <summary>
        /// Provides two dates used for obtaining first and last day of month.
        /// </summary>
        /// <returns></returns>
        /// <remarks>
        /// By returning dates rather than days makes this method flexible for 
        /// <see cref="ExtensionsTest.BetweenDateRangeFromTestBase"></see> other date 
        /// related routines.
        /// </remarks>
        public (DateTime StartRange, DateTime EndRange, DateTime Value) DateTimeItems()
        {
            var now = DateTime.Now;
            var startDate = new  DateTime(now.Year, now.Month, 1);
            var endDate = startDate.AddMonths(1).AddDays(-1).AddSeconds(-1);
 
            return (
                new DateTime(DateTime.Now.Year, DateTime.Now.Month, startDate.Day), 
                new DateTime(DateTime.Now.Year,  DateTime.Now.Month, endDate.Day), 
                new DateTime(DateTime.Now.Year,  DateTime.Now.Month, DateTime.Now.Day)
            );
        }
    }
}

Validating properties with DataAnnotations

DataAnnotations library is in .NET Framework. The purpose of DataAnnotations is to custom domain entity class with attributes. Therefore, DataAnnotations contains validation attributes to enforce validation rules, display attributes to specify how data from the class or member is displayed, and data modeling attributes to specify the intended use of data members and the relationships between data classes.  Where this comes into place for the date range is decorating the property  RecievedDateTime in Payment class so that it will be validated using the Between language extension method.

using Library.LanguageExtensions;
using System;
using System.ComponentModel.DataAnnotations;
 
namespace ValidatorLibrary
{
    /// <summary>
    /// Ensure date is in current month
    /// </summary>
    [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
    public class  RecievedDateTimeAttribute : ValidationAttribute
    {
        public override  bool IsValid(object value)
        {
            if (value == null)
            {
                return true;
            }
 
            var (FirstDayOfMonth, LastDayOfMonth) = DateTime.Now.FirstLastDayOfMonth();
            if (!Convert.ToDateTime(value).Between(FirstDayOfMonth, LastDayOfMonth))
            {
                return false;
            }
 
            return true;
        }
    }
}

To properly test a good test and an invalid test is required.

using System;
using BusnessEntities;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using ValidatorLibrary;
 
namespace UnitTestExtensions.Test
{
    [TestClass]
    public class  ValidationTest
    {
        /// <summary>
        /// Given a RecievedDateTime not in the current month validates.
        /// </summary>
        [TestMethod]
        [TestTraits(Trait.Validating)]
        public void  IsValidatePayment()
        {
            // arrange
            var payment = new  Payment { RecievedDateTime = DateTime.Now };
 
            // act
            var validationResult = ValidationHelper.ValidateEntity(payment);
 
            // assert
            Assert.IsFalse(validationResult.HasError,
                "Expected a validate payment");
 
        }
 
        /// <summary>
        /// Given a RecievedDateTime not in the current month validation fails.
        /// </summary>
        [TestMethod]
        [TestTraits(Trait.Validating)]
        public void  IsNotValidatePayment()
        {
            // arrange
            var payment = new  Payment { RecievedDateTime = DateTime.Now.AddMonths(-1) };
 
            // act
            var validationResult = ValidationHelper.ValidateEntity(payment);
 
            // assert
            Assert.IsTrue(validationResult.HasError,
                "Expected a invalidate payment");
 
        }
    }
}

Validation class project

The base code for validation resides in a class project which can be used in any Visual Studio solution. In the case of this article the class to validate a DateTime is within the validation library but if it was specific to one solution this method would be placed into a business class project which would reference the validation library for the base classes required for implementing any validating attribute. 

Summary

This article provided the basis for working through designing useful language extension methods, unit testing the language extension methods to ensure they work properly along with basics for using one of the extension methods for validating a property of an instance of a class which would be used to update an entity in a data container.

Named Tuples were used rather than using conventional methods to return multiple pieces of information from a method. When using named tuples you should not be looking to use them but instead, understand them and realize when coding that using named tuples make sense other another method to implement returning data. Note that Tuples support discards which permit ignoring one or more return values. 

Special note
In many of the test methods, the following provides a method to navigate to the method may seem unnecessary but this is not the intent. The intent is if a help file is made this provides a navigation method to the function/method.
 

Source code

https://github.com/karenpayneoregon/UnitTestingValidationExtensionMethods 

See also

Different methods to display unit test in Visual Studio  

References

Unit testing your code.
Unit test basics.
How to run unit test from Visual Studio.
Microsoft forum: Testing tool in Visual Studio.
Microsoft forum: Unit testing general.
Getting started with live unit testing in Visual Studio.