介绍
测试驱动开发(TDD)是软件开发的流行和有效的方法。TDD的一些优点包括:
- 更好地了解您在写入之前应该写的代码
- 在成功或失败方面快速衡量进展
- 通过有条不紊,更短,可衡量的步骤,分解和征服大而复杂的问题
- 快速确定对正在开发的系统进行更改的影响
任何形式和在开发过程中任何时刻创建的模型通常都是有益的。使用型号,您可以:
- 更好地理解和沟通高水平系统应该做什么
- 通过清晰的数据,关系和工作流程来划分和征服大量复杂的问题
- 在开发代码时利用模型信息
在本文中,我们将介绍一个利用TDD的示例问题,同时结合了面向模型的方法,并结合TDD的优点和利用模型。
请原谅本例中使用的代码。这个代码过于简单,而且这个例子很成熟。本文的重点不在于代码的质量,而在于方法,以及如何为自己的项目应用该方法。
我希望本文将会产生一些可以用来进一步完善本文中概述的方法和示例的讨论。在很多实际项目中直接应用TDD都没有强大的背景,我非常欢迎TDD社区的人士的投入。
未来的文章主题可能涵盖行为驱动开发(BDD)。
背景
由于Mo +专门为这种方式而设计,因此Mo + 将成为在本文中引入面向模型的方法的选择。该代码项目文章介绍了面向模型开发的Mo +面向对象的编程语言和Mo + Solution Builder IDE 。 本文的目的是演示如何将面向对象的开发应用于TDD。将会介绍Mo +模型和模板,但本文不是Mo +语言或工具如何。但是,您可以将本文中概述的技术与其他建模和代码生成工具应用到不同程度。如果你试图使用Mo +,t 他上面的文章还有一个了解更多部分,其他信息让你开始。
该示例将使用C#实现用于测试的代码和Visual Studio单元测试。
这篇文章的一些灵感来源于肯特·贝克的书和斯科特·安布勒(Scott Ambler)的博客。
过程定义
测试驱动开发(TDD)可以用这个基本公式来描述:
TDD = TFD +重构
在哪里结合测试第一次开发(TFD)和重构您仍然通过所有测试的代码的步骤。
模型驱动开发(MDD)通常意味着您使用一种或多种模型创建前端架构,并将(通常更复杂)的模型转换为代码。这不是我们要在这里做的。所以,我想投入一个术语,面向模型的开发(MOD),它允许在开发过程的任何时刻创建一个简单的,集中的模型并用于开发目的。使用MOD,您可以自由地使用尽可能少的重点发展模式,只有当您想要的时候才能使用。
最后,我将硬币另一个术语,面向模型的测试驱动开发(MOTDD),这将是我们将在这里使用的过程。MOTDD可以用以下公式进行描述:
MOTDD = TDD + MOD
使用MOTDD,除了根据标准TDD重构代码之外,您还可以以面向模型的方式重构测试用例和代码。考虑下图,其中上半部分表示TDD,下半部分表示并入MOD:
这个过程将通过下面的例子进一步解释,但你现在可能有几个问题。为什么要将MOD应用于TDD?什么时候我想把MOD应用到TDD?
为什么要申请MOD?
应用MOD是有用的,当您可以使用它来增强重构目标和您已经练习TDD的步骤。当您可以比单独使用自定义方法更快速,安全地实现目标时,MOD很有用。
何时申请MOD?
当您的模型中的信息变得已知和可用时,您可以应用MOD,当您识别面向模型的模式时,可以减少自定义代码的数量,并使您的系统对基于模型和团队最佳实践的更改更为稳健。
应用MOD的目标不是尽可能多地生成面向代码的代码,它是为了满足您在重构时考虑的总体目标。您可以在重构中找到机会,以使用强大的,独立于模型的自定义代码来替代面向模型的代码中的重复。而且,这是一件好事!
例子
我们希望通过使用Northwind数据库实现一个简单的电子商务场景,仅作为模型信息的来源。虽然该模型可以包含更多的信息,但是我们仅仅将为该模型导向代码使用实体和基本属性。
考虑一个变化:我们想要这个系统做什么?我们从一些简单的用户故事开始:
- 客户注册
- 客户找到一个产品
- 客户订购产品
- 客户收到产品
入门考试第一的方式
编写自定义测试:我们知道我们的系统与产品,订单和客户有关。要开始,让我们从上述用户故事中进行测试,并让它们通过(为了简洁起见,我们将一次性完成这些)。我们的客户测试看起来像:
[TestClass]public class CustomerTests{ [TestMethod] public void TestRegisterCustomer() { Customer customer = Customer.RegisterCustomer("John Doe", "555-1212"); Assert.AreEqual(customer.ContactName, "John Doe"); Assert.AreEqual(customer.Phone, "555-1212"); } [TestMethod] public void TestFindProduct() { Product product = Customer.FindProduct("My Widget"); Assert.AreEqual(product.ProductName, "My Widget"); } [TestMethod] public void TestOrderProduct() { Order order = Customer.OrderProduct("My Widget"); Assert.AreEqual(order.ProductName, "My Widget"); } [TestMethod] public void TestGetProduct() { Order order = Customer.GetProduct("My Widget"); Assert.AreEqual(order.ProductName, "My Widget"); }}
当然,这些测试无法编译或运行。
做一个自定义代码更改: 让我们写一些最小的代码来让这些测试通过。首先,我们的产品,订单和客户模型类如下所示:
public partial class Product{ public string ProductName { get; set; }}public partial class Order{ public string ProductName { get; set; }}public partial class Customer{ public string ContactName { get; set; } public string Phone { get; set; } public static Customer RegisterCustomer(string contactName, string phone) { Customer customer = new Customer { ContactName = contactName, Phone = phone }; return customer; } public static Product FindProduct(string productName) { Product product = new Product { ProductName = productName }; return product; } public static Order OrderProduct(string productName) { Order order = new Order { ProductName = productName }; return order; } public static Order GetProduct(string productName) { Order order = new Order { ProductName = productName }; return order; }}
好的,我们的单位测试通过了。请参阅示例下载中步骤1的代码。
自定义重构
做一个自定义代码更改:我们看看我们大多数假的代码,以便测试通过,我们意识到我们需要以某种方式存储客户,产品和订单数据。因此,我们只需创建一个存储此数据的虚拟存储库:
public partial class Repository{ public static List<Product> Products { get; set; } public static List<Customer> Customers { get; set; } public static List<Order> Orders { get; set; }}
重构自定义代码: 让我们在我们的客户类方法中使用这个“仓库”。稍后我们看到OrderProduct()和GetProduct()中存在一些潜在的问题,但至少我们可以修改RegisterCustomer()和FindProduct()来使用存储库:
public partial class Customer{ public string ContactName { get; set; } public string Phone { get; set; } public static Customer RegisterCustomer(string contactName, string phone) { Customer customer = new Customer { ContactName = contactName, Phone = phone }; Repository.Customers.Add(customer); return customer; } public static Product FindProduct(string productName) { Product product = Repository.Products.FirstOrDefault(i => i.ProductName == productName); return product; } public static Order OrderProduct(string productName) { Order order = new Order { ProductName = productName }; return order; } public static Order GetProduct(string productName) { Order order = new Order { ProductName = productName }; return order; }}
由于存储库中的空引用错误,我们的测试立即失败。
做一个自定义代码更改: 我们需要改变我们的测试运行方式,让他们上班。 因此,我们将添加一个Setup()方法,在运行每个测试之前调用该方法,以便初始化产品和客户的存储库:
[TestClass]public class CustomerTests{ [TestInitialize] public void Setup() { Repository.Products = new List<Product>(); Repository.Products.Add(new Product { ProductName = "My Widget" }); Repository.Customers = new List<Customer>(); Repository.Customers.Add(new Customer { ContactName = "Jane Doe", Phone = "555-1213" }); } [TestMethod] public void TestRegisterCustomer() { Customer customer = Customer.RegisterCustomer("John Doe", "555-1212"); Assert.AreEqual(customer.ContactName, "John Doe"); Assert.AreEqual(customer.Phone, "555-1212"); } [TestMethod] public void TestFindProduct() { Product product = Customer.FindProduct("My Widget"); Assert.AreEqual(product.ProductName, "My Widget"); } [TestMethod] public void TestOrderProduct() { Order order = Customer.OrderProduct("My Widget"); Assert.AreEqual(order.ProductName, "My Widget"); } [TestMethod] public void TestGetProduct() { Order order = Customer.GetProduct("My Widget"); Assert.AreEqual(order.ProductName, "My Widget"); }}
好的,我们的测试现在运行!请参阅示例下载中步骤2的代码。
一些简单的面向模型的重构
使用Northwind(SQL Server或MySQL)数据库作为模型来源,我们审查模型,以及这些信息如何影响我们的整体设计。下图说明了我们示例中对于实体和属性的Mo +中的一些模型信息:
我们立即看到我们的客户,产品和订单模型类中缺少的属性,以及所需的订单结构,以允许订单中的多个产品。但是,抱着你的马!我们正在做一个测试驱动的方法,我们不会尝试整合所有这些信息。
Refactor模型导向代码:我们注意到,我们可以轻松地以面向模型的方式重构我们的Repository类,使其对于模型中的未来变化是稳健的。我很懒,我想我可以先写出面向模型的测试,因为模型导向重构后的代码应该与以前一样。
重构面向模型的代码涉及创建或修改代码模板(或多个),并生成更新的面向模型的代码。以下是Repository类的主体的Mo +代码模板 (Mo +模板代码将仅作为图形显示为可读性,完整模板的实际代码可在附件样本下载中找到)。在第16行,foreach语句在感兴趣的解决方案中遍历每个Entity以添加到存储库,在第18行,我们正在为特定实体创建存储库,插入EntityName来自模型的信息。在我们的模型中,我们使用“ForDev”标记感兴趣的实体,我们可以使用它们将我们的存储库仅限于目前的兴趣实体:
我们生成的Repository类看起来像这样(与之前完全相同):
public partial class Repository{ public static List<Customer> Customers { get; set; } public static List<Order> Orders { get; set; } public static List<Product> Products { get; set; }}
更多地处理订单
在考虑这个小电子商务的情况下,我们意识到需要解决的几件事情:
- 客户经常登录以后订购
- 登录客户放置订单 并检索订单信息
考虑一个变化:所以,我们再添加一个用户故事:
- 客户登录
编写自定义测试:我们为此用户故事创建单元测试,并更新我们的订单产品,并获得基于登录客户的产品测试:
[TestClass]public class CustomerTests{ [TestInitialize] public void Setup() { Repository.Products = new List<Product>(); Repository.Products.Add(new Product { ProductName = "My Widget" }); Repository.Customers = new List<Customer>(); Repository.Customers.Add(new Customer { ContactName = "Jane Doe", Phone = "555-1213" }); } [TestMethod] public void TestRegisterCustomer() { Customer customer = Customer.RegisterCustomer("John Doe", "555-1212"); Assert.AreEqual(customer.ContactName, "John Doe"); Assert.AreEqual(customer.Phone, "555-1212"); } [TestMethod] public void TestLoginCustomer() { Customer customer = Customer.FindCustomer("Jane Doe"); Assert.AreEqual(customer.ContactName, "Jane Doe"); Assert.AreEqual(customer.Phone, "555-1213"); } [TestMethod] public void TestFindProduct() { Product product = Product.FindProduct("My Widget"); Assert.AreEqual(product.ProductName, "My Widget"); } [TestMethod] public void TestOrderProduct() { Customer customer = Customer.FindCustomer("Jane Doe"); Order order = customer.OrderProduct("My Widget"); Assert.AreEqual(order.ProductName, "My Widget"); } [TestMethod] public void TestGetProduct() { Customer customer = Customer.FindCustomer("Jane Doe"); Order order = customer.OrderProduct("My Widget"); order = customer.GetProduct("My Widget"); Assert.AreEqual(order.ProductName, "My Widget"); }}
进行自定义代码更改:我们进行自定义更改,将产品搜索移动到Product类,并添加一系列订单和方法,以支持Customer类中客户的登录和订单:
public partial class Product{ public string ProductName { get; set; } public static Product FindProduct(string productName) { Product product = Repository.Products.FirstOrDefault(i => i.ProductName == productName); return product; }}public partial class Customer{ public string ContactName { get; set; } public string Phone { get; set; } public List<Order> Orders { get; set; } public static Customer RegisterCustomer(string contactName, string phone) { Customer customer = new Customer { ContactName = contactName, Phone = phone }; Repository.Customers.Add(customer); return customer; } public static Customer FindCustomer(string contactName) { Customer customer = Repository.Customers.FirstOrDefault(i => i.ContactName == contactName); return customer; } public Order OrderProduct(string productName) { if (Orders == null) Orders = new List<Order>(); Order order = new Order { ProductName = productName }; Orders.Add(order); return order; } public Order GetProduct(string productName) { if (Orders == null) return null; foreach (Order order in Orders) { if (order.ProductName == productName) return order; } return null; }}
结合模型结构
我们现在想利用我们模型中发现的一些整体客户,产品和订单信息。将大量面向模型的代码与使用测试驱动方法创建的自定义代码相结合似乎是令人生畏的。但是,像以前的步骤一样,我们从一些单元测试开始,让它们通过。
编写自定义测试:让我们从一个简单的自定义单元测试开始。我们知道我们希望将所有信息保存在存储库中,对于每种类型的对象,我们需要将它们添加到存储库中。所以,让我们从ProductCRUDTests类开始测试将产品添加到存储库中:
[TestClass]public partial class ProductCRUDTests{ [TestInitialize] public void Setup() { Repository.Products = new List<Product>(); } [TestMethod] public void AddProduct() { int count = Repository.Products.Count; Product.AddProduct(new Product()); Assert.AreEqual(count + 1, Repository.Products.Count); }}
当然,这个单元测试失败,因为我们没有 AddProduct() 方法。
做一个自定义代码更改: 所以,让我们把这个缺少的方法添加到产品类:
public partial class Product{ public string ProductName { get; set; } public static Product FindProduct(string productName) { Product product = Repository.Products.FirstOrDefault(i => i.ProductName == productName); return product; } public static void AddProduct(Product product) { Repository.Products.Add(product); }}
单位测试再次通过。
编写面向模型的测试:编写面向模型的测试涉及创建或修改代码模板(或多个)以进行测试,并生成更新的面向模型的测试代码。首先,我们使用ProductCRUDTests 类作为模板为模型中每个实体添加测试的起点。测试类体的Mo +模板如下所示(我们从模型中插入EntityName信息):
接下来,使用此模板,我们为模型中的每个实体生成额外的单元测试。该ProductCRUDTests 类主体应该是相同的,我们只是做了一个自定义的。该OrderCRUDTests类例如如下所示:
[TestClass]public partial class OrderCRUDTests{ [TestInitialize] public void Setup() { Repository.Orders = new List<Order>(); } [TestMethod] public void AddOrder() { int count = Repository.Orders.Count; Order.AddOrder(new Order()); Assert.AreEqual(count + 1, Repository.Orders.Count); }}
现在我们可以删除自定义的ProductCRUDTests 文件,因为它不再需要。但是,当然,由于我们对应的模型类没有添加方法,所以其他单元测试(如OrderCRUDTests)将失败。那么,现在是以模型为导向的方式重构我们的模型类的时候了!
Refactor面向对象的代码:现在我们要为模型类做一些面向模型的重构。为了重构我们的模型类,我们使用Product类作为模板的起点。我们希望生成的模型类包含模型属性和添加方法。模型类体的Mo +模板如下所示(我们为每个Property添加信息,如第11行所示,并从模型中插入PropertyName和EntityName信息):
接下来,使用此模板,我们为模型中的每个实体生成模型类代码。产品类的生成代码如下所示:
public partial class Product{ public int ProductID { get; set; } public string ProductName { get; set; } public int? SupplierID { get; set; } public int? CategoryID { get; set; } public string QuantityPerUnit { get; set; } public decimal? UnitPrice { get; set; } public short? UnitsInStock { get; set; } public short? UnitsOnOrder { get; set; } public short? ReorderLevel { get; set; } public bool Discontinued { get; set; } public static void AddProduct(Product product) { Repository.Products.Add(product); }}
哦,现在我们的代码不编译!一些生成的属性和方法也存在于我们的自定义代码中。
我们需要删除我们的自定义代码中的重复元素。Product类的自定义代码如下所示:
public static Product FindProduct(string productName){ Product product = Repository.Products.FirstOrDefault(i => i.ProductName == productName); return product;}
好的,一切都编译了,而且单元测试也通过了! 请参阅示例下载中步骤5的代码。 CRUD单元测试和模型代码的实体级Mo +代码模板分别称为CRUDTestCode和ModelClassCode。
利用模型结构处理订单
现在我们已经结合了实际的模型结构,我们可以再来看看我们如何处理订单。但首先,我们进行一些自定义更改,以利用我们新生成的方法将项目添加到存储库。我们替换直接调用以将项目添加到CustomerTests.Setup()和Customer.RegisterCustomer()中的存储库中(您可以在示例下载中看到更改)。我们再次进行单元测试,并通过。
现在,我们来看看我们对于订单的支持,我们现在就了解了模型结构。放置订单的单元测试是CustomerTests.TestOrderProduct():
[TestMethod]public void TestOrderProduct(){ Customer customer = Customer.FindCustomer("Jane Doe"); Order order = customer.OrderProduct("My Widget"); Assert.AreEqual(order.ProductName, "My Widget");}
写(修改)自定义测试:我们知道订单不应该有产品名称,但应该包含一组包含产品相关信息的订单详细信息。如果客户订购一个产品,则本机测试应如下所示:
[TestMethod]public void TestOrderProduct(){ Customer customer = Customer.FindCustomer("Jane Doe"); Order order = customer.OrderProduct("My Widget"); Assert.AreEqual(1, order.OrderDetails.Count); Product product = Product.FindProduct(order.OrderDetails[0].ProductID); Assert.IsNotNull(product); Assert.AreEqual(product.ProductName, "My Widget");}
此更改会导致编译错误,我们需要先修复这些错误。
进行自定义代码更改:我们需要将OrderDetails列表添加到自定义Order类中:
public partial class Order{ public string ProductName { get; set; } public List<OrderDetail> OrderDetails { get; set; }}
自定义代码更改: 我们需要在自定义产品类中添加另一个FindProduct()方法:
public static Product FindProduct(int productID){ Product product = Repository.Products.FirstOrDefault(i => i.ProductID == productID); return product;}
好的,这个编译,但是TestOrderProduct()测试失败。我们在Customer.OrderProduct()中看到一些问题:
public Order OrderProduct(string productName){ if (Orders == null) Orders = new List<Order>(); Order order = new Order { ProductName = productName }; Orders.Add(order); return order;}
制定自定义代码更改: 订购产品时,应在订单中添加订单详细信息,并在产品库中找到产品信息:
public Order OrderProduct(string productName){ if (Orders == null) Orders = new List<Order>(); Product product = Product.FindProduct(productName); if (product != null) { Order order = new Order { CustomerID = CustomerID }; order.OrderDetails = new List<OrderDetail>(); Orders.Add(order); Order.AddOrder(order); OrderDetail detail = new OrderDetail { ProductID = product.ProductID, OrderID = order.OrderID }; order.OrderDetails.Add(detail); OrderDetail.AddOrderDetail(detail); return order; } return null;}
好的,现在TestOrderProduct()测试成功了,但现在我们打破了TestGetProduct():
[TestMethod]public void TestGetProduct(){ Customer customer = Customer.FindCustomer("Jane Doe"); Order order = customer.OrderProduct("My Widget"); order = customer.GetProduct("My Widget"); Assert.AreEqual(order.ProductName, "My Widget");}
编写(修改)自定义测试:我们发现测试中出现 了两件事情,知道客户应该只能“获取”作为该客户的有效订单的一部分的产品。所以这个单元测试应该更像:
[TestMethod]public void TestGetProduct(){ Customer customer = Customer.FindCustomer("Jane Doe"); Order order = customer.OrderProduct("My Widget"); Product product = customer.GetProduct("My Widget"); Assert.AreEqual(product.ProductName, "My Widget");}
进行自定义代码更改: 这当然会导致编译错误,并且我们将Customer.GetProduct()重构为在任何顺序下返回匹配产品的方法:
public Product GetProduct(string productName){ if (Orders == null) return null; foreach (Order order in Orders) { foreach (OrderDetail detail in order.OrderDetails) { Product product = Product.FindProduct(detail.ProductID); if (product != null && product.ProductName == productName) { return product; } } } return null;}
此时,您也可以从Order中删除ProductName属性。单元测试再次通过,我们有一些几乎看起来像一个订购过程(虽然我不会把我的任何$)。请参阅示例下载中步骤6的代码。
继续迭代,直到你满意
当然这个例子远不是一个真正的电子商务系统。在实践中,您将继续介入测试,以覆盖用户故事,编写自定义代码以使其通过,重构自定义代码,以及重构面向模型的测试和代码。
我们在这里再做一个迭代。
定制面向模型的测试:我们加强我们的AddProduct()测试以添加多个产品,并验证ids是唯一的(并且在我们处理时重命名为TestAddProduct()方法):
[TestMethod]public void TestAddProduct(){ int count = Repository.Products.Count; Product product1 = new Product(); Product.AddProduct(product1); Assert.AreEqual(count + 1, Repository.Products.Count); count = Repository.Products.Count; Product product2 = new Product(); Product.AddProduct(product2); Assert.AreEqual(count + 1, Repository.Products.Count); Assert.AreNotEqual(product1.ProductID, product2.ProductID);}
public static void AddProduct(Product product){ product.ProductID = Repository.Products.Count + 1; Repository.Products.Add(product);}
好的,单位测试再次通过,呃!
Refactor面向对象的代码: 但是我们定制了一些面向模型的代码,需要重构面向模型的单元测试和模型代码。为了重构,只是更新相关模板并重新生成面向模型的代码。
请参阅示例下载中的步骤7的代码,其中显示了更新的模板和更新的面向对象代码。
综上所述
希望这篇文章为您提供了与考试驱动开发相结合的考虑面向模式的开发思路。如果您想在流程描述和示例中看到更多的清晰度和细化,请提供一些反馈。
此外,我希望您尝试Mo +和Mo +解决方案构建器,以充分利用结合面向模型的方法来开发。免费的开源产品可以在moplus.codeplex.com上找到。除了产品,本网站还包含用于构建更完整型号的样品包,并生成完整的工作应用程序。视频教程和其他材料也可在这个网站。在莫+解决方案构建还包含板载帮助广泛。