8

I have designed an immutable class, because I want to have value-semantics for it. I wrote a hint into the commentary section of the class

// "This class is immutable, don't change this when adding new features to it."

But I know, sometimes those commentaries are overlooked by other team members, so I would like to create a unit test as an additional safeguard. Any idea how to accomplish this? Can one inspect a class via reflection to make sure only the constructors change it's inner state?

(Using C# 2.0 and NUnit, if that's important for anyone).

weston
  • 54,145
  • 21
  • 145
  • 203
Doc Brown
  • 19,739
  • 7
  • 52
  • 88

3 Answers3

10

An example to back up my comment on how you can use FieldInfo.IsInitOnly recursively to test for immutability.

There may be more special cases to consider like how I have handled string, but it will only give false negatives I believe, i.e. will tell you something is mutable that is not, not the other way around.

The logic is, every field must be readonly and be an immutable type itself. Note that it will not cope with self referential types or circular references.

using System;
using System.Linq;
using System.Reflection;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace ImmutableTests
{
    [TestClass]
    public class AssertImmutableTests
    {
        [TestMethod]
        public void Is_int_immutable()
        {
            Assert.IsTrue(Immutable<int>());
        }

        [TestMethod]
        public void Is_string_immutable()
        {
            Assert.IsTrue(Immutable<string>());
        }

        [TestMethod]
        public void Is_custom_immutable()
        {
            Assert.IsTrue(Immutable<MyImmutableClass>());
        }

        [TestMethod]
        public void Is_custom_mutable()
        {
            Assert.IsFalse(Immutable<MyMutableClass>());
        }

        [TestMethod]
        public void Is_custom_deep_mutable()
        {
            Assert.IsFalse(Immutable<MyDeepMutableClass>());
        }

        [TestMethod]
        public void Is_custom_deep_immutable()
        {
            Assert.IsTrue(Immutable<MyDeepImmutableClass>());
        }

        [TestMethod]
        public void Is_propertied_class_mutable()
        {
            Assert.IsFalse(Immutable<MyMutableClassWithProperty>());
        }

        private static bool Immutable<T>()
        {
            return Immutable(typeof(T));
        }

        private static bool Immutable(Type type)
        {
            if (type.IsPrimitive) return true;
            if (type == typeof(string)) return true;
            var fieldInfos = type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
            var isShallowImmutable = fieldInfos.All(f => f.IsInitOnly);
            if (!isShallowImmutable) return false;
            var isDeepImmutable = fieldInfos.All(f => Immutable(f.FieldType));
            return isDeepImmutable;
        }
    }

    public class MyMutableClass
    {
        private string _field;
    }

    public class MyImmutableClass
    {
        private readonly string _field;
    }

    public class MyDeepMutableClass
    {
        private readonly MyMutableClass _field;
    }

    public class MyDeepImmutableClass
    {
        private readonly MyImmutableClass _field;
    }

    public class MyMutableClassWithProperty
    {
        public string Prop { get; set; }
    }
}
weston
  • 54,145
  • 21
  • 145
  • 203
8

You could check that the class is sealed, and using reflection check that each field is read-only (using FieldInfo.IsInitOnly).

Of course, that only ensures shallow immutability - it wouldn't stop someone from putting a List<int> field in there, and then changing the contents of the list.

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • Good idea this, I have used this to create a `AssertImmutable()` method which you can call recusively on the field types to assert deep immutability. – weston Jun 17 '13 at 15:14
  • @weston Have you shared this? – denver Mar 06 '14 at 20:32
  • 1
    @denver I have now, it's recreated from memory and you may find some more edge cases like `string` http://stackoverflow.com/a/22251440/360211 – weston Mar 07 '14 at 13:31
2

Not sure if you've heard of NDepend, but this tool allows you to "introspect" over your source code and compiled assemblies and do all sorts of magic including dependency checking and much more.

One such check is a check for immutability. For instance, I have an IImmutable marker interface, and NDepend fails my build if any types have this interface but are mutable, using the following query:

WARN IF Count > 0 IN SELECT TYPES WHERE 
  Implement "MyCompany.MyAssemblies.Dto.IImmutable" AND
  !IsImmutable

You can also configure it to generate violation reports, as well as failing builds.

Obviously this isn't actually a unit test. However, it can be integrated as part of your build, and fail your build just as a unit test would, so I thought I'd mention it!

See here for more info on what it actually does and how.

Rob Levine
  • 40,328
  • 13
  • 85
  • 111
  • +1, this answer is useful, too, but I can only accept one. Perhaps I will try NDepend to see if is worth the price four our projects. – Doc Brown Mar 24 '11 at 11:59