ASP.NETシリーズ:ユニットテスト

17145 ワード

ユニットテストは、符号化、設計、デバッグから再構築まで、さまざまな面で作業効率と品質を大幅に向上させるのに有効です.githubには参考と学習ができる様々なオープンソースプロジェクトが多く、NopCommerce、Orchardなど、マイクロソフトのasp.Netmvc、entity framework関連の多くの項目は学習ユニットテストの参考とすることができる.ユニットテストの道(C#バージョン),.NETユニットテストアートとC#テスト駆動開発は良い学習資料です.

1.ユニットテストのメリット


(1)ユニットテスト支援設計
ユニットテストは私たちに関心の実現から関心のインタフェースに転換させ、ユニットテストを書く過程はインタフェースを設計する過程であり、ユニットテストを通過させる過程は私たちが実現を書く過程である.私はずっとこれがユニットテストの最も重要なメリットだと思っています.実現の詳細ではなくインタフェースに重点を置いてみましょう.
(2)ユニットテスト支援符号化
ユニットテストを適用すると、不要な結合を自発的に除去し、減らすことができます.出発点は、ユニットテストをより容易に完了するためかもしれませんが、結果は通常、タイプの職責がより集約され、タイプ間の結合が著しく低下します.これは既知の符号化品質を向上させる有効な手段であり、開発者の符号化レベルを向上させる有効な手段でもある.
(3)ユニットテストヘルプデバッグ
ユニットテストを適用したコードは、デバッグ時に問題の出所を迅速に特定できます.
(4)ユニットテスト支援再構築
既存のプロジェクトの再構築には、作成ユニットテストから始めるとより良い選択です.ローカルコードから再構築し、インタフェースを抽出してユニットテストを行い、タイプと階層レベルの再構築を行います.
ユニットテストの設計、符号化、デバッグにおける役割は、ソフトウェア開発関係者の必須スキルになるのに十分です.

2.アプリケーションユニットテスト


ユニットテストは、XUnitやMoqのようなテストやシミュレーションフレームワークを使用すると簡単に理解できるものではありません.まず、作成するコードについて十分な理解が必要です.通常、コードは静的で相互に関連するタイプと見なされ、タイプ間の依存はインタフェースを使用し、クラス実装インタフェースを実現し、実行時にカスタムファクトリまたは依存注入コンテナを使用して管理されます.1つのユニットテストは、通常、1つのメソッドでテストするメソッドまたはプロパティを呼び出し、Assert断言を使用してメソッドまたはプロパティの実行結果を検出します.通常、作成する必要があるテストコードは以下のとおりです.
(1)試験領域層
レルム層はPOCOからなり,レルムモデルの公開挙動と特性を直接テストできる.
(2)テストアプリケーション層
アプリケーション層は主にサービスインタフェースと実装からなり,アプリケーション層のインフラストラクチャコンポーネントへの依存性はインタフェース方式で存在し,これらのインフラストラクチャのインタフェースはMock方式でシミュレーションされる.
(3)テスト表示層
アプリケーション層に対する層の依存性はサービスインタフェースの呼び出しに現れ,Mock方式で依存インタフェースのインスタンスを取得する.
(4)試験インフラ層
インフラストラクチャ層のテストは、通常、プロファイル、Log、HttpContext、SMTPなどのシステム環境に関連し、通常はMockモードを使用する必要があります.
(5)ユニットテストによる統合テスト
まずシステム間ではインタフェース依存,注入コンテナ依存によりインタフェースインスタンスを取得し,依存を構成する際に実装した部分は直接構成し,ダミー実装の部分はMockフレームワークで生成したインスタンスオブジェクトとして構成する.システムが実装されるにつれて、構成に依存するMockオブジェクトが実装オブジェクトに置き換えられる.

3.Assertを用いて論理行為の正しさを判断する


Assert断言クラスはユニットテストフレームワークのコアクラスであり、ユニットテストの方法では、テストする方法または属性の実行結果をAssertクラスの静的方法で検証することによって論理的挙動が正しいかどうかを判断し、Should方法は通常、拡張方法の形式で提供されるAssertのパッケージである.
(1)Assertアサーション
システムを使ったことがあるならDiagnostics.Contracts.ContractのAssertメソッドは、XUnitなどのユニットテストフレームワークで提供するAssert静的クラスに対してより容易であり、同様に条件判断であり、ユニットテストフレームワークでのAssertクラスにはAssertなどのより具体的なメソッドが多数提供されている.True、Assert.NotNull、Assert.Equalなどは条件判断や情報出力に便利です.
(2)Should拡張方法
Should拡張メソッドを使用すると、パラメータの使用が減少し、意味が強化されます.また、テストに失敗したときのヒントもより友好的に提供されます.Xunit.shouldは更新を停止し、ShouldコンポーネントはXunitのAssert実装を多重化したが、更新も停止した.Shouldlyコンポーネントは自己実装を使用しており、現在も更新されているプロジェクトであり、structuremapはユニットテストでShouldlyを使用している.Assertを手動で梱包するのも簡単で、次のコードはNopCommerce 3.70のNUnitのAssertに対するカスタム拡張方法から抽出されます.
namespace Nop.Tests
{
    public static class TestExtensions
    {
        public static T ShouldNotNull(this T obj)
        {
            Assert.IsNull(obj);
            return obj;
        }

        public static T ShouldNotNull(this T obj, string message)
        {
            Assert.IsNull(obj, message);
            return obj;
        }

        public static T ShouldNotBeNull(this T obj)
        {
            Assert.IsNotNull(obj);
            return obj;
        }

        public static T ShouldNotBeNull(this T obj, string message)
        {
            Assert.IsNotNull(obj, message);
            return obj;
        }

        public static T ShouldEqual(this T actual, object expected)
        {
            Assert.AreEqual(expected, actual);
            return actual;
        }

        ///
        /// Asserts that two objects are equal.
        ///
        ///
        ///
        ///
        ///
        public static void ShouldEqual(this object actual, object expected, string message)
        {
            Assert.AreEqual(expected, actual);
        }

        public static Exception ShouldBeThrownBy(this Type exceptionType, TestDelegate testDelegate)
        {
            return Assert.Throws(exceptionType, testDelegate);
        }

        public static void ShouldBe(this object actual)
        {
            Assert.IsInstanceOf(actual);
        }

        public static void ShouldBeNull(this object actual)
        {
            Assert.IsNull(actual);
        }

        public static void ShouldBeTheSameAs(this object actual, object expected)
        {
            Assert.AreSame(expected, actual);
        }

        public static void ShouldBeNotBeTheSameAs(this object actual, object expected)
        {
            Assert.AreNotSame(expected, actual);
        }

        public static T CastTo(this object source)
        {
            return (T)source;
        }

        public static void ShouldBeTrue(this bool source)
        {
            Assert.IsTrue(source);
        }

        public static void ShouldBeFalse(this bool source)
        {
            Assert.IsFalse(source);
        }

        /// 
        /// Compares the two strings (case-insensitive).
        /// 
        /// 
        /// 
        public static void AssertSameStringAs(this string actual, string expected)
        {
            if (!string.Equals(actual, expected, StringComparison.InvariantCultureIgnoreCase))
            {
                var message = string.Format("Expected {0} but was {1}", expected, actual);
                throw new AssertionException(message);
            }
        }
    }
}

4.アーティファクトの使用

伪对象可以解决要测试的代码中使用了无法测试的外部依赖问题,更重要的是通过接口抽象实现了低耦合。例如通过抽象IConfigurationManager接口来使用ConfigurationManager对象,看起来似乎只是为了单元测试而增加更多的代码,实际上我们通常不关心后去的配置是否是通过ConfigurationManager静态类读取的config文件,我们只关心配置的取值,此时使用IConfigurationManager既可以不依赖具体的ConfigurationManager类型,又可以在系统需要扩展时使用其他实现了IConfigurationManager接口的实现类。

使用伪对象解决外部依赖的主要步骤:

(1)使用接口依赖取代原始类型依赖。

(2)通过对原始类型的适配实现上述接口。

(3)手动创建用于单元测试的接口实现类或在单元测试时使用Mock框架生成接口的实例

手动创建的实现类完整的实现了接口,这样的实现类可以在多个测试中使用。可以选择使用Mock框架生成对应接口的实例,只需要对当前测试需要调用的方法进行模拟,通常需要根据参数进行逻辑判断,返回不同的结果。无论是手动实现的模拟类对象还是Mock生成的伪对象都称为桩对象,即Stub对象。Stub对象的本质是被测试类依赖接口的伪对象,它保证了被测试类可以被测试代码正常调用。

解决了被测试类的依赖问题,还需要解决无法直接在被测试方法上使用Assert断言的情况。此时我们需要在另一类伪对象上使用Assert,通常我们把Assert使用的模拟对象称为模拟对象,即Mock对象。Mock对象的本质是用来提供给Assert进行验证的,它保证了在无法直接使用断言时可以正常验证被测试类。

Stub和Mock对象都是伪对象,即Fake对象

Stub或Mock对象的区分明白了就很简单,从被测试类的角度讲Stub对象,从Assert的角度讲Mock对象。然而,即使不了解相关的含义和区别也不会在使用时产生问题。比如测试邮件发送,我们通常不能直接在被测试代码上应用Assert,我们会在模拟的STMP服务器对象上应用Assert判断是否成功接收到邮件,这个SMTPServer模拟对象就是Mock对象而不是Stub对象。比如写日志,我们通常可以直接在ILogger接口的相关方法上应用Assert判断是否成功,此时的Logger对象即是Stub对象也是Mock对象。

5.ユニットテスト常用フレームワークとコンポーネント

(1)单元测试框架。

XUnit是目前最为流行的.NET单元测试框架。NUnit出现的较早被广泛使用,如nopCommerce、Orchard等项目从开始就一直使用的是NUnit。XUnit目前是比NUnit更好的选择,从github上可以看到asp.net mvc等一系列的微软项目使用的就是XUnit框架。

(2)Mock框架

Moq是目前最为流行的Mock框架。Orchard、asp.net mvc等微软项目使用Moq。nopCommerce使用Rhino MocksNSubstitute和FakeItEasy是其他两种应用广泛的Mock框架。

(3)邮件发送的Mock组件netDumbster

可以通过nuget获取netDumbster组件,该组件提供了SimpleSmtpServer对象用于模拟邮件发送环境。

通常我们无法直接对邮件发送使用Assert,使用netDumbster我们可以对模拟服务器接收的邮件应用Assert。

public void SendMailTest()
{
    SimpleSmtpServer server = SimpleSmtpServer.Start(25);
    IEmailSender sender = new SMTPAdapter();
    sender.SendMail("[email protected]", "[email protected]", "subject", "body");
    Assert.Equal(1, server.ReceivedEmailCount);
    SmtpMessage mail = (SmtpMessage)server.ReceivedEmail[0];
    Assert.Equal("[email protected]", mail.Headers["From"]);
    Assert.Equal("[email protected]", mail.Headers["To"]);
    Assert.Equal("subject", mail.Headers["Subject"]);
    Assert.Equal("body", mail.MessageParts[0].BodyData);
    server.Stop();
}

(4)HttpContextのMockコンポーネントHttpSimulator
同様にnugetで取得し、HttpSimulatorオブジェクトを使用してHttpリクエストを開始することで、そのライフサイクル内にHttContextオブジェクトが使用可能になります.
HttpContextは閉じているためMoqシミュレーションは使用できません.通常、次のコード・スライスを使用します.
private HttpContext SetHttpContext()
{
    HttpRequest httpRequest = new HttpRequest("", "http://mySomething/", "");
    StringWriter stringWriter = new StringWriter();
    HttpResponse httpResponse = new HttpResponse(stringWriter);
    HttpContext httpContextMock = new HttpContext(httpRequest, httpResponse);
    HttpContext.Current = httpContextMock;
    return HttpContext.Current;
}

HttpSimulatorを使用すると、次のようにコードを簡略化できます.
using (HttpSimulator simulator = new HttpSimulator())
{
  
}

これはIoCコンテナとEntityFrameworkのプログラムを用いたDbContextライフサイクルのテストにとって非常に重要であり、DbContextのライフサイクルはHttpRequestと一致しなければならないため、IoCコンテナのライフサイクルのテストが必要である.

6.使用ユニットテストの難点


(1)学習コストと既存の開発習慣を変更したくない.
(2)思考の習慣がなく,誤ってユニットテストをフレームワーク学とする.
(3)プロジェクト後期にユニットテストを適用する.すなわち、ユニットテストのメリットが得られず、コードのテストが友好的でないため、ユニットテストに誤解が生じる.
(4)効率,拡張性,デカップリングの考慮を拒否し,データと機能の実現のみを考慮する.