65.9K
CodeProject 正在变化。 阅读更多。
Home

如何在 .NET 中编写可测试的代码

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.97/5 (18投票s)

2010 年 11 月 6 日

CPOL

4分钟阅读

viewsIcon

56642

本文演示了如何管理依赖项以简化代码的单元测试。

引言

测试驱动开发(TDD)如果掌握得当,会非常有益。不幸的是,市面上许多教程和培训资源都略过了如何编写可测试的代码,因为它们通常是示例代码,不包含实际代码中常见的层级,例如服务层、数据访问层等。不可避免的是,当您尝试测试包含这些依赖项的代码时,测试会变得非常缓慢,难以编写,并且经常会因为底层依赖项返回的结果与预期不符而失败。

背景

编写良好的代码应该被分离成不同的层,每一层负责应用程序的不同部分。实际的分层会根据需求和开发者的习惯而有所不同,但常见的方案是:

ntier.PNG

  • 用户界面/表示层:这是您的表示逻辑和 UI 交互代码。
  • 业务逻辑/服务层:这是您的业务逻辑。例如:购物车代码。这个购物车知道如何计算购物车总价,如何统计订单中的商品数量等。
  • 数据访问层/持久化层:这部分代码知道如何连接数据库并返回一个购物车,或者如何将购物车保存到数据库。
  • 数据库:购物车内容保存在这里。

无依赖管理

在没有依赖管理的情况下,当您为表示层编写测试时,您的代码会连接到真实的服务,这些服务又会连接到真实的数据访问代码,最终访问真实数据库。实际上,当您测试“添加到购物车”或“获取购物车商品数量”等功能时,您希望代码能够独立运行,并能够保证代码产生 **可预测** 的结果。没有依赖管理,您的 UI 测试“添加到购物车”功能将非常缓慢,并且您的依赖项会返回不可预测的结果,这可能导致测试失败。

解决方案是依赖注入

解决这个问题的方案是依赖注入。依赖注入(Dependency Injection,简称 DI)对于初学者来说可能显得复杂和令人困惑,但实际上,它是一个非常非常简单的概念和过程,包含几个基本步骤。我们要做的就是集中管理您的依赖项,在本例中是 `ShoppingCart` 对象的用法,然后松散耦合您的代码,这样当您运行应用程序时,它会使用真实的服务;而在测试时,您可以使用快速且可靠的假服务。请注意,有几种方法可以实现这一点;为了简单起见,我只演示构造函数注入。

步骤 1 - 识别您的依赖项

依赖项是指您的代码与其他层交互的地方。例如,当您的表示层与服务层交互时。您的表示代码依赖于服务层,但我们希望独立测试表示代码。

public class ShoppingCartController : Controller
{
    public ActionResult GetCart()
    {
        //shopping cart service as a concrete dependency
        ShoppingCartService shoppingCartService = new ShoppingCartService();
        ShoppingCart cart = shoppingCartService.GetContents();
        return View("Cart", cart);
    }

    public ActionResult AddItemToCart(int itemId, int quantity)
    {
        //shopping cart service as a concrete dependency
        ShoppingCartService shoppingCartService = new ShoppingCartService();
        ShoppingCart cart = shoppingCartService.AddItemToCart(itemId, quantity);
        return View("Cart", cart);
    }

步骤 2 - 集中管理您的依赖项

虽然有几种方法可以做到这一点,但在本例中,我将创建一个 `ShoppingCartService` 类型的成员变量,然后在构造函数中创建的实例中为其赋值。在每次使用 `ShoppingCartService` 的地方,我将重用这个实例,而不是创建新实例。

public class ShoppingCartController : Controller
{
    private ShoppingCartService _shoppingCartService;

    public ShoppingCartController()
    {
        _shoppingCartService = new ShoppingCartService();
    }
    public ActionResult GetCart()
    {
        //now using the shared instance of the shoppingCartService dependency
        ShoppingCart cart = _shoppingCartService.GetContents();
        return View("Cart", cart);
    }

