在软件开发过程中,编写可测试的代码是一项重要的技能。然而,许多教程和培训资源往往忽略了这一点,因为它们通常不涉及真实的代码层,比如服务层、数据访问层等。当尝试测试具有这些依赖的代码时,会发现测试非常慢,难以编写,而且经常因为底层依赖返回了预期之外的结果而失败。
良好的代码编写习惯是将代码分层,每一层负责应用程序的不同部分。根据需要和开发者的习惯,层的划分各不相同,但常见的分层包括:
没有依赖管理的情况下,当为表示层编写测试时,代码会实际调用服务,然后调用数据访问代码,最后触及真实的数据库。实际上,当测试“添加到购物车”或“获取购物车商品数量”时,希望隔离测试代码,并能够保证代码的可预测结果。没有依赖管理,UI测试会很慢,依赖返回不可预测的结果,这可能导致测试失败。
解决这个问题的方案是依赖注入(Dependency Injection,简称DI)。依赖注入对于没有做过的人来说可能看起来复杂和令人困惑,但实际上它是一个非常简单的概念和过程,只有几个基本步骤。想要做的是集中管理依赖,在这种情况下,是使用ShoppingCart对象,然后松散地耦合代码,这样当运行应用程序时,它使用真实的服务,而当测试它时,可以使用快速且可靠的假服务。
依赖是指代码触及其他层。例如,当表示层触及服务层时。表示代码依赖于服务层,但希望隔离测试表示代码。
public class ShoppingCartController : Controller
{
public ActionResult GetCart()
{
// 购物车服务作为具体依赖
ShoppingCartService shoppingCartService = new ShoppingCartService();
ShoppingCart cart = shoppingCartService.GetContents();
return View("Cart", cart);
}
public ActionResult AddItemToCart(int itemId, int quantity)
{
// 购物车服务作为具体依赖
ShoppingCartService shoppingCartService = new ShoppingCartService();
ShoppingCart cart = shoppingCartService.AddItemToCart(itemId, quantity);
return View("Cart", cart);
}
}
虽然有几种方法可以做到这一点,但在这个例子中,将创建一个ShoppingCartService类型的成员变量,然后在构造函数中为其分配一个实例。在每次使用ShoppingCartService的地方,将重用这个实例,而不是创建一个新的实例。
public class ShoppingCartController : Controller
{
private ShoppingCartService _shoppingCartService;
public ShoppingCartController()
{
_shoppingCartService = new ShoppingCartService();
}
public ActionResult GetCart()
{
// 现在使用共享实例的shoppingCartService依赖
ShoppingCart cart = _shoppingCartService.GetContents();
return View("Cart", cart);
}
public ActionResult AddItemToCart(int itemId, int quantity)
{
// 现在使用共享实例的shoppingCartService依赖
ShoppingCart cart = _shoppingCartService.AddItemToCart(itemId, quantity);
return View("Cart", cart);
}
}
对接口编程而不是对具体对象编程。如果代码对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 Items { get; set; }
}
public class Product
{
public int ItemId { get; set; }
public string ItemName { get; set; }
}
public class ShoppingCartController : Controller
{
// 具体对象指向实际服务
private ShoppingCartService _shoppingCartService;
// 松散耦合代码使用接口而不是具体对象
private IShoppingCartService _shoppingCartService;
public ShoppingCartController()
{
_shoppingCartService = new ShoppingCartService();
}
public ActionResult GetCart()
{
// 现在使用共享实例的shoppingCartService依赖
ShoppingCart cart = _shoppingCartService.GetContents();
return View("Cart", cart);
}
public ActionResult AddItemToCart(int itemId, int quantity)
{
// 现在使用共享实例的shoppingCartService依赖
ShoppingCart cart = _shoppingCartService.AddItemToCart(itemId, quantity);
return View("Cart", cart);
}
}
现在已经将所有依赖集中在一个地方,代码现在与这些依赖松散耦合。与之前一样,有几种方法可以处理下一步。如果没有设置IoC容器,如NInject或StructureMap,最简单的方法是重载构造函数:
// 松散耦合代码使用接口而不是具体对象
private IShoppingCartService _shoppingCartService;
// MVC使用此构造函数
public ShoppingCartController()
{
_shoppingCartService = new ShoppingCartService();
}
// 可以在测试时使用此构造函数注入ShoppingCartService依赖
public ShoppingCartController(IShoppingCartService shoppingCartService)
{
_shoppingCartService = shoppingCartService;
}
一个可能的测试固定装置示例如下。注意,创建了一个ShoppingCartService的假桩(stub)。这个桩被传递到Controller对象中,GetContents方法被实现为返回一些假数据,而不是调用实际访问数据库的代码。由于这是100%的代码,它比查询数据库要快得多,而且永远不必担心在测试完成后安排测试数据或清理测试数据。注意,由于在步骤2中集中了依赖,只需要注入一次。由于步骤3,依赖是松散耦合的,所以可以传递任何对象,真实的或假的,只要它实现了IShoppingCartService接口。
[TestClass]
public class ShoppingCartControllerTests
{
[TestMethod]
public void GetCartSmokeTest()
{
// 安排
ShoppingCartController controller = new ShoppingCartController(new ShoppingCartServiceStub());
// 执行
ActionResult result = controller.GetCart();
// 断言
Assert.IsInstanceOfType(result, typeof(ViewResult));
}
}
///
///
这是ShoppingCartService的桩
///
public class ShoppingCartServiceStub : IShoppingCartService
{
public ShoppingCart GetContents()
{
return new ShoppingCart
{
Items = new List
{
new Product { ItemId = 1, ItemName = "Widget" }
}
};
}
public ShoppingCart AddItemToCart(int itemId, int quantity)
{
throw new NotImplementedException();
}
}