单元测试可维护性和工厂

时间:2012-07-19 23:33:15

标签: unit-testing maintainability factories

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

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

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

我不知道它是否正常工作它只是我为帖子所做的一个概念来证明我的观点,我想知道人们怎么想?如果您有任何建议或任何我很高兴听到它。

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

2 个答案:

答案 0 :(得分:0)

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

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

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

答案 1 :(得分:0)

我无法在2分钟内了解您的更新中的测试工厂级框架,因此我认为它不会使测试更容易。

但是我喜欢你有一个集中式testdatagenerator的概念。

在我看来,在大多数情况下,每种类型的测试数据工厂方法应该足够了。此方法定义类型的标准Testdata。

实际测试将差异分配给standard-testdata。

简单的考试:

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

注意:&#34;非现有目录测试&#34;应该独立于&#34;权限级别&#34;。因此,工厂的标准许可应该是enaugh。

当涉及多种类型时,每种类型的一种工厂方法的更复杂的检验:

        [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-world nbuilder中可以帮助您填充testdata数组。