开发者

Creating unit tests for assuring immutability

开发者 https://www.devze.com 2023-02-19 15:58 出处:网络
I have designed an immutable class, because I want to have开发者_StackOverflow中文版 value-semantics for it. I wrote a hint into the commentary section of the class

I have designed an immutable class, because I want to have开发者_StackOverflow中文版 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).


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; }
    }
}


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.


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.

0

精彩评论

暂无评论...
验证码 换一张
取 消

关注公众号