Introduction
Test driven development (TDD) is a popular and effective approach to software development. Some advantages of TDD include the ability to:
- Better understand the code you should write before you write it
- Measure progress quickly in terms of success or failure
- Divide and conquer large, complex problems through methodical, shorter, and measurable steps
- Quickly identify the impact of making changes to the system under development
Models in any form and created at any point during the development process are often beneficial. With models, you have the ability to:
- Better understand and communicate at a high level what the system should do
- Divide and conquer large, complex problems through clarity of data, relationships, and workflows
- Utilize model information in developing code
In this article, we will walk through an example problem that utilizes TDD, while also incorporating a model oriented approach, and in doing so combining the advantages of TDD and utilizing models.
Please pardon the code used in this example. The code is overly simplistic and the example contrived. The focus of the article is not on the quality of the code, but on the approach and how you might apply the approach for your own projects.
I am hoping that this article will generate some discussion that can be used to further refine the approach and example outlined in this article. I don't have a strong background in applying TDD directly on many actual projects, and I very much welcome input from those in the TDD community.
A future article topic may cover behavior driven development (BDD).
Background
Mo+ will be the choice for incorporating a model oriented approach in this article, since Mo+ is specifically designed for such an approach. The Mo+ model oriented programming language and the Mo+ Solution Builder IDE for model oriented development was introduced in this Code Project article. The intent of this article is to demonstrate how model oriented development can be applied to TDD. Mo+ models and templates will be presented, but this article is not a Mo+ language or tool how to. However, you can apply the technique outlined in this article with other modeling and code generation tools to various degrees. If you are trying to use Mo+, the above article also has a Learn More section with additional information to get you started.
The example will be implemented using C# for the code and Visual Studio unit tests for the tests.
Some sources of inspiration for this article came from Kent Beck's book and Scott Ambler's blog.
The Process Defined
Test driven development (TDD) can be described by this basic formula:
TDD = TFD + Refactoring
Where you combine test first development (TFD) with steps to refactor your code that still passes all of your tests.
Model driven development (MDD) generally implies that you create the front end architecture with models in one form or another, and translate (usually more complex) models into code. That is not what we are going to do here. So, I'd like to coin a term, model oriented development (MOD), that allows a simple, focused model to be created and used for development purposes at any point in the development process. With MOD, you are free to use as little or as much of the focused model for development, and only when you want it.
Finally, I'll coin another term, model oriented test driven development (MOTDD) that will be the process we will use here. MOTDD can be described with this formula:
MOTDD = TDD + MOD
With MOTDD, you consider refactoring your test cases and code in a model oriented way in addition to refactoring your code as per standard TDD. Consider the following diagram, where the top half represents TDD, and the bottom half represents incorporating MOD:
This process will be further explained through the example below, but you probably have a couple of questions right now. Why would I want to apply MOD to TDD? And when would I want to apply MOD to TDD?
Why Apply MOD?
Applying MOD is useful when you can use it to enhance the refactoring goals and steps that you already practice with TDD. MOD is useful when you can meet your goals more quickly and safely than with a custom approach alone.
When to Apply MOD?
You apply MOD when the information in your model becomes known and available, and when you recognize model oriented patterns that can reduce the amount of your custom code, and make your system more robust to changes based on the model and on team best practices.
The goal of applying MOD is not to generate as much model oriented code as possible, it is to meet your overall goals you consider when refactoring. You may find opportunities in your refactoring to replace duplication in model oriented code with robust, model independent custom code. And, that's a good thing!
The Example
We want to implement a simple e-commerce scenario utilizing the Northwind database merely as a source for model information. Although the model can contain much more information, we are only going to utilize entities and basic properties for the model oriented code this example.
Consider a Change: What do we want this system to do? Let's start with some simple user stories:
- Customer registers
- Customer finds a product
- Customer orders a product
- Customer receives product
Getting Started In A Test First Manner
Write a Custom Test: We know our system has something to do with products, orders, and customers. To get started, let's make tests out of the above user stories and make them pass (we'll do them all at once here for brevity). Our customer tests look like:
[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");
}
}
Of course, these tests fail to compile or run.
Make a Custom Code Change: Let's write some minimal code to get these tests to pass. To begin, our Product, Order, and Customer model classes look like the following:
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;
}
}
OK, our unit tests pass. See the code for Step 1 in the sample download.
Custom Refactoring
Make a Custom Code Change: We look at our mostly fake code written to get the tests to pass, and we realize we need to store customer, product, and order data in some way. So, let's just create a fake repository that stores lists of this data:
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; }
}
Refactor Custom Code: Let's use this "repository" in our customer class methods. We see some potential problems in OrderProduct() and GetProduct() to address later, but at least we can modify RegisterCustomer() and FindProduct() to use the repository:
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;
}
}
Our tests immediately fail due to null references errors in the repository.
Make a Custom Code Change: We need to change the way our tests are run to get them to work. So, we'll add a Setup() method that is called before each test is run to initialize the repository with a product and a customer:
[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");
}
}
OK, our tests run now! See the code for Step 2 in the sample download.
Some Simple Model Oriented Refactoring
Using the Northwind (SQL Server or MySQL) database as a model source, we review the model and how information there impacts our overall design. The following diagram illustrates some of the model information in Mo+ for entities and properties of interest for our example:
We immediately see missing properties in our Customer, Product, and Order model classes, and a required order structure to allow for multiple products in an order. But, hold your horses! We are doing a test driven approach, we are not going to try to incorporate all of this information yet.
Refactor Model Oriented Code: We do notice that we can easily refactor our Repository class in a model oriented way to make it robust with respect to future changes in the model. I'm lazy, I think I can get away from writing a model oriented test first, since the code after model oriented refactoring should be virtually the same as before.
Refactoring model oriented code involves creating or modifying a code template (or more than one), and generating updated model oriented code. Following is a Mo+ code template for the main body of the Repository class (Mo+ template code will be displayed as images only for readability, actual code for the complete templates can be found in attached sample download). At line 16, the foreach statement iterates through each Entity in the solution of interest for adding to the repository, and at line 18 we are creating a repository for a particular entity, inserting EntityName information from the model. In our model, we are tagging interested entities with "ForDev" that we can use to limit our repository only to entities of interest right now:
Our generated Repository class looks like this (virtually identical to before):
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; }
}
Our unit tests still pass. See the code for Step 3 in the sample download. Note that generated files in the sample are named with "_G" to quickly identify them. The project level Mo+ code template for this step is called RepositoryCode.
A Little More Towards Handling Orders
In thinking about this little e-commerce scenario, we realize a couple of things that we need to address:
- A customer often logs in later to order
- A logged in customer places the order and retrieves order information
Consider a Change: So, we add one more user story:
Write a Custom Test: We create a unit test for this user story, and update our order product and get product tests to be based on the logged in customer:
[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");
}
}
Make a Custom Code Change: We make custom changes, moving the product search to the Product class and adding a list of orders and methods to support login and orders by customer in the Customer class:
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;
}
}
Our unit tests still pass. See the code for Step 4 in the sample download.
Incorporating Model Structure
We now want to utilize some of the overall customer, product, and order information found in our model. Incorporating a fair amount of model oriented code with custom code created with a test driven approach may seem daunting. But, as with our previous steps, we start with some unit tests and get them to pass.
Write a Custom Test: Let's start with a simple custom unit test. We know we want to hold all of our information in the repositories, and for each type of object, we need to be able to add them to the repository. So, let's start with this ProductCRUDTests class to test adding a Product to the repository:
[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);
}
}
Of course, this unit test fails since we don't have an AddProduct() method.
Make a Custom Code Change: So, let's add this missing method to the Product class:
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);
}
}
The unit tests pass again.
Write a Model Oriented Test: Writing a model oriented test involves creating or modifying a code template (or more than one) for a test, and generating updated model oriented test code. First we utilize our ProductCRUDTests class as the starting point for a template to add tests for each entity in the model. The Mo+ template for the test class body looks like the following (where we insert EntityName information from the model):
Next, with this template we generate the additional unit tests for each entity in the model. The ProductCRUDTests class body should be identical to the custom one we just made. The OrderCRUDTests class for example looks like the following:
[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);
}
}
We can now delete the custom ProductCRUDTests file since it is no longer needed. But of course, the additional unit tests such as OrderCRUDTests fail, since our corresponding model classes do not have add methods. So, it is time to refactor our model classes in a model oriented way!
Refactor Model Oriented Code: Now we want to do some model oriented refactoring for our model classes. To refactor our model classes, we use the Product class as a starting point for the template. We want the generated model class to contain the model properties and the add method. The Mo+ template for the model class body looks like the following (where we add information for each Property as shown in line 11, and we insert PropertyName and EntityName information from the model):
Next, with this template we generate the model class code for each entity in the model. The generated code for the Product class looks like the following:
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);
}
}
Oh, but now our code doesn't compile! Some of the generated properties and methods also exist in our custom code.
We need to remove the duplicate elements in our custom code. The custom code for the Product class looks like:
public static Product FindProduct(string productName)
{
Product product = Repository.Products.FirstOrDefault(i => i.ProductName == productName);
return product;
}
OK, everything compiles now, and also the unit tests pass! See the code for Step 5 in the sample download. The entity level Mo+ code templates for the CRUD unit test and model code are called CRUDTestCode and ModelClassCode respectively.
Utilizing Model StructureTowards Handling Orders
Now that we have incorporated the actual model structure, we can take another look at how we handle ordering. But first, let's make some custom changes to utilize our newly generated methods to add items to repositories. We replace direct calls to add items to the repositories in CustomerTests.Setup() and Customer.RegisterCustomer() (you can see changes in sample download). We run our unit tests again and they pass.
Now, let's take a look at our support for ordering given what we know now about the model structure. The unit test for placing an order is CustomerTests.TestOrderProduct():
[TestMethod]
public void TestOrderProduct()
{
Customer customer = Customer.FindCustomer("Jane Doe");
Order order = customer.OrderProduct("My Widget");
Assert.AreEqual(order.ProductName, "My Widget");
}
Write (modify) a Custom Test: We know that an order should not have a product name, but should have a set of order details containing product related information. If the customer is ordering one product, this unit test should look like:
[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");
}
This change causes compilation errors and we need to fix those first.
Make a Custom Code Change: We need to add the OrderDetails list to the custom Order class:
public partial class Order
{
public string ProductName { get; set; }
public List<OrderDetail> OrderDetails { get; set; }
}
Make a Custom Code Change: We need to add another FindProduct() method in the custom Product class:
public static Product FindProduct(int productID)
{
Product product = Repository.Products.FirstOrDefault(i => i.ProductID == productID);
return product;
}
OK, this compiles, but the TestOrderProduct() test fails. We see some issues in 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;
}
Make a Custom Code Change: Ordering a product should add an OrderDetail to the order with product information found in the product repository:
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;
}
OK, now the TestOrderProduct() test succeeds, but now we broke 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");
}
Write (modify) a Custom Test: We spot a couple of things wrong with the test, knowing that a customer should only be able to "get" a product that is part of a valid order for that customer. So this unit test should look more like:
[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");
}
Make a Custom Code Change: This of course causes a compilation error, and we refactor Customer.GetProduct() to an approach that returns a matching product within any order:
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;
}
At this point, you can also remove the ProductName property from Order. The unit tests pass again, and we have something that almost looks like an ordering process (though I wouldn't put any of my $ into it). See the code for Step 6 in the sample download.
Continue To Iterate Until You Are Satisfied
Of course this example is far from a real e-commerce system. In practice you will continue to interate in writing tests to cover your user stories, writing custom code to make them pass, refactoring custom code, and refactoring model oriented tests and code.
We will do one more iteration here.
Customize A Model Oriented Test: Let's beef up our AddProduct() test to add multiple products and verify that ids are unique (and rename method to TestAddProduct() while we are at it):
[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);
}
This test fails! The ids for both products are the same.
Customize Model Oriented Code: Let's just implement something to increment ids in Product.AddProduct():
public static void AddProduct(Product product)
{
product.ProductID = Repository.Products.Count + 1;
Repository.Products.Add(product);
}
OK, the unit tests pass again, whew!
Refactor Model Oriented Code: But we customized some model oriented code and need to refactor the model oriented unit tests and model code. To refactor, it is just a matter of updating the associated templates and regenerating the model oriented code.
See the code for step 7 in the sample download that shows the updated templates and updated model oriented code.
In Summary
Hopefully this article has provided you with some food for thought in considering model oriented development in conjunction with test driven development. Please provide some feedback if you would like to see additional clarity and refinement in the process description and example.
In addition, I hope you try Mo+ and the Mo+ Solution Builder to fully utilize incorporating a model oriented approach to your development. The free, open source product is available at moplus.codeplex.com. In addition to the product, this site contains sample packs for building more complete models and generating complete working applications. Video tutorials and other materials are also available at this site. The Mo+ Solution Builder also contains extensive on board help.
Become a Member!
The Mo+ community gets additional support, and contributes to the evolution of Mo+ via the web site at https://modelorientedplus.com. Being a member gives Mo+ users additional benefits such as additional forum support, member contributed tools, and the ability to vote on and/or contribute to the direction of Mo+. Also, we will be running monthly contests for members, where you can win $ for developing solutions using Mo+.
If you are the least bit interested in efficient model oriented software development, please sign up as a member. It's free, and you won't get spam email!