280 likes | 335 Views
Writing Testable Code. by Jon Kruger. Implement this code. When calculating the total price of an order, add the price of the products in the order, the tax, and the shipping charges. Tax rate in Ohio = 7%, Michigan = 6.5%, other states = 0%. Can ship to US only.
E N D
Writing Testable Code by Jon Kruger
Implement this code When calculating the total price of an order, add the price of the products in the order, the tax, and the shipping charges. Tax rate in Ohio = 7%, Michigan = 6.5%, other states = 0%. Can ship to US only. Shipping charges = $5 if cost of products is less than $25, otherwise shipping is free.
Implementation without tests public class OrderProcessor { public decimal CalculateTotalPrice(intorderId) { // load order from database var order = Database.GetOrder(orderId); vartotalPriceOfAllProducts = order.Products.Sum(p => p.Price); // calculate tax decimal tax = 0; if (order.State == "OH") tax = totalPriceOfAllProducts * .07m; else if (order.State == "MI") tax = totalPriceOfAllProducts * .065m; // calculate shipping decimal shippingCharges = 0; if (totalPriceOfAllProducts < 25) shippingCharges = 5; return totalPriceOfAllProducts + tax + shippingCharges; } }
First attempt at a test [TestFixture] public class OrderProcessorTests { private decimal _totalPrice; private Order _order; [Test] public void CalculateTotalPrice() { Given_an_order(); When_calculating_the_total_price_of_an_order(); Then_the_total_price_of_the_order_should_be(15.70m); } [TearDown] public void Cleanup() { Database.DeleteOrder(_order); } private void Given_an_order() { _order = new Order { Id = 1, State = "OH", Products = new List<Product> { new Product {Price = 10} } }; Database.SaveOrder(_order); } private When_calculating_the_total_price_of_an_order() { _totalPrice= new OrderProcessor().CalculateTotalPrice(1); } private Then_the_total_price_of_the_order_should_be(decimal amount) { _totalPrice.ShouldEqual(amount); } }
Test cases needed We have to account for the following scenarios when writing our tests: Tax (51 possibilities) • Orders can be shipped to 51 states (50 states + DC) Shipping (3 possibilities) • Order total is < 25 • Order total is 25 exactly • Order total is > 25 Loading Order from the database (1 possibility) Return the sum of products, tax, and shipping (1 possibility) This means that we have 153 different combinations to test!
Let’s break this down What if we tested each individual piece of the order total calculating process in isolation? • Test tax calculation in each state (51 tests) • Test shipping calculation (3 tests) • Test that Order can be loaded from the database (1 test) • Test that return value is price of products + tax + shipping (1 test) Now we’re down to 56 test cases from 153 test cases!
public class OrderProcessor { public decimal CalculateTotalPrice(intorderId) { // load order from database var order = Database.GetOrder(orderId); vartotalPriceOfAllProducts = order.Products.Sum(p => p.Price); // calculate tax decimal tax = new TaxCalculator().CalculateTax(order); // calculate shipping decimal shippingCharges = new ShippingCalculator().CalculateShipping(order); return totalPriceOfAllProducts + tax + shippingCharges; } }
public class OrderProcessor { private readonlyTaxCalculator _taxCalculator; private readonlyShippingCalculator _shippingCalculator; public OrderProcessor(TaxCalculatortaxCalculator, ShippingCalculatorshippingCalculator) { _taxCalculator = taxCalculator; _shippingCalculator = shippingCalculator; } public decimal CalculateTotalPrice(intorderId) { // load order from database var order = Database.GetOrder(orderId); vartotalPriceOfAllProducts = order.TotalPriceOfAllProducts; // calculate tax decimal tax = _taxCalculator.CalculateTax(order); // calculate shipping decimal shippingCharges = _shippingCalculator.CalculateShipping(order); return totalPriceOfAllProducts + tax + shippingCharges; } } Now maybe I could create test classes that derive from TaxCalculator and ShippingCalculator…
public class OrderProcessor { private readonlyITaxCalculator _taxCalculator; private readonlyIShippingCalculator _shippingCalculator; public OrderProcessor(ITaxCalculatortaxCalculator, IShippingCalculatorshippingCalculator) { _taxCalculator = taxCalculator; _shippingCalculator = shippingCalculator; } public decimal CalculateTotalPrice(intorderId) { // load order from database var order = Database.GetOrder(orderId); vartotalPriceOfAllProducts = order.TotalPriceOfAllProducts; // calculate tax decimal tax = _taxCalculator.CalculateTax(order); // calculate shipping decimal shippingCharges = _shippingCalculator.CalculateShipping(order); return totalPriceOfAllProducts + tax + shippingCharges; } } Now I don’t have to worry about whether those dependencies have virtual methods.
public class OrderProcessor { private readonlyITaxCalculator _taxCalculator; private readonlyIShippingCalculator _shippingCalculator; public OrderProcessor(ITaxCalculatortaxCalculator, IShippingCalculatorshippingCalculator) { _taxCalculator = taxCalculator; _shippingCalculator = shippingCalculator; } public decimal CalculateTotalPrice(intorderId) { // load order from database var order =Database.GetOrder(orderId); vartotalPriceOfAllProducts = order.TotalPriceOfAllProducts; // calculate tax decimal tax = _taxCalculator.CalculateTax(order); // calculate shipping decimal shippingCharges = _shippingCalculator.CalculateShipping(order); return totalPriceOfAllProducts + tax + shippingCharges; } } I still can’t stub out the database access… I can’t take a static class in as a constructor parameter!
public class OrderProcessor { private readonlyIGetObjectService<Order> _getOrderService; private readonlyITaxCalculator _taxCalculator; private readonlyIShippingCalculator _shippingCalculator; public OrderProcessor(IGetObjectService<Order> getOrderService, ITaxCalculatortaxCalculator, IShippingCalculatorshippingCalculator) { _getOrderService = getOrderService; _taxCalculator = taxCalculator; _shippingCalculator = shippingCalculator; } public decimal CalculateTotalPrice(intorderId) { // load order from database var order = _getOrderService.Get(orderId); vartotalPriceOfAllProducts = order.TotalPriceOfAllProducts; // calculate tax decimal tax = _taxCalculator.CalculateTax(order); // calculate shipping decimal shippingCharges = _shippingCalculator.CalculateShipping(order); return totalPriceOfAllProducts + tax + shippingCharges; } } I removed the static class and replaced it with a non-static class hidden behind an interface.
What is testable code? • Testable code is code that can we can test using a unit test instead of an integration test • Provides a way to substitute fake objects for classes that the class that we’re testing depends on • Consistent results on every test run • Manual configuration is not needed before test run • Order of tests do not matter • Must be able to run only some of the tests • Tests must run fast
Rule #5: Entity objects should not have external dependencies
Rule #6: Follow the Law of Demeter The Law of Demeter states that a method of an object may only call methods of: 1) The object itself. 2) An argument of the method. 3) Any object created within the method. 4) Any direct properties/fields of the object.
Rule #6: Follow the Law of Demeter public class OrderDisplayService { private readonlyIOrderProcessor _orderProcessor; public OrderDisplayService(IOrderProcessororderProcessor) { _orderProcessor = orderProcessor; } public void ShowOrderDetails(Order order) { if (!_orderProcessor.UserAuthenticationService.IsAuthenticated) { throw new InvalidOperationException( "not logged in"); } // do more stuff } } Stubbing “IsAuthenticated” in a test would be difficult.
Rule #6: Follow the Law of Demeter public class OrderDisplayService { private readonlyIOrderProcessor _orderProcessor; public OrderDisplayService(IOrderProcessororderProcessor) { _orderProcessor = orderProcessor; } public void ShowOrderDetails(Order order) { if (!_orderProcessor.IsAuthenticated) { throw new InvalidOperationException( "not logged in"); } // do more stuff } } Encapsulate “IsAuthenticated” inside IOrderProcessor.
Rule #6: Follow the Law of Demeter public class OrderDisplayService { private IOrderProcessor _orderProcessor; private IUserAuthenticationService _userAuthenticationService; public OrderDisplayService(IOrderProcessororderProcessor, IUserAuthenticationServiceuserAuthenticationService) { _orderProcessor = orderProcessor; _userAuthenticationService = userAuthenticationService; } public void ShowOrderDetails(Order order) { if (!_userAuthenticationService.IsAuthenticated) { throw new InvalidOperationException( "not logged in"); } // do more stuff } }
Recap • Don’t new up dependencies • Don’t do real work in constructors • Don’t expose static anything • Don’t expose singletons • Entity objects should not have external dependencies • Follow the Law of Demeter