切入正文:
单元测试是通过写代码来测试代码的一个小单位测试方式。在一个单元测试中,一次只能测试一个类。例如,如果你正在测试一个使用类File,而另一个测试类DummyFile是用来消除对物理文件系统的需要,单元测试的测试类逻辑应该被测试。为了使class实现从真实到虚/存根之间切换,接口代替真正的类使用。所以,你的类应使用IFile或IDatabase而不是直接使用File和Database。
集成测试,也叫组装测试或联合测试。在单元测试的基础上,将所有模块按照设计要求(如根据结构图)组装成为子系统或系统,进行集成测试。例如:正在测试CunsomerData,它依赖于LINQ to SQL数据库连接,在集成测试中,确保能正常调动所有相关类的方法。
测试驱动开发(TDD)是单元测试的极端形式。总的原则是先写单元测试,然后编写实际的代码。例如,先写单元测试,测试CustomerData与在没有真正的代码类CustomerData。该CustomerData类可能包含类似功能InsertCustomer ,DeleteCustomer , GetCustomer等,它们做的不外乎返回一些虚拟Customer对象,以满足单元测试。 一旦单元测试都与虚拟数据相连,然后你开始写的CustomerData实际代码,它会访问数据库做真正的处理。编写真正的代码后,单元测试并没有改变测试代码。TDD要求类被设计在没有直接依赖于其他类中。所有的依赖关系通过接口。例如,CustomerData不直接使用SqlConnection,而是使用ISqlConnection ,所有的依赖提供给CustomerData构造。
使用行为驱动开发测试

测试的单一方法用于单个期望是麻烦的。你必须写更多的测试方法来测试每个方法的整体行为。此外,在每个测试方法中,你必须在适当环境下建立测试类,只是为了验证一个特定的期望方式。
例如:给定一个空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 。它首先检查缓存,如果发现没有,它从数据库中的网页中加载,并返回预期页面。


public void GetPage_Should_Return_A_Page_from_database_when_cache_is_empty_and_then_caches_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", ...};
    database
        .Expect<Page>(d => d.GetSingle<Page, int>(
                              DropthingsDataContext.SubsystemEnum.Page,
                               1, LinqQueries.CompiledQuery_GetPageById))
        .Returns(samplePage);
    "Given PageRepository and empty cache".Context(() =>
        {
            // cache is empty
            cache.Expect(c => c.Get(It.IsAny<string>())).Returns(default(object));
            // It will cache the Page object afte loading from database
            cache.Expect(c =>
                 c.Add(It.Is<string>(cacheKey =>
                       cacheKey == CacheSetup.CacheKeys.PageId(pageId)),
                      It.Is<Page>(cachePage =>
                          object.ReferenceEquals(cachePage, samplePage))))
                .AtMostOnce().Verifiable();
        });
    "when GetPageById is called".Do(() =>
        page = pageRepository.GetPageById(1));
    "it checks in the cache first and finds nothing and then caches it".Assert(() =>
        cache.VerifyAll());
    "it loads the page from database".Assert(() =>
        database.VerifyAll());
    "it returns the page as expected".Assert(() =>
        {
            Assert.Equal<int>(pageId, page.ID);
        }); 
}
单元测试的意义何在?

我觉得写单元测试时,所测试的方法不只是在调用测试方法。单元测试已经确切地知道什么其它的类和方法将被调用。在上面的例子中,是否使用cache或database是在方法中决定的,所以,可以进行逻辑测试。例如,我改变了代码来使用AspectF库。这需要代码变更PageRepository 。更改代码后,我需要确保PageRepository还是按照预期的行为。不管我用什么方法的缓存,它不应该改变缓存行为:检查缓存,以确保所请求的对象是不是已经在缓存中,然后从数据库中加载并缓存它。改变方法GetPageById,实施后AspectF ,如下所示:


public Page GetPageById(int pageId)
{
    return AspectF.Define
        .Cache<Page>(_cacheResolver, CacheSetup.CacheKeys.PageId(pageId))
        .Return<Page>(() =>
            _database.GetSingle<Page, int>(DropthingsDataContext.SubsystemEnum.Page,
                pageId, LinqQueries.CompiledQuery_GetPageById).Detach());
}
现在,当我运行单元测试,它表示通过。

它确认行为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:xunitxunit.console.exe
d: runksrcDropthings.Business.Facade.TestsinDebugDropthings.Business.Facade.Tests.dll
/html FacadeTest.html

您可以使用GUI xUnit:

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