1

編集:同じ方法を使用してインスタンスを作成するのは良いことですが、他のことを諦めて問題を複雑にしているため、アイデアを完全に捨てることにしました。

私の単体テストプロジェクトには、基本的に、以下のような単体テストのすべてのファクトリクラスを含むフォルダがあります。

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

これは、単体テストをクリーンに保ち、テストに集中するのに役立ちます。他の人がこれをどのように処理し、テストをクリーンに保つために何をしているのか疑問に思っています。

アップデート:

アドロニウスに応えて、私はこれらの工場を減らすことを目指しているもののプロトタイプで私の投稿を更新しました。

主な懸念事項は、インスタンスを作成し、ファクトリクラスの数を減らすために、テスト全体でまったく同じ構文を使用することです。

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

それが機能しているかどうかはわかりませんが、それは私の主張を示すために投稿のために作成した概念にすぎません。人々はそれについてどう思うのでしょうか。何か提案や何かがあれば、私はそれについて聞いてうれしいです。

私は車輪の再発明が好きではないので、あなたがより良いアプローチを持っているなら、私もそれについて聞きたいです。:)

4

2 に答える 2

0

2分以内にアップデートのtest-factory-class-frameworkを理解できなかったので、私の意見では、テストの保守が容易になるわけではありません。

しかし、私は一元化されたtestdatageneratorを持つというあなたの概念が好きです。

私の意見では、ほとんどの場合、タイプごとに1つの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>();
        }

注:「存在しないディレクトリテスト」は、「許可レベル」から独立している必要があります。したがって、工場の標準的な許可は十分でなければなりません。

複数のタイプが含まれる場合の、タイプごとに1つのファクトリメソッドのより複雑な例:

        [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はテストデータの配列を設定するのに役立ちます。

于 2012-07-21T06:00:33.987 に答える
0

単体テストでもほぼ同じアプローチを使用しました。

1 つのテスト フィクスチャ クラスに重複がある場合、モック (ダブル) の作成 (初期化ではない) のほとんどを setUp メソッドに入れます。

しかし、いくつかのテスト フィクスチャ クラスにはまだ重複があります。したがって、これらの重複に対しては、インスタンスを作成するメソッドがないことを除いて、あなたと似た「 TestDoubleFactory 」静的クラスを使用しますが、常にモックオブジェクトのみを作成するため、テストでそれらをさらに変更 (セットアップ) できます。

于 2012-07-20T18:45:36.670 に答える