    public ActionResult AddItemToCart(int itemId, int quantity)
    {
        //now using the shared instance of the shoppingCartService dependency
        ShoppingCart cart = _shoppingCartService.AddItemToCart(itemId, quantity);
        return View("Cart", cart);
    }
}

步骤 3 - 松散耦合

针对接口而不是具体对象进行编程。如果您用 `IShoppingCartService` 接口编写代码,而不是直接使用具体的 `ShoppingCartService`,那么在测试时,您可以替换为一个实现 `IShoppingCartService` 接口的假购物车服务。在下图的示例中,请注意,唯一的变化是成员变量的类型现在是 `IShoppingCartService`,而不是 `ShoppingCartService`。

public interface IShoppingCartService
{
    ShoppingCart GetContents();
    ShoppingCart AddItemToCart(int itemId, int quantity);
}
public class ShoppingCartService : IShoppingCartService
{
    public ShoppingCart GetContents()
    {
        throw new NotImplementedException("Get cart from Persistence Layer");
    }
    public ShoppingCart AddItemToCart(int itemId, int quantity)
    {
        throw new NotImplementedException("Add Item to cart then return updated cart");
    }
}
public class ShoppingCart
{
    public List<product> Items { get; set; }
}
public class Product
{
    public int ItemId { get; set; }
    public string ItemName { get; set; }
}

public class ShoppingCartController : Controller
{
    //Concrete object below points to actual service
    //private ShoppingCartService _shoppingCartService;

    //loosely coupled code below uses the interface rather than the 
    //concrete object
    private IShoppingCartService _shoppingCartService;

    public ShoppingCartController()
    {
        _shoppingCartService = new ShoppingCartService();
    }
    public ActionResult GetCart()
    {
        //now using the shared instance of the shoppingCartService dependency
        ShoppingCart cart = _shoppingCartService.GetContents();
        return View("Cart", cart);
    }

    public ActionResult AddItemToCart(int itemId, int quantity)
    {
        //now using the shared instance of the shoppingCartService dependency
        ShoppingCart cart = _shoppingCartService.AddItemToCart(itemId, quantity);
        return View("Cart", cart);
    }
}

步骤 4 - 注入依赖项

现在我们将所有依赖项集中在一个地方,并且我们的代码已与这些依赖项松散耦合。与之前一样,有几种方法可以处理下一步。如果没有设置像 NInject 或 StructureMap 这样的 IoC 容器,最简单的方法就是重载构造函数。

//loosely coupled code below uses the interface rather 
//than the concrete object
private IShoppingCartService _shoppingCartService;

//MVC uses this constructor 
public ShoppingCartController()
{
    _shoppingCartService = new ShoppingCartService();
}

//You can use this constructor when testing to inject the    
//ShoppingCartService dependency
public ShoppingCartController(IShoppingCartService shoppingCartService)
{
    _shoppingCartService = shoppingCartService;
}

步骤 5 - 使用存根进行测试

下面是一个可能的测试夹具示例。请注意,我已经创建了一个 `ShoppingCartService` 的假版本(也称为存根)。这个存根被传递到我的 Controller 对象中,并且 `GetContents` 方法被实现为返回一些假数据,而不是调用实际访问数据库的代码。由于这是 100% 代码,它比查询数据库 **快得多**,而且我永远不必担心在测试完成后准备测试数据或清理测试数据。请注意,由于步骤 2 中我们集中管理了依赖项,所以我只需要注入一次。由于步骤 3,我的依赖项是松散耦合的,所以只要它实现了 `IShoppingCartService` 接口,我就可以传入任何对象,无论是真实的还是假的。

[TestClass]
public class ShoppingCartControllerTests
{
    [TestMethod]
    public void GetCartSmokeTest()
    {
        //arrange
        ShoppingCartController controller = 
           new ShoppingCartController(new ShoppingCartServiceStub());

        // Act
        ActionResult result = controller.GetCart();

        // Assert
        Assert.IsInstanceOfType(result, typeof(ViewResult));
    }
}

/// <summary>
/// This is is a stub of the ShoppingCartService
/// </summary>
public class ShoppingCartServiceStub : IShoppingCartService
{
    public ShoppingCart GetContents()
    {
        return new ShoppingCart
        {
            Items = new List<product> {
                new Product {ItemId = 1, ItemName = "Widget"}
            }
        };
    }

    public ShoppingCart AddItemToCart(int itemId, int quantity)
    {
        throw new NotImplementedException();
    }
}

接下来我该怎么做?

  • 使用 IoC/DI 容器:.NET 中最常见和最受欢迎的 IoC 容器是 StructureMapNinject。在实际的生产代码中,您将拥有大量的依赖项,您的依赖项又会有依赖项,等等。您会很快发现这变得难以管理。答案是使用 DI/IoC 框架来管理它们。
  • 使用隔离框架:创建存根和模拟对象可能会非常耗时,使用模拟框架可以为您节省大量时间和代码。.NET 中最常见的框架是 Rhino MocksMoq
© . All rights reserved.