單元測試和集成測試業務應用程序

                                                                   20160509單元測試和集成測試業務應用程序--翻譯

單元測試,集成測試,測試驅動開發等介紹

     1.單元測試是寫代碼來測試代碼的一個小單位的方式。  在一個單元測試中,一次只能測試一個類。 例如,如果您正在測試一個使用類File ,而另一個測試類DummyFile是用來消除對物理文件系統的需要。 單元測試的測試類邏輯應該被測試。 爲了使類從真正落實給虛/存根實現切換,接口代替真正的類的使用。 所以,你的類應使用IFile或IDatabase而不是直接使用File和Database。  

        2.集成測試,也叫組裝測試或聯合測試。在單元測試的基礎上,將所有模塊按照設計要求(如根據結構圖)組裝成爲子系統或系統,進行集成測試。 例如,假設您正在測試CustomerData ,它依賴於LINQ to SQL數據庫連接。

        測試驅動開發(TDD)是單元測試的極端形式。 總的原則是先寫單元測試,然後編寫實際的代碼。 例如,先寫單元測試,測試CustomerData與在沒有真正的代碼類CustomerData在頭等艙。該CustomerData類可能包含類似功能InsertCustomer , DeleteCustomer , GetCustomer等,它們做的不外乎返回一些虛擬Customer對象,以滿足單元測試。 一旦單元測試都與虛擬數據相連,然後你開始寫的CustomerData實際代碼,它會訪問數據庫,做真正的處理。 編寫真正的代碼後,你的單元測試應該沒有改變測試代碼。 TDD也要求類被設計在沒有直接類依賴於其他類中。 所有的依賴關係通過接口。 例如, CustomerData不直接使用SqlConnection,而是使用ISqlConnection 。
例如,如果你正在測試小號Stack的Pop方法,單一的單元測試方法只測試一個期望Pop的方法。 Pop應該返回的最後一個項目,所以傳統的單元測試方法可以寫成:
public void Pop_Should_Return_The_Last_Pushed_Item() 

  Stack stack = new Stack(); 
  stack.Push(1); 
  Assert.Equal(1, stack.Pop()); 


        然而,測試的單一方法用於單個期望是麻煩的。 你必須寫太多的測試方法來測試每個方法的整體行爲。 此外,在每個測試方法,你必須在適當環境下建立測試類,只是爲了驗證一個特定的期望方式。 這篇文章中使用xUnit和Subspec,這是xUnit擴展單元測試。
例如,
給定一個空Stack 當一個項目被壓入堆棧和Pop被稱爲對象然後壓入堆棧中的最後一項被返回時,該項目從堆棧中移除,以及任何額外調用Pop拋出異常。在這裏,您定義的完整的行爲Pop的方法。 測試這種行爲的方法的所有預期和相關行爲Pop 。使用BDD單元測試
在第一個例子中,我們將進行單元測試數據訪問層。 使用LINQ到SQL對象持久化數據訪問層交易緩存在實體層面。 例如,當你要加載一個用戶,它首先檢查高速緩存,看看用戶是否已經緩存,如果沒有,它從數據庫中加載用戶,然後緩存它。我們來看看PageRepository ,其中所涉及Page實體持久性。共同創建,讀取,更新和銷燬(CRUD)方法。 
舉一個例子方法GetPageById ,需要一個PageId並加載該Page從數據庫中。
public class PageRepository : IPageRepository
{
    #region Fields
    private readonly IDropthingsDataContext _database;
    private readonly ICache _cacheResolver;

    #endregion Fields

    #region Constructors
    public PageRepository(IDropthingsDataContext database, ICache cacheResolver)
    {
        this._database = database;
        this._cacheResolver = cacheResolver;
    }
    #endregion Constructors

