Successfully reported this slideshow.
Your SlideShare is downloading. ×

How to Start Test-Driven Development in Legacy Code

Ad
Ad
Ad
Ad
Ad
Ad
Ad
Ad
Ad
Ad
Ad

Check these out next

1 of 73 Ad

More Related Content

Slideshows for you (20)

Advertisement

Similar to How to Start Test-Driven Development in Legacy Code (20)

Recently uploaded (20)

Advertisement

How to Start Test-Driven Development in Legacy Code

  1. 1. How to Start Test-Driven Development in Legacy Code The New York XP & Agile Meetup May 17, 2012 Daniel Wellman Director of Technology Tweet #legacytdd #xpnyc
  2. 2. A Quick Survey
  3. 3. “red, green, refactor” by Török Gábor
  4. 4. Yay, TDD!
  5. 5. Boo, my project!
  6. 6. Get to Safety
  7. 7. Be Persistent
  8. 8. Photo by the Dorothy Peyton Gray Transportation Library and Archive at the Los Angeles County Metropolitan Transportation Authority
  9. 9. “Another flaw in the human character is that everybody wants to build and nobody wants to do Kurt Vonnegut maintenance.” Hocus Pocus
  10. 10. Photo by Fraser Speirs
  11. 11. Get to Safety
  12. 12. TDD Cycle Red Green Refactor
  13. 13. What makes code hard to test?
  14. 14. Highly Coupled Code
  15. 15. Smaller Code
  16. 16. How do I test this?
  17. 17. Seams Photo by Art Crimes a.k.a. Susan M. Coles
  18. 18. Legacy TDD Cycle Introduce Seams Red Green Refactor
  19. 19. ??
  20. 20. Break dependencies just enough to get tests in place
  21. 21. Use Safe Refactoring Tools (if you have them)
  22. 22. Or move very slowly and carefully
  23. 23. A Few Techniques
  24. 24. The Challenge of ‘new’
  25. 25. public class TestCase { public void run() { TestResult result = new TestResult(); try { setUp(); runTest(result); } catch (Exception e) { result.addFailure(e, this); } tearDown(); } }
  26. 26. public class TestCase { public void run() { TestResult result = new TestResult(); try { setUp(); runTest(result); } catch (Exception e) { result.addFailure(e, this); } tearDown(); } }
  27. 27. public class TestCase { public void run() { TestResult result = new TestResult(); try { setUp(); runTest(result); } catch (Exception e) { result.addFailure(e, this); } tearDown(); @Test } } public void exceptionsReportAsFailures() { TestCase testCase = new TestCase(); testCase.run(); // ...?? }
  28. 28. After Extract Parameter public class TestCase { public void run(TestResult testResult) { TestResult result = testResult; try { setUp(); runTest(result); } catch (Exception e) { result.addFailure(e, this); } tearDown(); } }
  29. 29. After Extract Parameter public class TestCase { public void run(TestResult testResult) { TestResult result = testResult; try { setUp(); runTest(result); @Test } catch (Exception e) { result.addFailure(e, this); public void exceptionsReportAsFailures() { } TestCase testCase = new TestCase(); tearDown(); TestResult result = new TestResult(); } testCase.run(result); } // Make assertions on result }
  30. 30. The Dangerous “Live Wire”
  31. 31. public class MailingListDispatcher { private MailService mailService; public MailingListDispatcher(Status status) { this.mailService = new SMTPMailService("smtp.gmail.com"); // ... } public Result send(Message message) { // ... mailService.send(message); // ... } }
  32. 32. public class MailingListDispatcher { private MailService mailService; public MailingListDispatcher(Status status) { this.mailService = new SMTPMailService("smtp.gmail.com"); // ... } public Result send(Message message) { // ... mailService.send(message); @Test // ... public void testSendSuccessful() { } MailingListDispatcher dispatch = } new MailingListDispatcher(OK); dispatch.send(new Message()); // ... ??? }
  33. 33. After Parameterize Constructor public class MailingListDispatcher { private MailService mailService; public MailingListDispatcher( MailService service, Status status) { this.mailService = service; // ... } public Result send(Message message) { // ... mailService.send(message); // ... } }
  34. 34. After Parameterize Constructor public class MailingListDispatcher { private MailService mailService; public MailingListDispatcher( MailService service, Status status) { this.mailService = service; // ... } public Result send(Message message) { @Test public// ... void testSendSuccessful() throws Exception { MailService mailService = new FakeMailService(); mailService.send(message); MailingListDispatcher dispatch = // ... } new MailingListDispatcher(mailService, OK); dispatch.send(new Message()); // Inspect the FakeMailService } }
  35. 35. Can’t Make a Fake?
  36. 36. public class SMTPMailServer { public void helo(String hostname) {} public void from(String address) {} public void size(long byteCount) {} public void data() {} public void send(Message message) {} public void quit() {} ... private int sendBytes(byte[] toSend) {} ... }
  37. 37. Override Methods public class FakeOverrideSMTPMailServer extends SMTPMailServer { public byte[] sent; // public! @Override protected int sendBytes(byte[] toSend) { // Save the bytes for later inspection // ... return 0; // Whatever... } }
  38. 38. Override Methods public class FakeOverrideSMTPMailServer extends SMTPMailServer { public byte[] sent; // public! @Override protected int sendBytes(byte[] toSend) { // Save the bytes for later inspection // ... return 0; // Whatever... public class SMTPMailServer { } } // ... protected int sendBytes(byte[] toSend) { } ... }
  39. 39. Extract Interface public interface MailServer { public void helo(String hostname); public void send(Message message); public void quit(); }
  40. 40. Extract Interface sh -i public interface MailServer { public void helo(String hostname); public void send(Message message); public void quit(); }
  41. 41. Extract Interface sh -i public interface MailServer { public void helo(String hostname); public void send(Message message); public void quit(); } public class SMTPMailServer implements MailServer { // Retro-fitted production... }
  42. 42. Extract Interface sh -i public interface MailServer { public void helo(String hostname); public void send(Message message); public void quit(); } public class SMTPMailServer implements MailServer { // Retro-fitted production... } public class FakeMailServer implements MailServer { // Testing ... }
  43. 43. Long Method?
  44. 44. public Duration vacationRemaining(Date today) { Duration remaining; // ... // ... // Calculate hours worked // ... // ... // How much vacation do they get? // ... // ... // Is it a leap year? // ... // ... return remaining; }
  45. 45. Extract Methods public Time hoursWorked(Time start, Time end) { // ... } public int vacationDaysAllowed(EmployeeType type) { // ... } public boolean isLeapMonth(Date date) { // ... }
  46. 46. Not much time to change?
  47. 47. Sprout Method and Sprout Class “sprouting bean” photo by Zoë Jackson
  48. 48. public class EmployeeTable { private List<Employee> employees; public String table() { StringBuilder html = new StringBuilder(); html.append("<table>"); for (Employee employee : employees) { html.append("<tr>"); // Print employee data ... html.append("</tr>"); } html.append("</table>"); return html.toString(); } }
  49. 49. public class EmployeeTable { private List<Employee> employees; public String table() { StringBuilder html = new StringBuilder(); html.append("<table>"); for (Employee employee : employees) { html.append("<tr>"); // Print employee data ... html.append("</tr>"); TODO: } Show count html.append("</table>"); of full-time return html.toString(); } employees }
  50. 50. Sprout Class public class FullTimeEmployeeCount { private List<Employee> employees; public FullTimeEmployeeCount( List<Employee> employees) { this.employees = employees; } public int count() { int count = 0; for (Employee employee : employees) { if (employee.isFullTime()) count++; } return count; } }
  51. 51. public String table() { StringBuilder html = new StringBuilder(); html.append("<table>"); for (Employee employee : employees) { html.append("<tr>"); // Print employee data ... html.append("</tr>"); } html.append("<tr>"); html.append("<td colspan='3'>"); html.append( new FullTimeEmployeeCount(employees).count()); html.append("</td>"); html.append("</tr>"); html.append("</table>"); return html.toString(); }
  52. 52. Crazy Third-Party API?
  53. 53. You are not alone
  54. 54. Google
  55. 55. End-to-End Tests?
  56. 56. Be Persistent
  57. 57. It’s going to be hard
  58. 58. 12 9 6 3 0 Iteration 20 Iteration 21 Iteration 22 Iteration 23 Iteration 24 Velocity
  59. 59. What if management doesn’t think TDD is valuable?
  60. 60. Any amount is an improvement over nothing
  61. 61. Many small steps add up over time
  62. 62. Do it every day
  63. 63. Don’t be afraid to delete and reshape
  64. 64. Be Creative
  65. 65. Pair Programming
  66. 66. Spread experts around
  67. 67. Get to Safety Be Persistent
  68. 68. Yay, TDD on my project!
  69. 69. Upcoming Event “How to Hire and Retain Developers” by Debbie Madden, EVP Monday @ 6 PM General Assembly http://generalassemb.ly/education/ More upcoming NYC and Boston Cyrus events at http://www.cyrusinnovation.com/about/events
  70. 70. Questions? Discussion! Twitter: @wellman E-mail: dwellman@cyrusinnovation.com www.cyrusinnovation.com Twitter: @cyrusinnovation

Editor's Notes

  • \n
  • \n
  • Learned about XP, TDD appealed to me.\n
  • ... there is a sense of &amp;#x201C;Yay, pair programming!&amp;#x201D;\n
  • &amp;#x201C;TDD sounds great, but it won&amp;#x2019;t work on my project.&amp;#x201D;\n
  • Story: I&amp;#x2019;m a Survivor\n
  • \n
  • I&amp;#x2019;m not saying it will be easy. Many legacy projects are in maintenance mode. And maintenance is generally something that doesn&amp;#x2019;t feel particularly exciting or associated with new technology.\n
  • But I think this may be a general part of the human condition. \nOr at least Kurt Vonnegut did. \n\n\n
  • In maintenance it can feel as if the all the fun stuff has ended and the rest of the work is trivial. To get in a mindset where working with and testing legacy code can be fun,\n
  • ... I offer you this quote from Michael Feathers \n&amp;#x201C;Design is a continuous process, even when a project is in maintenance mode.&amp;#x201D;\n\n
  • CALL TO ACTION - If you are a programmer and you haven&amp;#x2019;t read this book, read it!\nDescribes:\n1. How to figure out what legacy code is doing\n2. How to add tests to legacy code bases\n3. Includes a catalog of patterns to make code easier to test\n
  • \n
  • This is the TDD Cycle. But on legacy code, even the first step of &amp;#x201C;Getting to Red&amp;#x201D; can be unpredictably hard.\nAnd so to get in to this loop, we need to figure out how to test our code -- and why it&amp;#x2019;s hard to test it.\n
  • \n
  • Highly coupled code is difficult to test because it can be hard to get small units to operate in isolation, and it can be hard to observe the effect you&amp;#x2019;re trying to test for.\n\nTesting Legacy Code is all about improving and refactoring the design.\n\nThat usually means decoupling highly-coupled code, and making big structures smaller.\n
  • = Easier to reason about and test\n
  • So how do you add tests to code that wasn&amp;#x2019;t designed for testing?\n
  • &amp;#x201C;A seam is a place where you can alter behavior in your program without editing in that place.&amp;#x201D; - Feathers\n\nWhy Seams? They are places where it becomes possible to replace undesirable production behavior or observe changes.\n
  • Sometimes the seams are there already, so you can just use them.\nBut if there isn&amp;#x2019;t a seam, you may need to change your code to create one.\n\n
  • But how do you change the code if you&amp;#x2019;re supposed to have a test for it in the first place?\n
  • The initial result may look weird, and maybe even ugly, but over time these tend to get smoothed over.\n\nAnd how do you break these dependencies safely?\n
  • \n
  • Concentrate on making the minimal amount of changes to get you into a place where you can test.\nAnd personally, I find that.... (finding seams can be fun!)\n
  • Finding seams can be fun!\n\n\n
  • ! Language Disclaimer !\n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • If you want, you can leave the single argument constructor from before and have it delegate to the new one, so that production code is left unchanged.\n
  • Do whatever it takes!\n
  • \n
  • The &amp;#x2018;sent&amp;#x2019; byte array is public, which we&amp;#x2019;d probably never do in production code, but it till get us going for our tests.\nAnd we may not know what &amp;#x2018;sendBytes&amp;#x2019; needs to return, so we just send\n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n\n
  • ... and you don&amp;#x2019;t want to break the habit of writing a test first and then production code.\n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • Tend to prevent more regressions in the project at first\nEasier to sell to new developers\nBut massive amounts can be expensive\n... Generally a few catch many deployment-related issues.\nStory: It&amp;#x2019;s all Selenium\n
  • \n
  • 1. New People (to TDD. and maybe the code)\n2. Legacy Code\n3. Political\n
  • \n
  • \n
  • \n
  • Being persistent\n
  • - bugs \n- new features\n
  • \n
  • \n
  • Pairing can help\n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • I&amp;#x2019;m not saying it will be easy. Many legacy projects are in maintenance mode. And maintenance is generally something that doesn&amp;#x2019;t feel particularly exciting or associated with new technology.\n
  • \n
  • Many techniques are available for this\nDebugging\nRefactoring for learning, then reverting\nCharacterization Tests\n

×