1 / 25

Writing Testable Code

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.

sargent
Download Presentation

Writing Testable Code

An Image/Link below is provided (as is) to download presentation Download Policy: Content on the Website is provided to you AS IS for your information and personal use and may not be sold / licensed / shared on other websites without getting consent from its author. Content is provided to you AS IS for your information and personal use only. Download presentation by click this link. While downloading, if for some reason you are not able to download a presentation, the publisher may have deleted the file from their server. During download, if you can't get a presentation, the file might be deleted by the publisher.

E N D

Presentation Transcript


  1. Writing Testable Code by Jon Kruger

  2. 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.

  3. 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; } }

  4. 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); } }

  5. 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!

  6. 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!

  7. 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; } }

  8. 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…

  9. 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.

  10. 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!

  11. 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.

  12. We just refactoreduntestablecode and made it testable!

  13. 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

  14. Rules for writing testable code

  15. Rule #1: Don’t new up dependencies

  16. Rule #2: Don’t do real work in constructors

  17. Rule #3: Don’t expose static anything

  18. Rule #4: Don’t expose singletons

  19. Rule #5: Entity objects should not have external dependencies

  20. 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.

  21. 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.

  22. 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.

  23. 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 } }

  24. 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

  25. ?

More Related