创建单元测试以确保不变性

发布于 2024-10-26 06:17:21 字数 288 浏览 1 评论 0原文

我设计了一个 不可变类,因为我想为其提供值语义。我在类的注释部分写了一个提示

//“这个类是不可变的,向它添加新功能时不要更改它。”

但我知道,有时这些注释会被其他类忽略团队成员,所以我想创建一个单元测试作为额外的保障。知道如何实现这一点吗?可以通过反射检查类以确保只有构造函数更改其内部状态吗?

(使用 C# 2.0 和 NUnit,如果这对任何人都很重要的话)。

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).

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

老旧海报 2024-11-02 06:17:21

一个支持我关于如何递归使用 FieldInfo.IsInitOnly 来测试不变性的评论的示例。

可能还有更多特殊情况需要考虑,例如我如何处理string,但我相信它只会给出假阴性,即会告诉您某些东西是可变的,但不是,而不是相反。

逻辑是,每个字段都必须是只读的并且本身是不可变类型。请注意,它不会处理自引用类型或循环引用。

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

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; }
    }
}
稍尽春風 2024-11-02 06:17:21

您可以检查该类是否已密封,并使用反射检查每个字段是否为只读(使用 FieldInfo.IsInitOnly)。

当然,这只能确保不变性 - 它不会阻止某人将List字段放入其中,然后更改列表的内容。

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.

自控 2024-11-02 06:17:21

不确定您是否听说过 NDepend,但此工具允许您“内省”源代码并编译程序集并执行各种魔法,包括依赖项检查等等。

其中一项检查是对不变性的检查。例如,我有一个 IImmutable 标记接口,如果任何类型具有此接口但可变,则使用以下查询,NDepend 会导致我的构建失败:

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

您还可以将其配置为生成违规报告,以及失败的构建。

显然这实际上不是单元测试。但是,它可以集成为构建的一部分,并且像单元测试一样使构建失败,所以我想我会提到它!

请参阅此处,了解有关其实际功能和操作方式的更多信息。

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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文