ASP.NET 系列:單元測試

單元測試能夠有效的能夠在編碼、設計、調試到重構等多方面顯著提高咱們的工做效率和質量。github上可供參考和學習的各類開源項目衆多,NopCommerce、Orchard等以及微軟的asp.net mvc、entity framework相關多數項目均可以做爲學習單元測試的參考。單元測試之道(C#版本)、.NET單元測試藝術C#測試驅動開發都是不錯的學習資料。git

1.單元測試的好處

(1)單元測試幫助設計github

單元測試迫使咱們從關注實現轉向關注接口,編寫單元測試的過程就是設計接口的過程,使單元測試經過的過程是咱們編寫實現的過程。我一直以爲這是單元測試最重要的好處,讓咱們關注的重點放在接口上而非實現的細節。服務器

(2)單元測試幫助編碼mvc

應用單元測試會使咱們主動消除和減小沒必要要的耦合,雖然出發點多是爲了更方便的完成單元測試,但結果一般是類型的職責更加內聚,類型間的耦合顯著下降。這是已知的提高編碼質量的有效手段,也是提高開發人員編碼水平的有效手段。框架

(3)單元測試幫助調試asp.net

應用了單元測試的代碼在調試時能夠快速定位問題的出處。單元測試

(4)單元測試幫助重構學習

對於現有項目的重構,從編寫單元測試開始是更好的選擇。先從局部代碼進行重構,提取接口進行單元測試,而後再進行類型和層次級別的重構。測試

單元測試在設計、編碼和調試上的做用足以使其成爲軟件開發相關人員的必備技能。this

2.應用單元測試

單元測試不是簡單的瞭解使用相似XUnit和Moq這樣的測試和模擬框架就可使用了,首先必須對咱們要編寫的代碼有足夠的瞭解。一般咱們把代碼當作一些靜態的互相關聯的類型,類型之間的依賴使用接口,實現類實現接口,在運行時經過自定義工廠或使用依賴注入容器管理。一個單元測試一般是在一個方法中調用要測試的方法或屬性,經過使用Assert斷言對方法或屬性的運行結果進行檢測,一般咱們須要編寫的測試代碼有如下幾種。

(1)測試領域層

領域層由POCO組成,能夠直接測試領域模型的公開行爲和屬性。

(2)測試應用層

應用層主要由服務接口和實現組成,應用層對基礎設施組件的依賴以接口方式存在,這些基礎設施的接口經過Mock方式模擬。

(3)測試表示層

表示層對應用層的依賴表如今對服務接口的調用上,經過Mock方式獲取依賴接口的實例。

(4)測試基礎設施層

基礎設施層的測試一般涉及到配置文件、Log、HttpContext、SMTP等系統環境,一般須要使用Mock模式。

(5)使用單元測試進行集成測試

首先系統之間經過接口依賴,經過依賴注入容器獲取接口實例,在配置依賴時,已經實現的部分直接配置,僞實現的部分配置爲Mock框架生成的實例對象。隨着系統的不斷實現,不斷將依賴配置的Mock對象替換爲實現對象。

3.使用Assert判斷邏輯行爲正確性

Assert斷言類是單元測試框架中的核心類,在單元測試的方法中,經過Assert類的靜態方法對要測試的方法或屬性的運行結果進行校驗來判斷邏輯行爲是否正確,Should方法一般是以擴展方法形式提供的Assert的包裝。

(1)Assert斷言

若是你使用過System.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進行包裝也很容易,下面的代碼提取自 NopComnerce 3.70 中對NUnit的Assert的自定義擴展方法。

namespace Nop.Tests
{
    public static class TestExtensions
    {
        public static T ShouldNotNull<T>(this T obj)
        {
            Assert.IsNull(obj);
            return obj;
        }

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

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

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

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

        ///<summary>
        /// Asserts that two objects are equal.
        ///</summary>
        ///<param name="actual"></param>
        ///<param name="expected"></param>
        ///<param name="message"></param>
        ///<exception cref="AssertionException"></exception>
        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<T>(this object actual)
        {
            Assert.IsInstanceOf<T>(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<T>(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);
        }

        /// <summary>
        /// Compares the two strings (case-insensitive).
        /// </summary>
        /// <param name="actual"></param>
        /// <param name="expected"></param>
        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("sender@here.com", "receiver@there.com", "subject", "body");
    Assert.Equal(1, server.ReceivedEmailCount);
    SmtpMessage mail = (SmtpMessage)server.ReceivedEmail[0];
    Assert.Equal("sender@here.com", mail.Headers["From"]);
    Assert.Equal("receiver@there.com", 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)拒絕考慮效率、擴展性和解耦,只考慮數據和功能的實現。

相關文章
相關標籤/搜索