Your SlideShare is downloading. ×
0
Unit testing legacy code
Unit testing legacy code
Unit testing legacy code
Unit testing legacy code
Unit testing legacy code
Unit testing legacy code
Unit testing legacy code
Unit testing legacy code
Unit testing legacy code
Unit testing legacy code
Unit testing legacy code
Unit testing legacy code
Unit testing legacy code
Unit testing legacy code
Unit testing legacy code
Unit testing legacy code
Unit testing legacy code
Unit testing legacy code
Unit testing legacy code
Unit testing legacy code
Unit testing legacy code
Unit testing legacy code
Unit testing legacy code
Unit testing legacy code
Unit testing legacy code
Unit testing legacy code
Unit testing legacy code
Unit testing legacy code
Unit testing legacy code
Unit testing legacy code
Unit testing legacy code
Upcoming SlideShare
Loading in...5
×

Thanks for flagging this SlideShare!

Oops! An error has occurred.

×
Saving this for later? Get the SlideShare app to save on your phone or tablet. Read anywhere, anytime – even offline.
Text the download link to your phone
Standard text messaging rates apply

Unit testing legacy code

423

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 …

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
1 Like
Statistics
Notes
  • Be the first to comment

No Downloads
Views
Total Views
423
On Slideshare
0
From Embeds
0
Number of Embeds
3
Actions
Shares
0
Downloads
10
Comments
0
Likes
1
Embeds 0
No embeds

Report content
Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
No notes for slide

Transcript

  • 1. Unit testing legacy code Lars Thorup ZeaLake Software Consulting May, 2014
  • 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. 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. 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. What comes first: the test or the refactoring?
  • 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. 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. 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. 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. 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. 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. Remove the characterization tests ● When unit test code coverage is good enough ● To speed up feedback ● To avoid test duplication
  • 13. Small steps - elephant carpaccio ● Any big refactoring... ● ...can be done in small steps ● Demo: Security system (see slide 19 through 31)
  • 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. 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. 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. A big refactoring is needed...
  • 18. Avoid feature branches ● For features as well as large refactorings ● Delayed integration ● Increases risk ● Increases cost
  • 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. Step 10: Clean up ● After proper validation in production, delete the old implementation

×