    #region Methods
    public Page GetPageById(int pageId)
    {
        string cacheKey = CacheSetup.CacheKeys.PageId(pageId);
        object cachedPage = _cacheResolver.Get(cacheKey);
        if (null == cachedPage)
        {
            var page = _database.GetSingle<Page, int>(
                   DropthingsDataContext.SubsystemEnum.Page, 
                    pageId, 
                    LinqQueries.CompiledQuery_GetPageById);
            page.Detach();
            _cacheResolver.Add(cacheKey, page);
            return page;
        }
        else
        {
            return cachedPage as Page;
        }
    } 
        PageRepository需要IDropthingsDataContext ,這是可測試與LINQ to SQL的一個單位DataContext 。 默認情況下,LINQ到SQL不會生成DataContext就是單元測試。 你將不得不嘗試做一個DataContext單元測試。 接着,它需要一個ICache其是與緩存涉及的接口。 
在這個例子中,假設有一個名爲類EnterpriseLibraryCache它實現ICache 。
讓我們測試一下。 我們將確保: 
鑑於新PageRepository和一個空的緩存, 
當 GetPageById是帶一個PageId , 

然後,它首先檢查緩存。 如果發現沒有,它加載從數據庫中的網頁,並預期返回頁面。上面的語句是一個行爲。 

讓我們使用代碼xUnit和Subspec :

public void GetPage_Should_Return_A_Page_from_cache_when_it_is_already_cached()
{
    var cache = new Mock<ICache>();
    var database = new Mock<IDropthingsDataContext>();
    IPageRepository pageRepository = new PageRepository(database.Object, cache.Object);
    const int pageId = 1;
    var page = default(Page);
    var samplePage = new Page() { ID = pageId, Title = "Test Page",
            ColumnCount = 3, LayoutType = 3, UserId = Guid.Empty, VersionNo = 1,
            PageType = Enumerations.PageTypeEnum.PersonalPage,
            CreatedDate = DateTime.Now };
    "Given PageRepository and the requested page in cache".Context(() =>
    {
        cache.Expect(c => c.Get(CacheSetup.CacheKeys.PageId(samplePage.ID)))
            .Returns(samplePage);
    });
    "when GetPageById is called".Do(() =>
        page = pageRepository.GetPageById(1));            
    "it checks in the cache first and finds the object is in cache".Assert(() => 
    {
        cache.VerifyAll();
    });

單元測試的意義何在?

        我覺得寫單元時,所測試的方法不只是在調用測試方法。單元測試已經確切地知道什麼其他的類和方法將被調用。在上面的例子中,是否使用cache或database在方法中決定的,所以,可以進行邏輯測試。在這樣的情況下,單元測試一定道理。如果您還不是服氣,考慮這樣一個場景,這些單元測試會挽救你的生命的一個例子。 比方說,你改緩存數據訪問層中所做的那樣。 例如,我改變了我的代碼來使用AspectF庫。 這需要對代碼變更PageRepository 。 更改代碼後,我需要確保PageRepository還是做事按照預期的行爲。 不管我用什麼方法的緩存,它不應該改變緩存行爲:檢查緩存,以確保所請求的對象是不是已經在緩存中,然後從數據庫中加載並緩存它。 該改變GetPageById方法,實施AspectF後,如下所示:
    "it returns the page as expected".Assert(() =>
    {
        Assert.Equal<int>(pageId, page.ID);
    });
}

       這裏AspectF正在做什麼,但讓我緩存的生活更輕鬆。 現在,當我運行單元測試,它通過罰款。

       它確認的行爲PageRepository沒有改變,儘管它的代碼急劇變化。有了正確的單元測試,即使你在代碼中急劇變化的信心,只要你的單元測試全部通過,你的系統是沒有問題。接下來讓我們來測試,當緩存滿了,它正確地從緩存中返回一個對象,而不是不必要的查詢數據庫。 下面的試驗將確保:

public void GetPage_Should_Return_A_Page_from_cache_when_it_is_already_cached()
{
    var cache = new Mock<ICache>();
    var database = new Mock<IDropthingsDataContext>();
    IPageRepository pageRepository = new PageRepository(database.Object, cache.Object);
    const int pageId = 1;
    var page = default(Page);
    var samplePage = new Page() { ID = pageId, Title = "Test Page",
            ColumnCount = 3, LayoutType = 3, UserId = Guid.Empty, VersionNo = 1,
            PageType = Enumerations.PageTypeEnum.PersonalPage,
            CreatedDate = DateTime.Now };
    "Given PageRepository and the requested page in cache".Context(() =>
    {
        cache.Expect(c => c.Get(CacheSetup.CacheKeys.PageId(samplePage.ID)))
            .Returns(samplePage);
    });
    "when GetPageById is called".Do(() =>
        page = pageRepository.GetPageById(1));            
    "it checks in the cache first and finds the object is in cache".Assert(() => 
    {
        cache.VerifyAll();
    });
    "it returns the page as expected".Assert(() =>
    {
        Assert.Equal<int>(pageId, page.ID);
    });
}

