by jon kruger. when calculating the total price of an order, add the price of the products in the...

25
Writing Testable Code by Jon Kruger

Upload: kacie-clint

Post on 29-Mar-2015

212 views

Category:

Documents


0 download

TRANSCRIPT

Page 1: By Jon Kruger. 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

Writing Testable Code

by Jon Kruger

Page 2: By Jon Kruger. 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

Implement this codeWhen 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.

Page 3: By Jon Kruger. 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

Implementation without tests

public class OrderProcessor{ public decimal CalculateTotalPrice(int orderId) { // load order from database var order = Database.GetOrder(orderId); var totalPriceOfAllProducts = 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; }}

Page 4: By Jon Kruger. 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

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

Page 5: By Jon Kruger. 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

Test cases neededWe 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 < 25Order total is 25 exactlyOrder 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!

Page 6: By Jon Kruger. 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

Let’s break this downWhat 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!

Page 7: By Jon Kruger. 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

public class OrderProcessor{ public decimal CalculateTotalPrice(int orderId) { // load order from database var order = Database.GetOrder(orderId); var totalPriceOfAllProducts = 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; }}

Page 8: By Jon Kruger. 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

public class OrderProcessor{ private readonly TaxCalculator _taxCalculator; private readonly ShippingCalculator _shippingCalculator;

public OrderProcessor(TaxCalculator taxCalculator, ShippingCalculator shippingCalculator) { _taxCalculator = taxCalculator; _shippingCalculator = shippingCalculator; }

public decimal CalculateTotalPrice(int orderId) { // load order from database var order = Database.GetOrder(orderId); var totalPriceOfAllProducts = 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…

Page 9: By Jon Kruger. 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

public class OrderProcessor{ private readonly ITaxCalculator _taxCalculator; private readonly IShippingCalculator _shippingCalculator;

public OrderProcessor(ITaxCalculator taxCalculator, IShippingCalculator shippingCalculator) { _taxCalculator = taxCalculator; _shippingCalculator = shippingCalculator; }

public decimal CalculateTotalPrice(int orderId) { // load order from database var order = Database.GetOrder(orderId); var totalPriceOfAllProducts = 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.

Page 10: By Jon Kruger. 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

public class OrderProcessor{ private readonly ITaxCalculator _taxCalculator; private readonly IShippingCalculator _shippingCalculator;

public OrderProcessor(ITaxCalculator taxCalculator, IShippingCalculator shippingCalculator) { _taxCalculator = taxCalculator; _shippingCalculator = shippingCalculator; }

public decimal CalculateTotalPrice(int orderId) { // load order from database var order = Database.GetOrder(orderId); var totalPriceOfAllProducts = 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!

Page 11: By Jon Kruger. 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

public class OrderProcessor{ private readonly IGetObjectService<Order> _getOrderService; private readonly ITaxCalculator _taxCalculator; private readonly IShippingCalculator _shippingCalculator;

public OrderProcessor(IGetObjectService<Order> getOrderService, ITaxCalculator taxCalculator, IShippingCalculator shippingCalculator) { _getOrderService = getOrderService; _taxCalculator = taxCalculator; _shippingCalculator = shippingCalculator; }

public decimal CalculateTotalPrice(int orderId) { // load order from database var order = _getOrderService.Get(orderId); var totalPriceOfAllProducts = 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.

Page 12: By Jon Kruger. 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

We just refactored untestable code and made it testable!

Page 13: By Jon Kruger. 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

What is testable code?Testable code is code that can we can test using a unit test instead of an integration testProvides a way to substitute fake objects for classes that the class that we’re testing depends onConsistent results on every test runManual configuration is not needed before test runOrder of tests do not matterMust be able to run only some of the testsTests must run fast

Page 14: By Jon Kruger. 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

Rules for writing testable code

Page 15: By Jon Kruger. 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

Rule #1: Don’t new up dependencies

Page 16: By Jon Kruger. 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

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

Page 17: By Jon Kruger. 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

Rule #3: Don’t expose static

anything

Page 18: By Jon Kruger. 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

Rule #4: Don’t expose singletons

Page 19: By Jon Kruger. 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

Rule #5: Entity objects should not

have external dependencies

Page 20: By Jon Kruger. 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

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.

Page 21: By Jon Kruger. 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

Rule #6: Follow the Law of Demeter

public class OrderDisplayService{ private readonly IOrderProcessor _orderProcessor;

public OrderDisplayService(IOrderProcessor orderProcessor) { _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.

Page 22: By Jon Kruger. 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

Rule #6: Follow the Law of Demeter

public class OrderDisplayService{ private readonly IOrderProcessor _orderProcessor;

public OrderDisplayService(IOrderProcessor orderProcessor) { _orderProcessor = orderProcessor; }

public void ShowOrderDetails(Order order) { if (!_orderProcessor.IsAuthenticated) { throw new InvalidOperationException( "not logged in"); } // do more stuff }}

Encapsulate “IsAuthenticated” inside IOrderProcessor.

Page 23: By Jon Kruger. 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

Rule #6: Follow the Law of Demeter

public class OrderDisplayService{ private IOrderProcessor _orderProcessor; private IUserAuthenticationService _userAuthenticationService;

public OrderDisplayService(IOrderProcessor orderProcessor, IUserAuthenticationService userAuthenticationService) { _orderProcessor = orderProcessor; _userAuthenticationService = userAuthenticationService; }

public void ShowOrderDetails(Order order) { if (!_userAuthenticationService.IsAuthenticated) { throw new InvalidOperationException( "not logged in"); } // do more stuff }}

Page 24: By Jon Kruger. 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

RecapDon’t new up dependenciesDon’t do real work in constructorsDon’t expose static anythingDon’t expose singletonsEntity objects should not have external dependenciesFollow the Law of Demeter

Page 25: By Jon Kruger. 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

?