posts - 19 , comments - 8 , trackbacks - 0

A Generic Assert Class for C#

Overview

The following code provides a mechanism for creating asserts which throw a generic exception.

My initial motivation was to express in a more concise way common argument and result checking code. For example the following code:

        public string Read(Stream stream, Encoding encoding, int count)
        {
            if (stream == null)
                throw new ArgumentNullException("stream", "The stream must be created.");
            if (encoding == null)
                throw new ArgumentNullException("encoding", "The encoding must be specified.");
            if (count < 0)
                throw new ArgumentException("The count must be positive.", "count");

            var buffer = new byte[count];
            var bytesRead = stream.Read(buffer, 0, count);
            if (bytesRead != count)
                throw new InvalidDataException("Unable to read the requested bytes.");

            return encoding.GetString(buffer);
        }

This could be expressed in the following way.

        public string Read(Stream stream, Encoding encoding, int count)
        {
            Asserter<ArgumentNullException>.IsNotNull(stream, "stream", "The stream must be created.");
            Asserter<ArgumentNullException>.IsNotNull(encoding, "encoding", "The encoding must be specified.");
            Asserter<ArgumentException>.IsTrue(count >= 0, "The count must be positive.", "count");

            var buffer = new byte[count];
            var bytesRead = stream.Read(buffer, 0, count);
            Asserter<InvalidDataException>.AreEqual(count, bytesRead, "Unable to read the requested bytes.");

            return encoding.GetString(buffer);
        }

A second motivation was to express unit test assertions not supported by the Microsoft library in a tidy manner. The following demonstrates such a test.

    [TestClass]
    public class AsserterTest
    {
        [TestMethod]
        public void TestMethod1()
        {
            var foo = new[] {1, 2, 3, 4};
            var bar = new List<int> {1, 2, 3, 4};
            Asserter<AssertFailedException>.SequenceEqual(foo, bar, "I can't count to four.");
        }
    }

Usage

The arguments following the test args should be of the same type and in the same order as the specified exception constructor accepts. They are simply lifted off the end of the assertion and passed to create instance method of the activator class.

Notes On The Code

The most upsetting feature is the use of Activator. I experimented with a number of factory class style implementations using static generics, but it seemed like the bang for bucks simplicity provided by this class outweighed the performance gains. Also, as we are throwing exceptions, performance isn't necessarily the highest priority.

An oddity in the code is the evaluation of any delegate parameters before the arguments are passed to the create method. Consider the following code.

        public void ThrowIfNotGreater(int x, int y)
        {
            Asserter<Exception>.IsTrue(x > y, string.Format("{0} should be greater than {1}", x, y));
        }

The string formatting will occur whether the assertion succeeds or fails. Performance is an issue here as we want to avoid slowing down code in the non-error path. If we pass delegates in we can avoid this.

        public void ThrowIfNotGreater(int x, int y)
        {
            Asserter<Exception>.IsTrue(x > y, ((Func<string>)(() => string.Format("{0} should be greater than {1}", x, y))));
        }

The code

using System;
using System.Collections.Generic;
using System.Linq;

namespace JetBlack.Diagnostics
{
    public static class Asserter<TException> where TException : Exception
    {
        private static void Raise(params object[] args)
        {
            var expandedArgs = new object[args.Length];
            for (var i = 0; i < args.Length; ++i)
                expandedArgs[i] = args[i] is Delegate ? ((Delegate)args[0]).DynamicInvoke() : args[i];

            throw (TException)Activator.CreateInstance(typeof(TException), expandedArgs);
        }

        public static void AreEqual<T>(T expected, T received, params object[] args)
        {
            if (!Equals(expected, received))
                Raise(args);
        }

        public static void AreEqual(double expected, double received, double threshold, params object[] args)
        {
            if (Math.Abs(expected - received) >= threshold)
                Raise(args);
        }

        public static void AreNotEqual<T>(T expected, T received, params object[] args)
        {
            if (Equals(expected, received))
                Raise(args);
        }

        public static void AreNotEqual(double expected, double received, double threshold, params object[] args)
        {
            if (Math.Abs(expected - received) < threshold)
                Raise(args);
        }

        public static void IsTrue(bool value, params object[] args)
        {
            if (!value)
                Raise(args);
        }

        public static void IsFalse(bool value, params object[] args)
        {
            if (value)
                Raise(args);
        }

        public static void IsNull(object value, params object[] args)
        {
            if (value != null)
                Raise(args);
        }

        public static void IsNotNull(object value, params object[] args)
        {
            if (value == null)
                Raise(args);
        }

        public static void ShouldThrow<T>(Action action, params object[] args) where T : Exception
        {
            try
            {
                action();
            }
            catch (T)
            {
                return;
            }
            Raise(args);
        }

        public static void SequenceEqual<T>(IEnumerable<T> first, IEnumerable<T> second, params object[] args)
        {
            if (!first.SequenceEqual(second))
                Raise(args);
        }
    }
}

Print | posted on Thursday, December 4, 2014 1:25 PM | Filed Under [ C# Assert Generic ]

Feedback

No comments posted yet.
Post A Comment
Title:
Name:
Email:
Comment:
Verification:
 

Powered by: