【问题标题】:Unit Tests maintainability and factories单元测试可维护性和工厂
【发布时间】:2012-07-19 23:33:15
【问题描述】:

编辑:我决定完全放弃这个想法,因为虽然使用相同的方法创建我的实例很好,但我放弃了其他事情并使问题复杂化。

在我的单元测试项目中,我有一个文件夹,其中基本上包含了我的单元测试的所有工厂类,如下所示。

public static class PackageFileInfoFactory
{
    private const string FILE_NAME = "testfile.temp";

    public static PackageFileInfo CreateFullTrustInstance()
    {
        var mockedFileInfo = CreateMockedInstance(AspNetHostingPermissionLevel.Unrestricted);

        return mockedFileInfo.Object;
    }

    public static PackageFileInfo CreateMediumTrustInstance()
    {
        var mockedFileInfo = CreateMockedInstance(AspNetHostingPermissionLevel.Medium);

        return mockedFileInfo.Object;
    }

    private static Mock<PackageFileInfo> CreateMockedInstance(AspNetHostingPermissionLevel trustLevel)
    {
        var mockedFileInfo = new Mock<PackageFileInfo>(FILE_NAME);

        mockedFileInfo.Protected().SetupGet<AspNetHostingPermissionLevel>("TrustLevel").Returns(() => trustLevel);

        mockedFileInfo.Protected().Setup<string>("CopyTo", ItExpr.IsAny<string>()).Returns<string>(destFileName => "Some Unknown Path");

        return mockedFileInfo;
    }
}

这是我的单元测试示例。

public class PackageFileInfoTest
{
    public class Copy
    {
        [Fact]
        public void Should_throw_exception_in_medium_trust_when_probing_directory_does_not_exist()
        {
            // Arrange
            var fileInfo = PackageFileInfoFactory.CreateMediumTrustInstance();

            fileInfo.ProbingDirectory = "SomeDirectory";

            // Act
            Action act = () => fileInfo.Copy();

            // Assert
            act.ShouldThrow<InvalidOperationException>();
        }

        [Fact]
        public void Should_throw_exception_in_full_trust_when_probing_directory_does_not_exist()
        {
            // Arrange
            var fileInfo = PackageFileInfoFactory.CreateFullTrustInstance();

            fileInfo.ProbingDirectory = "SomeDirectory";

            // Act
            Action act = () => fileInfo.Copy();

            // Assert
            act.ShouldThrow<InvalidOperationException>();
        }

        [Fact]
        public void Should_throw_exception_when_probing_directory_is_null_or_empty()
        {
            // Arrange
            var fileInfo = PackageFileInfoFactory.CreateFullTrustInstance();

            // Act
            Action act = () => fileInfo.Copy();

            // Assert
            act.ShouldThrow<InvalidOperationException>();
        }
    }
}

这有助于我保持单元测试干净并专注于测试,我只是想知道其他人如何处理这个问题以及他们在做什么来保持测试干净。

更新:

作为对 Adronius 的回应,我更新了我的帖子,其中包含了我旨在减少这些工厂的原型。

一个主要问题是在我的测试中使用完全相同的语法来创建实例并减少工厂类的数量。

namespace EasyFront.Framework.Factories
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics.Contracts;

    using EasyFront.Framework.Diagnostics.Contracts;

    /// <summary>
    ///     Provides a container to objects that enable you to setup them and resolve instances by type.
    /// </summary>
    /// <remarks>
    ///     Eyal Shilony, 20/07/2012.
    /// </remarks>
    public class ObjectContainer
    {
        private readonly Dictionary<string, object> _registeredTypes;

        public ObjectContainer()
        {
            _registeredTypes = new Dictionary<string, object>();
        }

        public TResult Resolve<TResult>()
        {
            string keyAsString = typeof(TResult).FullName;

            return Resolve<TResult>(keyAsString);
        }

        public void AddDelegate<TResult>(Func<TResult> func)
        {
            Contract.Requires(func != null);

            Add(typeof(TResult).FullName, func);
        }

        protected virtual TResult Resolve<TResult>(string key)
        {
            Contract.Requires(!string.IsNullOrEmpty(key));

            if (ContainsKey(key))
            {
                Func<TResult> func = GetValue<Func<TResult>>(key);

                Assert.NotNull(func);

                return func();
            }

            ThrowWheNotFound<TResult>();

            return default(TResult);
        }

        protected void Add<T>(string key, T value) where T : class
        {
            Contract.Requires(!string.IsNullOrEmpty(key));

            _registeredTypes.Add(key, value);
        }

        protected bool ContainsKey(string key)
        {
            Contract.Requires(!string.IsNullOrEmpty(key));

            return _registeredTypes.ContainsKey(key);
        }

        protected T GetValue<T>(string key) where T : class
        {
            Contract.Requires(!string.IsNullOrEmpty(key));

            return _registeredTypes[key] as T;
        }

        protected void ThrowWheNotFound<TResult>()
        {
            throw new InvalidOperationException(string.Format("The type '{0}' was not found in type '{1}'.", typeof(TResult).FullName, GetType().ReflectedType.FullName));
        }

        [ContractInvariantMethod]
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Required for code contracts.")]
        private void ObjectInvariant()
        {
            Contract.Invariant(_registeredTypes != null);
        }

    }
}

然后我可以扩展它以采用这样的参数。

namespace EasyFront.Framework.Factories
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics.Contracts;

    using EasyFront.Framework.Diagnostics.Contracts;

    public class ObjectContainer<T> : ObjectContainer
    {
        public TResult Resolve<TResult>(T key = default(T))
        {
            string keyAsString = EqualityComparer<T>.Default.Equals(key, default(T)) ? typeof(TResult).FullName : GetKey(key);

            Assert.NotNullOrEmpty(keyAsString);

            return Resolve<TResult>(keyAsString);
        }

        public void AddDelegate<TReturn>(T key, Func<T, TReturn> func)
        {
            Contract.Requires(func != null);

            Add(GetKey(key), func);
        }

        protected override TResult Resolve<TResult>(string key)
        {
            if (ContainsKey(key))
            {
                Func<TResult> func = GetValue<Func<TResult>>(key);

                Assert.NotNull(func);

                return func();
            }

            throw new InvalidOperationException(string.Format("The type '{0}' was not setup for type '{1}'.", typeof(TResult).FullName, GetType().ReflectedType.FullName));
        }

        /// <summary> Gets the full name of the type and the hash code as the key. </summary>
        /// <remarks> Eyal Shilony, 20/07/2012. </remarks>
        /// <param name="value"> The value to use to get key. </param>
        /// <returns> The full name of the type and the hash code as the key. </returns>
        private static string GetKey(T value)
        {
            Contract.Ensures(!string.IsNullOrEmpty(Contract.Result<string>()));

            string key = value.GetType().FullName + "#" + value.ToString().GetHashCode();

            Assert.NotNullOrEmpty(key);

            return key;
        }
    }
}

实现是这样的。

namespace EasyFront.Tests.Factories
{
    using System.Web;

    using EasyFront.Framework.Factories;
    using EasyFront.Framework.Web.Hosting.Packages;

    using Moq;
    using Moq.Protected;

    public class PackageFileInfoFactory : IObjectFactory<AspNetHostingPermissionLevel>
    {
        private const string FILE_NAME = "testfile.temp";

        private readonly ObjectContainer<AspNetHostingPermissionLevel> _container;

        public PackageFileInfoFactory()
        {
            _container = new ObjectContainer<AspNetHostingPermissionLevel>();

            _container.AddDelegate(AspNetHostingPermissionLevel.Unrestricted, value =>
            {
                var mockedFileInfo = CreateMockedInstance(value);

                return mockedFileInfo.Object;
            });

            _container.AddDelegate(AspNetHostingPermissionLevel.Medium, value =>
            {
                var mockedFileInfo = CreateMockedInstance(value);

                return mockedFileInfo.Object;
            });
        }

        public TResult CreateInstance<TResult>(AspNetHostingPermissionLevel first)
        {
            return _container.Resolve<TResult>(first);
        }

        private static Mock<PackageFileInfo> CreateMockedInstance(AspNetHostingPermissionLevel trustLevel)
        {
            var mockedFileInfo = new Mock<PackageFileInfo>(FILE_NAME);

            mockedFileInfo.Protected().SetupGet<AspNetHostingPermissionLevel>("TrustLevel").Returns(() => trustLevel);

            mockedFileInfo.Protected().Setup<string>("CopyTo", ItExpr.IsAny<string>()).Returns<string>(destFileName => "Some Unknown Path");

            return mockedFileInfo;
        }
    }
}

终于可以这样使用了。

namespace EasyFront.Framework.Web.Hosting.Packages
{
    using System;
    using System.Web;

    using EasyFront.Tests.Factories;

    using FluentAssertions;

    using global::Xunit;

    public class PackageFileInfoTest
    {
        public class Copy
        {
            private readonly PackageFileInfoFactory _factory;

            public Copy()
            {
                _factory = new PackageFileInfoFactory();
            }

            [Fact]
            public void Should_throw_exception_in_medium_trust_when_probing_directory_does_not_exist()
            {
                // Arrange
                var fileInfo = _factory.CreateInstance<PackageFileInfo>(AspNetHostingPermissionLevel.Medium);

                fileInfo.ProbingDirectory = "SomeDirectory";

                // Act
                Action act = () => fileInfo.Copy();

                // Assert
                act.ShouldThrow<InvalidOperationException>();
            }

            [Fact]
            public void Should_throw_exception_in_full_trust_when_probing_directory_does_not_exist()
            {
                // Arrange
                var fileInfo = _factory.CreateInstance<PackageFileInfo>(AspNetHostingPermissionLevel.Unrestricted);

                fileInfo.ProbingDirectory = "SomeDirectory";

                // Act
                Action act = () => fileInfo.Copy();

                // Assert
                act.ShouldThrow<InvalidOperationException>();
            }

