2. Background
• Experience: 10+ years as a developer of various systems
• Projects from the scratch: 8 (2 very large)
• Projects with legacy code: 6 (4 very large)
• Projects with good tests: 4 (1 with legacy code)
• Frameworks: xUnit, MsTests, NUnit
3. Rule 1: Use Bottom-Up (Inside-Out)
• For new systems → Top-Down (Outside-In)
• For systems with legacy code → Bottom-Up (Inside-Out)
• Reasons
• Cannot be properly designed upfront
• Cannot modify components
• Cannot grasp all components
• Cannot mock/stub legacy components
• Interactions have side effects
• Change as little as possible
• Balance
• When injecting the change → bottom-up
• When implementing the change → top-down
4. Rule 1: Example (way of thinking)
public class EuropeShop : Shop
{
public override void CreateSale()
{
var items = LoadSelectedItemsFromDb();
var taxes = new EuropeTaxes();
var saleItems = items.Select(item => taxes.ApplyTaxes(item)).ToList();
var cart = new Cart();
cart.Add(saleItems);
taxes.ApplyTaxes(cart);
SaveToDb(cart);
}
}
// TODO > NEW FEATURE > Send notification to accounting if cart total amount exceeds 1000
Source code: https://github.com/xtrmstep/ApplyingTddToLegacyCode
5. Rule 2: Test Modified Code ONLY
• Cannot grasp all use cases
• Writing tests for all use cases requires enormous time
• Legacy Code was accepted → works as expected
• Your modification is the only thing which breaks the
stable system
• Don’t bother about Code Coverage
6. Rule 2: Example (new feature)
public class EuropeShop : Shop
{
public override void CreateSale()
{
var items = LoadSelectedItemsFromDb();
var taxes = new EuropeTaxes();
var saleItems = items.Select(item => taxes.ApplyTaxes(item)).ToList();
var cart = new Cart();
cart.Add(saleItems);
taxes.ApplyTaxes(cart);
// TODO
SaveToDb(cart);
}
}
public class EuropeShop : Shop
{
public override void CreateSale()
{
var items = LoadSelectedItemsFromDb();
var taxes = new EuropeTaxes();
var saleItems = items.Select(item => taxes.ApplyTaxes(item)).ToList();
var cart = new Cart();
cart.Add(saleItems);
taxes.ApplyTaxes(cart);
new EuropeShopNotifier().Send(cart); // NEW FEATURE
SaveToDb(cart);
}
}
Source code: https://github.com/xtrmstep/ApplyingTddToLegacyCode
7. Rule 3: Test Requirements ONLY
• Users = classes which use your code
• Consider use cases of method, class, component
• Consider behaviour of the code
• BDD, Feature testing
• Extract responsibilities
8. Rule 3: Example (responsibilities)
public class EuropeShop : Shop
{
public override void CreateSale()
{
// 1) load from DB
var items = LoadSelectedItemsFromDb();
// 2) Tax-object creates SaleItem and
// 4) goes through items and apply taxes
var taxes = new EuropeTaxes();
var saleItems = items.Select(item => taxes.ApplyTaxes(item)).ToList();
// 3) creates a cart and 4) applies taxes
var cart = new Cart();
cart.Add(saleItems);
taxes.ApplyTaxes(cart);
new EuropeShopNotifier().Send(cart);
// 4) store to DB
SaveToDb(cart);
}
}
Source code: https://github.com/xtrmstep/ApplyingTddToLegacyCode
public class EuropeShop : Shop
{
public override void CreateSale()
{
// 1) extracted to a repository
var itemsRepository = new ItemsRepository();
var items = itemsRepository.LoadSelectedItems();
// 2) extracted to a mapper
var saleItems = items.ConvertToSaleItems();
// 3) still creates a cart
var cart = new Cart();
cart.Add(saleItems);
// 4) all routines to apply taxes are extracted to the Tax-object
new EuropeTaxes().ApplyTaxes(cart);
new EuropeShopNotifier().Send(cart);
// 5) extracted to a repository
itemsRepository.Save(cart);
}
}
9. Rule 3: Example (tests)
public class EuropeTaxesTests
{
public void Should_not_fail_for_null()
{
}
public void Should_apply_taxes_to_items()
{
}
public void Should_apply_taxes_to_whole_cart()
{
}
public void Should_apply_taxes_to_whole_cart_and_change_items()
{
}
}
Source code: https://github.com/xtrmstep/ApplyingTddToLegacyCode
public class EuropeShopNotifierTests
{
public void Should_not_send_when_less_or_equals_to_1000()
{
// arrange
var cart = new Cart();
var notifier = new EuropeShopNotifier();
// add items to cart
// act
notifier.Send(cart);
// assert
// PROBLEM: how to verify if we don't have access to the object?
}
public void Should_send_when_greater_than_1000()
{
}
public void Should_raise_exception_when_cannot_send()
{
}
}
10. Rule 4: Inject Tested Code ONLY
• Don’t change legacy code
• Use technique Sprout method/class
• New testable code is called from the old not-testable
• Old code is not changed
• Use technique Wrap method
• New code goes before/after the old not-testable one
• Create a new method to call the new and old one
11. Rule 4: Example (Sprout method)
public class EuropeShop : Shop
{
public override void CreateSale()
{
var items = LoadSelectedItemsFromDb();
var taxes = new EuropeTaxes();
var saleItems = items.Select(item => taxes.ApplyTaxes(item)).ToList();
var cart = new Cart();
cart.Add(saleItems);
taxes.ApplyTaxes(cart);
// TODO
SaveToDb(cart);
}
}
public class EuropeShop : Shop
{
public override void CreateSale()
{
var items = LoadSelectedItemsFromDb();
var taxes = new EuropeTaxes();
var saleItems = items.Select(item => taxes.ApplyTaxes(item)).ToList();
var cart = new Cart();
cart.Add(saleItems);
taxes.ApplyTaxes(cart);
new EuropeShopNotifier().Send(cart); // Sprout method
SaveToDb(cart);
}
}
Source code: https://github.com/xtrmstep/ApplyingTddToLegacyCode
12. Rule 4: Example (Wrap method)
public class EuropeTaxes : Taxes
{
internal override SaleItem ApplyTaxes(Item item)
{
var saleItem = new SaleItem(item)
{
SalePrice = item.Price*1.2m
};
return saleItem;
}
internal override void ApplyTaxes(Cart cart)
{
if (cart.TotalSalePrice <= 300m) return;
var exclusion = 30m/cart.SaleItems.Count;
foreach (var item in cart.SaleItems)
if (item.SalePrice - exclusion > 100m)
item.SalePrice -= exclusion;
}
}
public class EuropeTaxes : Taxes
{
internal override void ApplyTaxes(Cart cart)
{
ApplyToItems(cart);
ApplyToCart(cart);
}
private void ApplyToItems(Cart cart)
{
foreach (var item in cart.SaleItems)
item.SalePrice = item.Price*1.2m;
}
private void ApplyToCart(Cart cart)
{
if (cart.TotalSalePrice <= 300m) return;
var exclusion = 30m / cart.SaleItems.Count;
foreach (var item in cart.SaleItems)
if (item.SalePrice - exclusion > 100m)
item.SalePrice -= exclusion;
}
}
Source code: https://github.com/xtrmstep/ApplyingTddToLegacyCode
13. Rule 5: Break Hidden Dependencies
• Stop instantiate classes in methods
• Extract initialization to constructor
• Replace usages of class with usages of interface/abstract
• Low coupling, High cohesion
• Extract giant methods to classes
• Remove violations of Law of Demeter
“Cohesion partitions your functionality so that it is concise and closest to
the data relevant to it, whilst decoupling ensures that the functional
implementation is isolated from the rest of the system.”
- Adrian Regan @ stackoverflow.com
14. Rule 5: Example (dependencies)
public class EuropeShop : Shop
{
public override void CreateSale()
{
var itemsRepository = new ItemsRepository();
var items = itemsRepository.LoadSelectedItems();
var saleItems = items.ConvertToSaleItems();
var cart = new Cart();
cart.Add(saleItems);
new EuropeTaxes().ApplyTaxes(cart);
new EuropeShopNotifier().Send(cart);
itemsRepository.Save(cart);
}
}
public class EuropeShop : Shop
{
private readonly IItemsRepository _itemsRepository;
private readonly Taxes.Taxes _europeTaxes;
private readonly INotifier _europeShopNotifier;
public override void CreateSale()
{
var items = _itemsRepository.LoadSelectedItems();
var saleItems = items.ConvertToSaleItems();
var cart = new Cart();
cart.Add(saleItems);
_europeTaxes.ApplyTaxes(cart);
_europeShopNotifier.Send(cart);
_itemsRepository.Save(cart);
}
}
Source code: https://github.com/xtrmstep/ApplyingTddToLegacyCode
Skipped by now, but we can use Factory to isolate
15. Rule 6: The Less “Big” tests, the Better
• “Big” tests
• Integration tests
• End-to-end tests
• UI tests
• Complexity grows by exponent
• Double time to maintain and write
• Special cases:
• Performance tests
• Load tests
• DON’T substitute unit tests with integration tests
16. Rule 7: Don’t Test Private Methods
• Responsibility violation sign
• Fragile tests
• Not WHAT class does, but HOW it does
• Don’t convert private methods to public ones
• Extract and use internal for module inside logic
• Consider new classes for private methods
• Consider internal classes as complete tools
17. Rule 7: Example (test)
public class EuropeTaxes : Taxes {
public override void ApplyTaxes(Cart cart) {
ApplyToItems(cart);
ApplyToCart(cart);
}
private void ApplyToItems(Cart cart) {
foreach (var item in cart.SaleItems)
item.SalePrice = item.Price*1.2m;
}
private void ApplyToCart(Cart cart) {
if (cart.TotalSalePrice <= 300m) return;
var exclusion = 30m / cart.SaleItems.Count;
foreach (var item in cart.SaleItems)
if (item.SalePrice - exclusion > 100m)
item.SalePrice -= exclusion;
}
}
public class EuropeTaxesTests {
public void Should_not_fail_for_null() {
}
public void Should_apply_taxes_to_items() {
}
public void Should_apply_taxes_to_whole_cart() {
}
public void Should_apply_taxes_to_whole_cart_and_change_items() {
}
public void Should_apply_taxes_to_cart_greater_300() {
}
}
Source code: https://github.com/xtrmstep/ApplyingTddToLegacyCode
18. Rule 7: Example (test body)
public void Should_apply_taxes_to_cart_greater_300()
{
// arrange
// list of items which will create a cart greater 300
var saleItems = new List<Item>(new []{new Item {Price = 83.34m},
new Item {Price = 83.34m},new Item {Price = 83.34m}})
.ConvertToSaleItems();
var cart = new Cart();
cart.Add(saleItems);
const decimal expected = 83.34m*3*1.2m;
// act
new EuropeTaxes().ApplyTaxes(cart);
// assert
Assert.Equal(expected, cart.TotalSalePrice);
}
Source code: https://github.com/xtrmstep/ApplyingTddToLegacyCode
19. Rule 8: Don’t Test Algorithms
• Fragile tests
• Don’t care about number of calls
• Don’t care about which method is executed
• Do care about effects
20. Rule 9: Stop Modifying Legacy Code w/o Tests
• Add tests to Definition of Done & Code Review
• May look as a waste of time at the early stage
• Cumulative result at the later stage
• Always practicing
21. See Also
• Book “Working Effectively with Legacy Code” by Michael Feathers
• https://danlimerick.wordpress.com/2012/04/25/tdd-when-up-to-your-neck-in-legacy-code/
• https://danlimerick.wordpress.com/2012/06/11/breaking-hidden-dependencies/
• https://danlimerick.wordpress.com/2012/04/25/the-legacy-code-lifecycle/
• https://www.quora.com/Should-you-unit-test-private-methods-on-a-class
• http://blog.ploeh.dk/2015/09/22/unit-testing-internals/
• https://medium.com/javascript-scene/5-common-misconceptions-about-tdd-unit-tests-863d5beb3ce9#.uav3gih3k
• http://www.daedtech.com/intro-to-unit-testing-5-invading-legacy-code-in-the-name-of-testability/
• https://en.wikipedia.org/wiki/Law_of_Demeter