单元测试和集成测试业务应用程序

作者: 商领云 | 来源:发表于2016-05-31 11:19 被阅读234次

本文主要通过小例子介绍下单元测试,集成测试,测试驱动开发等概念。

它确认行为PageRepository没有改变,尽管它的代码急剧变化。有了正确的单元测试,即使你在代码中改变了,只要你的单元测试全部通过,你的系统是没有问题。接下来让我们来测试,当缓存满了,它正确地从缓存中返回一个对象,而不是不必要的查询数据库。下面的试验将确保:

[Specification]
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 ,当第一次全新的用户访问,用户获得创建默认的页面和窗口小部件。这些页面和小部件来自一个模板。有一个名为anon_user@dropthings.com的用户拥有默认的页面和窗口小部件。特定用户的页面和窗口小部件被复制到每一个新用户中。由于这是一个复杂的操作,适合做自动化的集成测试。

当用户首次访问该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));
            });
          });
        });
      }
    });
  }

需要进一步的解释:为从模板用户发现每个页面确保新用户从模板用户的页面的部件获得完全一样的页面。获得来自新用户的页面的窗口小部件比较每个插件。当在做业务层的变化对于每个插件确保具有相同的名称,状态,位置等独一无二的部件,我可以运行集成测试,以确保关键功能是否按预期工作完成,而且在整个业务层没有破损任何地方。 我用xunit.console.exe上运行的集成测试测试并生成一个不错html报告:

该报告使用下面的命令行产生:
d:\xunit\xunit.console.exe
d:\trunk\src\Dropthings.Business.Facade.Tests\bin\Debug\Dropthings.Business.Facade.Tests.dll
/html FacadeTest.html

您可以使用GUI xUnit:

使用BDD的单元测试测试驱动开发
到目前为止,我们已经通过代码编写测试,但如果你先代码编写测试有关驱动开发?假设我们要添加行为:给定一个PageRepository ,当 Insert被调用时,它应该在数据库中插入页面,清除了得到的新页面,用户页面任何缓存集合,返回新插入的页面。
编写测试代码:

[Specification]
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。它应该会fail,因为它不满足目前数据库对象的期望集。如果没有失败,则表明我们的测试是错误的。


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

现在,当单元测试是对的PageRepository ,它通过新的测试,与以往的测试一起:


使用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));
          });
        });
      });
    }
  });
}

做集成测试的正确方法是编写单元测试的对立面。在单元测试中,这种方法是通过调用一种方法和存根。在集成测试,你应该测试不仅只有一个操作,而且还执行其它相关操作,以确保测试的操作确实是它应该做的。概括了可能的测试用例分为以下类别:

  • 当测试创建新数据操作(例如,在数据库中插入行或调用Web服务来创建一个实体),保证了操作通过适当进行:
    调用,通过再次读取该行或调用另一个Web服务,以获得创建的实体读取数据等操作。如果数据没有被正确插入(例如,插入子行)应该失败。 这是一个积极的测试。
    调用如果插入成功,例如再次插入同一行会产生一个违反约束,将失败的其它操作。这是一种消极的考验。
  • 当测试的操作的更新数据(例如,更新数据库中的行),保证了操作的数据,通过适当更新
    调用使用更新后的数据,如果没有正确的更新数据会失败,例如其它的操作使余额不足的账户两次连续的汇款后。这是一个积极的测试。
    如果调用更新成功,将是失败的其它操作,例如尝试使用更新后的相同值约束冲突在数据库中插入新行。这是一种消极的考验。
  • 当你测试删除一些数据的操作,保证适当的数据删除
    如果调用数据存在例如重新插入同一行产生一个违反约束将失败,其它的操作。
    呼叫,如果数据被正确地删除,例如插入的子行是不存在的行,将是失败的操作。

在集成测试做正反两方面的测试,即使你正在做单元测试,以确保测试涵盖了系统的所有主要行为是很重要的。集成测试的一个好处是在假设你自己的代码已经单元测试的基础设施不可预测性超过测试自己的代码。重要的是尽可能多的从正和负两方面尽可能覆盖以排除基础设施变量。

作者信息:

原文作者:Omar Al Zabir
原文链接:http://www.codeproject.com/Articles/44276/Unit-Testing-and-Integration-Testing-in-Business-A
翻译来与Maxleap团队_云服务研发成员:Sunny Zhang
中文翻译原链接:https://blog.maxleap.cn/archives/855
转载请标明作者信息一栏,及该栏自媒体公号。

相关文章

  • 单元测试和集成测试业务应用程序

    本文主要通过小例子介绍下单元测试,集成测试,测试驱动开发等概念。 切入正文: 单元测试是通过写代码来测试代码的一个...

  • Android测试

    单元测试: 本地单元测试 仪器单元测试集成测试: 测试不易交互的部件: service和contentProvid...

  • 前端面试日更解答 2020-03-10

    今天的知识点 (2020.03.10) 14.单元测试、集成测试、系统测试区别? 13.什么是单元测试和集成测试?...

  • 《徐昊-TDD项目实战70讲》学习笔记 -- Day 7

    07|TDD中的测试(3):集成测试还是单元测试? 集成测试还是单元测试? TDD 中的单元测试 在 TDD 的语...

  • 测试过程

    一、测试过程简介 单元测试 集成测试 系统测试 二、测试过程单元集成系统及比较 1.单元测试--函数 单元测试时针...

  • 软件测试系列一软件测试基础

    最关键价值 :业务知识、对客户的理解和把握、对客户的沟通单元测试 :开发人员完成集成测试 :主要测试接口项...

  • 软件测试读书笔记(佟伟光著)4

    软件测试过程 软件测试过程包括单元测试、集成测试、系统测试和验收测试。 单元测试 单元测试是对软件设计的最小单元—...

  • 软件测试各阶段的测试策略是什么?

    和开发过程相对应,测试过程会依次经历单元测试、集成测试、系统测试、验收测试四个主要阶段: 单元测试:单元测试是针对...

  • 套路深!软件测试各阶段的测试策略是什么?

    和开发过程相对应,测试过程会依次经历单元测试、集成测试、系统测试、验收测试四个主要阶段: 单元测试:单元测试是针对...

  • 软件测试的分类(一)

    按测试阶段来分类 单元测试,集成测试,系统测试,验收测试 单元测试1.什么是单元测试?对软件中的进行检查和验证。 ...

网友评论

    本文标题:单元测试和集成测试业务应用程序

    本文链接:https://www.haomeiwen.com/subject/zkvcdttx.html