            [Fact]
            public void Should_throw_exception_when_probing_directory_is_null_or_empty()
            {
                // Arrange
                var fileInfo = _factory.CreateInstance<PackageFileInfo>(AspNetHostingPermissionLevel.Unrestricted);

                // Act
                Action act = () => fileInfo.Copy();

                // Assert
                act.ShouldThrow<InvalidOperationException>();
            }
        }
    }
}

我不知道它是否有效,这只是我为帖子提出的一个概念,以证明我的观点,我想知道人们对此有何看法?如果您有任何建议或任何事情,我会很高兴听到的。

我不喜欢重新发明轮子,所以如果你有更好的方法,我也想听听。 :)

【问题讨论】:

    标签: unit-testing maintainability factories


    【解决方案1】:

    我无法在 2 分钟内理解您更新中的 test-factory-class-framework,因此我认为它不会使维护测试变得更容易。

    不过,我喜欢您拥有集中式测试数据生成器的概念。

    在我看来,在大多数情况下,每种类型一个 test-data-factory-method 就足够了。此方法定义类型的标准测试数据。

    实际测试将差异分配给标准测试数据。

    简单示例:

    public class PackageFileInfoTest
    {
        public class Copy
        {
            [Fact]
            public void Should_throw_exception_when_probing_directory_does_not_exist()
            {
                // Arrange
                var fileInfo = TestData.CreatePackageFileInfo();
                fileInfo.ProbingDirectory = "/Some/Directory/That/Does/Not/Exist";
                // Act
                Action act = () => fileInfo.Copy();
    
                // Assert
                act.ShouldThrow<InvalidOperationException>();
            }
    

    注意:“不存在的目录测试”应该独立于“权限级别”。所以工厂的标准许可应该足够了。

    当涉及多个类型时,每个类型一个工厂方法的更复杂示例:

            [Fact]
            public void Should_throw_exception_in_low_trust_when_writing_to_protected_directory()
            {
                // Arrange
                var protectedDirectory = TestData.CreateDirectory();
                protectedDirectory.MinimalPermissionRequired = AspNetHostingPermissionLevel.Full;
    
                var currentUser= TestData.CreateUser();
                currentUser.TrustLevel = AspNetHostingPermissionLevel.Low;
    
                // Act
                Action act = () => FileUploadService.CopyTo(protectedDirectory);
    
                ....
        }
    }
    

    在 Dotnet 世界中,nbuilder 可以帮助您填充测试数据数组。

    【讨论】:

    • 不知道 NBuilder,这似乎很有用,虽然我不认为它给了我相同的功能,但我可能是错的。你能告诉我你不明白的地方吗?类的用法或实现?实际的复制方法完全独立于文件系统,我必须提供信任级别的原因是因为 PackageFileInfo 对于每个信任级别的行为不同。
    • 感谢 NBuilder,尽管它对我非常有用,如果我可以投票,我会投票,就为了这个。 :)
    • 我使用的经验法则是“如果我需要超过 2 分钟的时间来理解某些实用方法/类的好处、处理和/或含义,它可能太复杂了 (codesmell) 。”我只是没有花费超过这 2 分钟的时间来找出“旨在减少这些工厂”以及代码的确切含​​义。这并不一定意味着您的解决方案很糟糕。理解 DependencyInjection 和 InversionOfControl 花了我几个小时,我喜欢这些 ....
    • 好吧,我想我可以做得更好,因为这只是我在几分钟内对我想要实现的目标所做的概念证明。感谢您的反馈,我非常感谢您的宝贵时间。 :)
    • 虽然我不同意 2 分钟规则,因为我认为它非常主观,但封装的想法隐藏了细节,你不应该太深入研究我是如何创建实例的,如果你告诉我理解测试类本身需要超过 2 分钟,那么我会同意这个规则,但是,你让我意识到我使用的方法不是那么直观,我同意.
    【解决方案2】:

    我在单元测试中使用了几乎相同的方法。

    如果我在一个测试夹具类中有重复,我将大部分创建(而不是初始化)模拟(双打)放入 setUp 方法。

    但是几个测试夹具类之间仍然存在重复。所以对于那些重复我使用“TestDoubleFactory”静态类,看起来和你的很相似,除了我没有创建实例的方法,但我总是只创建模拟对象,所以我可以修改(设置)它们在进一步的测试中。

    【讨论】:

    • 我正在尝试删除这些工厂,我的主要问题之一是我没有获得相同的语法来解析实例,所以我想以相同的方式创建实例。我想过使用 IoC,但我真的不需要大锤来破解坚果,所以我想出了一个想法,使用一个包含委托的轻量级容器,我将使用我的原型更新我的帖子。
    猜你喜欢
    • 2011-08-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-02-24
    • 2011-02-21
    • 1970-01-01
    • 1970-01-01
    • 2017-10-07
    相关资源
    最近更新 更多