Asserting Expected Exception Messages – Reloaded

Posted: April 18, 2013 in .Net, Delegates, Generics, Unit Testing
Tags: , , , , ,

Just this week I blogged about how to assert the message of an expected exception and one of the things I was mentioning is how quite some people had proposed different implementations.

Today I found this post from a fellow blogger in which he talks about the MSTest Extensions library. Basically it explains how to install the NuGet package and shows a small example of how to use the ExceptionAssert method to fulfill the need of asserting the thrown exception’s message.

Unfortunately this implementation still suffers from the limitations that we discussed the last time, it can only deal with Actions so if you want to test a method with a return value or a constructor, this option will not provide a solution.

Not everything about this option is bad though, the use of generics is a nice touch so I thought I could include a couple of improvements to the solution I proposed:

    public static class AssertException
    {
        public static void Is<T>(string message, Delegate action, params object[] parameters) where T : Exception
        {
            try
            {
                action.DynamicInvoke(parameters);
                Assert.Fail(string.Format("Expected exception of type <{0}> with message <{1}> but none was thrown.", typeof(T).Name, message));
            }
            catch (Exception ex)
            {
                if (ex is AssertFailedException || ex is AssertInconclusiveException) throw;

                if (ex.InnerException == null)
                {
                    Assert.IsTrue(ex is T, string.Format("Expected exception type: <{0}> Actual: {1}", typeof(T).Name, ex.GetType().Name));
                    Assert.AreEqual(message, ex.Message, true, CultureInfo.InvariantCulture);
                }
                else
                {
                    Assert.IsTrue(ex.InnerException is T, string.Format("Expected exception type: <{0}> Actual: {1}", typeof(T).Name, ex.InnerException.GetType().Name));
                    Assert.AreEqual(message, ex.InnerException.Message, true, CultureInfo.InvariantCulture);
                }
            }
        }

        public static void Is(Exception expected, Delegate action, params object[] parameters)
        {
            try
            {
                action.DynamicInvoke(parameters);
                Assert.Fail(string.Format("Expected exception of type <{0}> with message <{1}> but none was thrown.", expected.GetType().FullName, expected.Message));
            }
            catch (Exception ex)
            {
                if (ex is AssertFailedException || ex is AssertInconclusiveException) throw;

                if (ex.InnerException == null)
                {
                    Assert.IsTrue(expected.GetType().IsInstanceOfType(ex), string.Format("Expected exception type: <{0}> Actual: {1}", expected.GetType().Name, ex.GetType().Name));
                    Assert.AreEqual(expected.Message, ex.Message, true, CultureInfo.InvariantCulture);
                }
                else
                {
                    Assert.IsTrue(expected.GetType().IsInstanceOfType(ex.InnerException), string.Format("Expected exception type: <{0}> Actual: {1}", expected.GetType().Name, ex.InnerException.GetType().Name));
                    Assert.AreEqual(expected.Message, ex.InnerException.Message, true, CultureInfo.InvariantCulture);
                }
            }
        }
    }

As you can see now the class has two overloaded methods, a generic and a non-generic versions. Also the names of both the class and the method changed so it reads a little bit better and finally both now support inherited exceptions.

This is what the use of this class will now look like:

        [TestMethod]
        public void ConstructorThrowsExceptionWhenArgumentsAreInvalid()
        {
           AssertException.Is<ArgumentNullException>("Value cannot be null.\r\nParameter name: number",
              new Func<int?, string, Foo>((num, name) => new Foo(num, name)),
              null, "any");

           AssertException.Is<ArgumentNullException>("Value cannot be null.\r\nParameter name: name",
              new Func<int?, string, Foo>((num, name) => new Foo(num, name)),
              1, string.Empty);

           AssertException.Is(new ArgumentNullException("name"),
              new Func<int?, string, Foo>((num, name) => new Foo(num, name)),
              1, string.Empty);
        }

        [TestMethod]
        public void MethodsThrowExceptions()
        {
            var firstFoo = new Foo(-5, "something");
            AssertException.Is<ApplicationException>("Number should not be negative",
               new Action(firstFoo.DoSomething));

            var secondFoo = new Foo(5, "something");
            AssertException.Is<DivideByZeroException>("Number cannot be divided by zero",
               new Action<int>(secondFoo.DoSomethingElse),
               0);
        }

        [TestMethod]
        public void FunctionsThrowExceptions()
        {
            var foo = new Foo(1, "invalid");

            AssertException.Is<InvalidCredentialException>("Authorization denied",
               new Func<bool>(foo.Validate));

            AssertException.Is<NullReferenceException>("Seriously Foo cannot be null",
               new Func<string, int?, Foo, Foo>(foo.GetNewFoo),
               "any", 3, null);
        }

As you can see the code now almost reads as regular English. It is still more verbose than the attribute option but we discussed the advantages and disadvantages of both approaches on the previous post.

So a couple of conclusions that I can get are that even though we may have a good solution for a particular problem we can always find inspiration and ideas to improve those solutions and we should always be humble enough to realize that our solutions could be improved, no matter how much we might like them.

The same as last time here you have several options to assert the message of exceptions in your unit tests. Use the one you consider the most appropriate. Happy coding.

Advertisements

Does this make sense to you?

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s