       這個試驗是很簡單的。 唯一的區別是在設置Context ,我們設定一個期望,從緩存請求特定的頁面時,它將返回samplePage對象。 Mock將拋出一個異常,只要其中任何被調用函數中有沒有期望設置,如果代碼試圖調用上的任何database對象或任何東西上的其他cache對象時,它會拋出一個異常,從而表明它沒有做什麼不應該做。 


集成測試使用BDD
       集成測試意味着你要測試的一些類,而它與其它類和基礎設施集成,如數據庫,文件系統,郵件服務器等,當你寫一個集成測試,測試組件的行爲應該是沒有任何實物模型。 此外,它們提供額外的信心代碼工作,因爲所有必需的組件和依賴關係也被測試。如何測試業務外觀層,業務外觀處理數據訪問組件和所有其他實用程序組件的編排。它封裝了用戶操作爲一體的商業運作。 例如,在Dropthings ,當第一次全新的用戶訪問,用戶獲得創建默認的頁面和窗口小部件。這些頁面和小部件來自一個模板。有一個名爲[email protected]的用戶擁有默認的頁面和窗口小部件。 特定用戶的頁面和窗口小部件被複制到每一個新用戶中。由於這是一個複雜的操作,適合做自動化的集成測試。當用戶首次訪問該Default.aspx,該FirstVisitHomePage是呼籲Facade 。 它通過一個複雜的過程來克隆模板頁面和小部件和設置默認用戶設置等集成測試,將確保如果FirstVisitHomePage被調用參數標識一個新的用戶訪問的站點,那麼它將返回可以對用戶創建的默認頁面和部件的一個對象。 因此: 由於之前從來沒有誰訪問過該網站的匿名用戶, 當用戶第一次訪問, 然後在準確的列和位置作爲anon_user的網頁新創建的頁面創建的小部件。
public class TestUserVisit
{
  public TestUserVisit()
  {
    Facade.BootStrap();
  }
  /// <summary>
  /// Ensure the first visit produces the pages and widgets defined in the template user
  /// </summary>
  [Specification]
  public void First_visit_should_create_same_pages_and_widgets_as_the_template_user()
  {
    MembershipHelper.UsingNewAnonUser((profile) =>
    {
      using (var facade = new Facade(new AppContext(string.Empty, profile.UserName)))
      {
        UserSetup userVisitModel = null;
        // Load the anonymous user pages and widgets
        string anonUserName = facade.GetUserSettingTemplate()
             .AnonUserSettingTemplate.UserName;
        var anonPages = facade.GetPagesOfUser(facade.GetUserGuidFromUserName(anonUserName));
        "Given anonymous user who has never visited the site before"
           .Context(() => { });
        "when the user visits for the first time".Do(() =>
        {
          userVisitModel = facade.FirstVisitHomePage(profile.UserName,
             string.Empty, true, false);

        });

        "it creates widgets on the newly created page at exact columns and
         positions as the anon user's pages".Assert(() =>
        {
          anonPages.Each(anonPage =>
          {
            var userPage = userVisitModel.UserPages.First(page =>
                    page.Title == anonPage.Title
                    && page.OrderNo == anonPage.OrderNo
                    && page.PageType == anonPage.PageType);
            facade.GetColumnsInPage(anonPage.ID).Each(anonColumn =>
            {
              var userColumns = facade.GetColumnsInPage(userPage.ID);
              var userColumn = userColumns.First(column =>
                      column.ColumnNo == anonColumn.ColumnNo);
              var anonColumnWidgets = 
                facade.GetWidgetInstancesInZoneWithWidget(anonColumn.WidgetZoneId);
              var userColumnWidgets = 
                facade.GetWidgetInstancesInZoneWithWidget(userColumn.WidgetZoneId);
              // Ensure the widgets from the anonymous user template's columns are 
              // in the same column and row.
              anonColumnWidgets.Each(anonWidget =>
                 Assert.True(userColumnWidgets.Where(userWidget =>
                  userWidget.Title == anonWidget.Title
                  && userWidget.Expanded == anonWidget.Expanded
                  && userWidget.State == anonWidget.State
                  && userWidget.Resized == anonWidget.Resized
                  && userWidget.Height == anonWidget.Height
                  && userWidget.OrderNo == anonWidget.OrderNo).Count() == 1));
            });
          });
        });
      }
    });
  }
       需要進一步的解釋:爲從模板用戶發現每個頁面確保新用戶獲得完全一樣的頁面獲得從模板用戶的頁面的部件。獲得來自新用戶的頁面的窗口小部件比較每個插件。 每當我在做業務層的變化對於每個插件確保具有相同的名稱,狀態,位置等獨一無二的部件,我可以運行集成測試,以確保關鍵功能是否按預期工作。  
該報告使用下面的命令行產生:
d:\xunit\xunit.console.exe 
 d:\trunk\src\Dropthings.Business.Facade.Tests\bin\Debug\Dropthings.Business.Facade.Tests.dll
    /html FacadeTest.html

使用BDD的單元測試測試驅動開發
        到目前爲止,我們已經通過代碼編寫測試,但如果先代碼編寫測試有關驅動開發? 假設我們要添加行爲:給定一個PageRepository ,當 Insert被調用時,那麼它應該在數據庫中插入頁面,清除了誰得到新的頁面,用戶頁面任何緩存集合,返回新插入的頁面。
讓我們先來編寫測試。
public void InsertPage_should_insert_a_page_in_database_and_cache_it()
{
  var cache = new Mock<ICache>();
  var database = new Mock<IDropthingsDataContext>();
  IPageRepository pageRepository = new PageRepository(database.Object, cache.Object);
  const int pageId = 1;
  var page = default(Page);
  var samplePage = new Page() { ID = pageId, Title = "Test Page", ColumnCount = 3, 
    LayoutType = 3, UserId = Guid.NewGuid(), VersionNo = 1, 
    PageType = Enumerations.PageTypeEnum.PersonalPage, CreatedDate = DateTime.Now };
  database
      .Expect<Page>(d => d.Insert<Page>(DropthingsDataContext.SubsystemEnum.Page,
          It.IsAny<Action<Page>>()))
      .Returns(samplePage);
  "Given PageRepository".Context(() =>
  {
    // It will clear items from cache
    cache.Expect(c => c.Remove(CacheSetup.CacheKeys.PagesOfUser(samplePage.UserId)));
  });
  "when Insert is called".Do(() =>
      page = pageRepository.Insert((newPage) =>
      {
        newPage.Title = samplePage.Title;
        newPage.ColumnCount = samplePage.ColumnCount;
        newPage.LayoutType = samplePage.LayoutType;
        newPage.UserId = samplePage.UserId;
        newPage.VersionNo = samplePage.VersionNo;
        newPage.PageType = samplePage.PageType;
      }));
  ("then it should insert the page in database" +
  "and clear any cached collection of pages for the user who gets the new page" +
  "and it returns the newly inserted page").Assert(() =>
  {
    database.VerifyAll();
    cache.VerifyAll();
    Assert.Equal<int>(pageId, page.ID);
  });      
}
      首先,我們將寫在一些虛擬代碼PageRepository.Insert方法,返回一個新的Page。它應該會失敗,因爲它不會滿足目前數據庫對象的期望集。如果沒有失敗,我們的測試是錯誤的。
public Page Insert(Action<Page> populate)
        {
                    return new Page();
        } 
運行故障測試結果如預期:
TestCase 'Given PageRepository when InsertPage is called, then it should insert the 
page in databaseand clear any cached collection of pages for the user who gets the 
new pageand it returns the newly inserted page'
failed: Moq.MockVerificationException : The following expectations were not met:
IDropthingsDataContext d => d.Insert(Page, null)
    at Moq.Mock`1.VerifyAll()
    PageRepositoryTest.cs(278,0): at 
Dropthings.DataAccess.UnitTest.PageRepositoryTest.<>c__DisplayClass35.
<InsertPage_should_insert_a_page_in_database_and_cache_it>b__34()
這表明,沒有呼叫database.Insert ,所以測試失敗。 我們實現了TDD的第一支柱,這是寫一個測試並使其失敗以來的第一期望沒有正確組件下檢驗。
現在讓我們添加真正的代碼:
public Page Insert(Action<Page> populate)
       {
           var newPage = _database.Insert<Page>(
               DropthingsDataContext.SubsystemEnum.Page, populate);
           RemoveUserPagesCollection(newPage.UserId);
           return newPage.Detach();
       }

 
使用BDD進行集成測試測試驅動開發

       如果我們想爲集成測試做TDD? 我們如何先寫測試代碼,然後寫它與其他組件集成業務層的代碼? 我們如何爲Web層做TDD?首先你編寫測試代碼,給出正確的輸入,並期望從經營輸出的權利,然而,集成測試不應該只調用單獨一個業務操作,以確保它能正常工作,沒有拋出異常。 集成測試還應該確保執行其他操作時出現正確的行爲。例如,在測試FirstVisitHomePage 時,期望的是,第一次訪問之後,用戶具有創建的正確頁面。測試代碼通過檢查返回的對象模型驗證這一點,但實際情況是,在第一次訪問後,根據用戶的返回,他們應該看到相同的部件。 再次確認第一和複診返回相同的數據。

應該測試如下

public void Revisit_should_load_the_pages_and_widgets_exactly_the_same()
{
  MembershipHelper.UsingNewAnonUser((profile) =>
  {
    using (var facade = new Facade(new AppContext(string.Empty, profile.UserName)))
    {
      UserSetup userVisitModel = null;
      UserSetup userRevisitModel = null;
      "Given an anonymous user who visited first".Context(() =>
      {
        userVisitModel = facade.FirstVisitHomePage(profile.UserName, ...);
      });
      "when the same user visits again".Do(() =>
      {
        userRevisitModel = facade.RepeatVisitHomePage(profile.UserName, ...);
      });
      "it should load the exact same pages, column and
         widgets as the first visit produced".Assert(() =>
      {
        userVisitModel.UserPages.Each(firstVisitPage =>
        {
          Assert.True(userRevisitModel.UserPages.Exists(page =>
                    page.ID == firstVisitPage.ID));
          var revisitPage = userRevisitModel.UserPages.First(page =>
                     page.ID == firstVisitPage.ID);
          var revisitPageColumns = facade.GetColumnsInPage(revisitPage.ID);
          facade.GetColumnsInPage(firstVisitPage.ID).Each(firstVisitColumn =>
          {
            var revisitColumn = revisitPageColumns.First(column =>
                 column.ID == firstVisitColumn.ID);
            var firstVisitWidgets = facade
               .GetWidgetInstancesInZoneWithWidget(firstVisitColumn.WidgetZoneId);
            var revisitWidgets = facade
               .GetWidgetInstancesInZoneWithWidget(revisitColumn.WidgetZoneId);
            firstVisitWidgets.Each(firstVisitWidget =>
                Assert.True(revisitWidgets.Where(revisitWidget =>
                    revisitWidget.Id == firstVisitWidget.Id).Count() == 1));
          });
        });
      });
    }
  });
}

      做集成測試的正確方法是編寫單元測試的對立面。在單元測試中,這種方法是通過調用一種方法和存根。在集成測試,你應該測試不僅只有一個操作,而且還執行其他相關操作,以確保測試的操作確實是它應該做的。概括了可能的測試用例分爲以下類別:

     1.當測試創建新數據操作(例如,在數據庫中插入行或調用Web服務來創建一個實體),保證了操作通過適當進行:

                調用,通過再次讀取該行或調用另一個Web服務,以獲得創建的實體讀取數據等操作。 如果數據沒有被正確插入(例如,插入子行)應該失敗。 這是一個積極的測試。 

                調用如果插  入成功,例如再次插入同一行會產生一個違反約束,將失敗的其他操作。 這是一種消極的考驗。

     2.當測試的操作的更新數據(例如,更新數據庫中的行),保證了操作的數據,通過適當更新

               調用使用更新後的數據,如果沒有正確更新的數據會失敗,例如其他的操作使兩個連續的匯款那裏是餘額不足的賬戶首次匯款後)。 這是一個積極的測試。

               如果調用更新成功,將失敗的其它操作,例如嘗試使用更新後的相同值應產生約束衝突在數據庫中插入新行。 這是一種消極的考驗。

     3.當你測試刪除一些數據的操作,保證手術通過適當的數據刪除

                如果調用數據存在例如重新插入同一行產生一個違反約束將失敗,其他的操作。
                呼叫,如果數據被正確地刪除,例如插入的子行的不存在的行,將失敗的其他操作。
       在集成測試做正反兩方面的測試,即使你正在做單元測試,以確保測試涵蓋了系統的所有主要行爲是很重要的。集成測試的一個好處是在假設你自己的代碼已經單元測試的基礎設施不可預測性超過測試自己的代碼。重要的是儘可能多的從正和負兩方面儘可能覆蓋以排除基礎設施變量。


原文鏈接:http://www.codeproject.com/Articles/44276/Unit-Testing-and-Integration-Testing-in-Business-A&usg=ALkJrhhfabjxfW2ibo72JCesxwkeZDec8A

發佈了20 篇原創文章 · 獲贊 15 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章