Unit testing legacy code

1,351 views

Published on

Unit testing and test-driven development are practices that makes it easy and efficient to create well-structured and well-working code. However, many software projects didn't create unit tests from the beginning.

In this presentation I will show a test automation strategy that works well for legacy code, and how to implement such a strategy on a project. The strategy focuses on characterization tests and refactoring, and the slides contain a detailed example of how to carry through a major refactoring in many tiny steps

Published in: Software
0 Comments
3 Likes
Statistics
Notes
  • Be the first to comment

No Downloads
Views
Total views
1,351
On SlideShare
0
From Embeds
0
Number of Embeds
136
Actions
Shares
0
Downloads
37
Comments
0
Likes
3
Embeds 0
No embeds

No notes for slide

Unit testing legacy code

  1. 1. Unit testing legacy code Lars Thorup ZeaLake Software Consulting May, 2014
  2. 2. Who is Lars Thorup? ● Software developer/architect ● JavaScript, C# ● Test Driven Development ● Continuous Integration ● Coach: Teaching TDD and continuous integration ● Founder of ZeaLake ● @larsthorup
  3. 3. The problems with legacy code ● No tests ● The code probably works... ● Hard to refactor ● Will the code still work? ● Hard to extend ● Need to change the code... ● The code owns us :( ● Did our investment turn sour?
  4. 4. How do tests bring us back in control? ● A refactoring improves the design without changing behavior ● Tests ensure that behavior is not accidentally changed ● Without tests, refactoring is scary ● and with no refactoring, the design decays over time ● With tests, we have the courage to refactor ● so we continually keep our design healthy
  5. 5. What comes first: the test or the refactoring?
  6. 6. How do we get to sustainable legacy code? ● Make it easy to add characterization tests ● Have good unit test coverage for important areas ● Don't worry about code you don't need to change ● Test-drive all new code ● Now we own the code :)
  7. 7. Making legacy code sustainable ● Select an important area ● Driven by change requests ● Add characterization tests ● Make code testable ● Refactor the code ● Add unit tests ● Remove characterization tests ● Small steps
  8. 8. Characterization tests ● Characterize current behavior ● Integration tests ● Either high level unit tests ● Or end-to-end tests ● Don't change existing code ● Faulty behavior = current behavior: don't change it! ● Make a note to fix later ● Test at a level that makes it easy ● The characterization tests are throw-aways ● Demo: ● Web service test: VoteMedia ● End-to-end browser test: entrylist.demo.test.js
  9. 9. Make code testable ● Avoid large methods ● They require a ton of setup ● They require lots of scenarios to cover all variations ● Avoid outer scope dependencies ● They require you to test at a higher level ● Avoid external dependencies ● ... a ton of setup ● They slow you down
  10. 10. Refactor the code ● Add interface ● Inject a mock instead of the real thing ● Easier setup ● Infinitely faster Notifier EmailSvc IEmailSvc EmailSvcStub NotifierTest ● Extract method ● Split up large methods ● To simplify unit testing single behaviors ● Demo: VoteWithVideo_Vimas ● Add parameter ● Pass in outer-scope dependencies ● The tests can pass in their own dummy values ● Demo: Entry.renderResponse
  11. 11. Add unit tests ● Now that the code is testable... ● Write unit tests for you small methods ● Pass in dummy values for parameters ● Mock dependencies ● Rinse and repeat...
  12. 12. Remove the characterization tests ● When unit test code coverage is good enough ● To speed up feedback ● To avoid test duplication
  13. 13. Small steps - elephant carpaccio ● Any big refactoring... ● ...can be done in small steps ● Demo: Security system (see slide 19 through 31)
  14. 14. Test-drive all new code ● Easy, now that unit testing tools are in place Failing test Succeeding test Good design Refactor Test Intention Think, talk Code
  15. 15. Making legacy code sustainable ● Select an important area ● Driven by change requests ● Add characterization tests ● Make code testable ● Refactor the code ● Add unit tests ● Remove characterization tests ● Small steps
  16. 16. It's not hard - now go do it! ● This is hard ● SQL query efficiency ● Cache invalidation ● Scalability ● Pixel perfect rendering ● Cross-browser compatibility ● Indexing strategies ● Security ● Real time media streaming ● 60fps gaming with HTML5 ● ... and robust Selenium tests! ● This is not hard ● Refactoring ● Unit testing ● Dependency injection ● Automated build and test ● Continuous Integration ● Fast feedback will make you more productive ● ... and more happy
  17. 17. A big refactoring is needed...
  18. 18. Avoid feature branches ● For features as well as large refactorings ● Delayed integration ● Increases risk ● Increases cost
  19. 19. Use feature toggles ● Any big refactoring... ● ...can be done in small steps ● Allows us to keep development on trunk/master ● Drastically lowering the risk ● Commit after every step ● At most a couple of hours
  20. 20. Security example ● Change the code from the old security system ● To our new extended security model interface IPrivilege { bool HasRole(Role); } class Permission { bool IsAdmin(); }
  21. 21. Step 0: existing implementation ● Code instantiates Legacy.Permission ● and calls methods like permission.IsAdmin() ● ...all over the place ● We want to replace this with a new security system void SomeController() { var p = new Permission(); if (p.IsAdmin()) { ... } }
  22. 22. Step 1: New security implementation ● Implements an interface ● This can be committed gradually interface IPrivilege { bool HasRole(Role); } class Privilege : IPrivilege { bool HasRole(Role r) { ... } }
  23. 23. Step 2: Wrap the old implementation ● Create Security.LegacyPermission ● Implement new interface ● Wrap existing implementation ● Expose existing implementation class LegacyPermission : IPrivilege { LegacyPermission(Permission p) { this.p = p; } bool HasRole(Role r) { if (r == Role.Admin) return p.IsAdmin(); return false; } Permission Permission { get: { return p; } } private Permission p; }
  24. 24. Step 3: Factory ● Create a factory ● Have it return the new implementation ● Unless directed to return the wrapped old one class PrivilegeFactory { IPrivilege Create(bool old=true) { if(!old) { return new Privilege(); } return new LegacyPermission(); } }
  25. 25. Step 4: Test compatibility ● Write tests ● Run all tests against both implementations ● Iterate until the new implementation has a satisfactory level of backwards compatibility ● This can be committed gradually [TestCase(true)] [TestCase(false)] void HasRole(bool old) { // given var f = new PrivilegeFactory(); var p = f.Create(old); // when var b = p.HasRole(Role.Admin); // then Assert.That(b, Is.True); }
  26. 26. Step 5: Dumb migration ● Replace all uses of the old implementation with the new wrapper ● Immediately use the exposed old implementation ● This can be committed gradually void SomeController() { var priv = f.Create(true) as LegacyPermission; var p = priv.Permission; if (p.IsAdmin()) { ... } }
  27. 27. Step 6: Actual migration ● Rewrite code to use the new implementation instead of the exposed old implementation ● This can be committed gradually void SomeController() { var p = f.Create(true); if (p.HasRole(Role.Admin) { ... } }
  28. 28. Step 7: Verify migration is code complete ● Delete the property exposing the old implementation ● Go back to previous step if the code does not compile ● Note: at this point the code is still using the old implementation everywhere! class LegacyPermission : IPrivilege { ... // Permission Permission // { // get: { return p; } // } private Permission p; }
  29. 29. Step 8: Verify migration works ● Allow QA to explicitly switch to the new implementation ● We now have a Feature Toggle ● Do thorough exploratory testing with the new implementation ● If unintented behavior is found, go back to step 4 and add a new test that fails for this reason, fix the issue and repeat class PrivilegeFactory { IPrivilege Create(bool old=true) { var UseNew = %UseNew%; if(!old || UseNew) { return new Privilege(); } return new LegacyPermission(); } }
  30. 30. Step 9: Complete migration ● Always use the new implementation ● Mark the old implementation as Obsolete to prevent new usages class PrivilegeFactory { IPrivilege Create() { return new Privilege(); } } [Obsolete] class Permission { ... }
  31. 31. Step 10: Clean up ● After proper validation in production, delete the old implementation

×