依赖注入与代码测试性

在软件开发过程中,编写可测试的代码是一项重要的技能。然而,许多教程和培训资源往往忽略了这一点,因为它们通常不涉及真实的代码层,比如服务层、数据访问层等。当尝试测试具有这些依赖的代码时,会发现测试非常慢,难以编写,而且经常因为底层依赖返回了预期之外的结果而失败。

良好的代码编写习惯是将代码分层,每一层负责应用程序的不同部分。根据需要和开发者的习惯,层的划分各不相同,但常见的分层包括:

  • 用户界面/表示层:这是展示逻辑和用户界面交互代码。
  • 业务逻辑/服务层:这是业务逻辑。例如,购物车代码。购物车知道如何计算购物车总额,如何计算订单中的商品数量等。
  • 数据访问层/持久层:这段代码知道如何连接数据库并返回购物车,或者如何将购物车保存到数据库。
  • 数据库:这是保存购物车内容的地方。

没有依赖管理的情况下,当为表示层编写测试时,代码会实际调用服务,然后调用数据访问代码,最后触及真实的数据库。实际上,当测试“添加到购物车”或“获取购物车商品数量”时,希望隔离测试代码,并能够保证代码的可预测结果。没有依赖管理,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(); } }
沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485