Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Legacy Code Kata v3.0

1,921 views

Published on

A code kata in C# to help practice techniques for safely removing dependencies form legacy code and creating unit tests. Questions? Suggestions? Contact @dubmun.

Published in: Software
  • Be the first to comment

  • Be the first to like this

Legacy Code Kata v3.0

  1. 1. Legacy Code Kata v3.0 William Munn, CGFWB
  2. 2. You Need This Stuff • Seed Legacy Code • https://github.com/KatasForLegacyCode/kCSharp/ archive/Step0.zip • An IDE • Visual Studio 2012+ • An nUnit test runner (I recommend nCrunch) • OR Jetbrains Rider
  3. 3. Legacy Code
  4. 4. Read This Book
  5. 5. Dependency
  6. 6. Code Kata • A code kata is an exercise in programming which helps a us hone our skills through practice and repetition. • The word kata is taken from Japanese arts most traditionally Martial Arts • Why a code kata? Because we learn by doing. We Begin With Practice
  7. 7. Dependency Kata: Initial Structure DoItAllTests.cs Database.cs DoItAll.cs UserDetails.cs ConsoleAdapter.cs Program.cs Logger.cs Existing Files We Will Create
  8. 8. Test1 Test2 Test3 Test4 Build Dependency Kata: Kata Barometer
  9. 9. Dependency Kata: First Things First Let’s start by trying to get the existing test running. • Currently, running the integration test times out. Why? • DEPENDENCIES! • Begin by abstracting and breaking the dependency on Console.Readline() DoItAll.cs Test1 Test2 Test3 Test4 Build
  10. 10. Dependency Kata: First Things First We’ll use an adapter, but we could use the Console.SetIn method and a TextReader. • Create an interface public interface IConsoleAdapter { string GetInput(); } • Create a class that implements the interface & implement method to handle our dependency public class ConsoleAdapter : IConsoleAdapter { public string GetInput() { return Console.ReadLine(); } } ConsoleAdapter.cs ConsoleAdapter.cs Did you know? In software engineering, the adapter pattern is a software design pattern that allows the interface of an existing class to be used as another interface. Test1 Test2 Test3 Test4 Build
  11. 11. Dependency Kata: First Things First • Create new constructor to accept IConsoleAdapter & set private variable private readonly IConsoleAdapter _consoleAdapter; public DoItAll(IConsoleAdapter consoleAdapter) { _consoleAdapter = consoleAdapter; } DoItAll.cs Test1 Test2 Test3 Test4 Build
  12. 12. Dependency Kata: First Things First • Replace 4 calls to Console.ReadLine() WITH _consoleAdapter.GetInput() • Replace 2 calls to Console.ReadKey() WITH _consoleAdapter.GetInput() DoItAll.cs DoItAll.cs Test1 Test2 Test3 Test4 Build
  13. 13. Dependency Kata: First Things First We have broken instantiations of DoItAll • Instantiate and pass in our new handler var doItAll = new DoItAll(new ConsoleAdapter());Program.cs Test1 Test2 Test3 Test4 Build
  14. 14. Dependency Kata: First Things First Let’s update our integration test or it will also fail. • Create a new implementation of IConsoleAdapter public class DoItAllTests { […] var doItAll = new DoItAll(new FakeConsoleAdapter()); […] } public class FakeConsoleAdapter : IConsoleAdapter { public string GetInput() { return string.Empty; } } DoItAllTests.cs Test1 Test2 Test3 Test4 Build
  15. 15. Dependency Kata: First Things First • When the test is run we now get a meaningful exception. All of the dependencies that caused the hang are gone. • the test is now failing because the password is empty. This is a meaningful case but let’s just update our fake for now. public string GetInput() { return “someTestString”; } DoItAllTests.cs Test1 Test2 Test3 Test4 Build
  16. 16. Dependency Kata: First Things First • Following good testing practice, let’s add an assert: [Test, Category("Integration")] public void DoItAll_Does_ItAll() { var doItAll = new DoItAll(new FakeConsoleAdapter()); Assert.That(() => doItAll.Do(), Throws.Nothing); } • Our test should still pass. DoItAllTests.cs Test1 Test2 Test3 Test4 Build
  17. 17. Dependency Kata: Better Coverage Some of our legacy code has no coverage and produces no quantifiable results • Copy the existing test and rename it DoItAll_Fails_ToWriteToDB [Test, Category("Integration")] public void DoItAll_Does_ItAll() { […] } [Test, Category("Integration")] public void DoItAll_Fails_ToWriteToDB() { […] } DoItAllTests.cs Test1 Test2 Test3 Test4 BuildTest1 Test2 Test3 Test4 Build
  18. 18. Dependency Kata: Better Coverage • Change the assert Assert.That(doItAll.Do(), Is.StringContaining("Database.SaveToLog Exception:”)); • Build will fail because our Do() method doesn’t return anything. DoItAllTests.cs Test1 Test2 Test3 Test4 Build
  19. 19. Dependency Kata: Better Coverage • Change Do()’s return type to string public string Do() • Add/update return statements in 3 locations: public string Do() { […] if (_userDetails.PasswordEncrypted […]) { […] return string.Empty; } […] { […] using (var writer = new StreamWriter("log.txt", true)) { […] return string.Empty; } } […] return string.Empty; } DoItAll.cs DoItAll.cs Test1 Test2 Test3 Test4 Build
  20. 20. Dependency Kata: Better Coverage Now create variables to hold the messages to return public string Do() { […] if (_userDetails.PasswordEncrypted […]) { var passwordsDontMatch = "The passwords don't match."; […] } […] { […] using (var writer = new StreamWriter("log.txt", true)) { var errorMessage = $"{message} - Database.SaveToLog Exception: rn{ex.Message}"; […] } } […] } DoItAll.cs Test1 Test2 Test3 Test4 Build
  21. 21. Dependency Kata: Better Coverage Return the new values as appropriate public string Do() { […] if (_userDetails.PasswordEncrypted […]) { var passwordsDontMatch = "The passwords don't match."; Console.WriteLine(passwordsDontMatch); _consoleAdapter.GetInput(); return passwordsDontMatch; } […] { […] using (var writer = new StreamWriter("log.txt", true)) { var errorMessage = $"{message} - Database.SaveToLog Exception: rn{ex.Message}"; writer.WriteLine(errorMessage); return errorMessage; } } } DoItAll.cs Test1 Test2 Test3 Test4 Build
  22. 22. Dependency Kata: Better Abstraction Abstract Console completely by adding the following code: • Add a new IConsoleAdapter method void SetOutput(string output); • Update ConsoleAdapter implementation public void SetOutput(string output) { Console.WriteLine(output); } ConsoleAdapter.cs ConsoleAdapter.cs Test1 Test2 Test3 Test4 Build
  23. 23. Dependency Kata: More Abstraction Update FakeConsoleAdapter implementation public void SetOutput(string output) {} DoItAllTests.cs Test1 Test2 Test3 Test4 Build
  24. 24. Dependency Kata: More Abstraction • Update DoItAll Implementation • Replace 5 instances of Console.WriteLine WITH _consoleAdapter.SetOutput • Replace 1 instance of Console.Write WITH _consoleAdapter.SetOutput DoItAll.cs Test1 Test2 Test3 Test4 Build
  25. 25. Dependency Kata: Refactor DoItAll.Do() is trying to do too much. Let’s Refactor! Write a TEST Logger.cs Test1 Test2 Test3 Test4 Build
  26. 26. Dependency Kata: Refactor DoItAll.Do() is trying to do too much. Let’s Refactor! • Extract logging functionality by creating a new interface public interface ILogger { string LogMessage(string message); } • Create a new class implementing ILogger public class Logger : ILogger { public string LogMessage(string message) { throw new NotImplementedException(); } } Logger.cs Logger.cs Test1 Test2 Test3 Test4 Build
  27. 27. Dependency Kata: Refactor • Copy the entire try/catch block in DoItAll.Do() into the implementation of Logger.LogMessage() • Modify var errorMessage to be outputMessage • Make sure all paths return a outputMessage public string LogMessage(string message) { var outputMessage = message; […] return outputMessage; } Logger.cs Test1 Test2 Test3 Test4 Build
  28. 28. Dependency Kata: Refactor • Copy the entire try/catch block in DoItAll.Do() into the implementation of Logger.LogMessage() • Modify var errorMessage to be outputMessage • Make sure all paths return a outputMessage public string LogMessage(string message) { var outputMessage = message; try { Database.SaveToLog(outputMessage); } catch (Exception ex) { // If database write fails, write to file using (var writer = new StreamWriter("log.txt", true)) { outputMessage = $"{message} - Database.SaveToLog Exception: rn{ex.Message}"; writer.WriteLine(message); } } return outputMessage; } Logger.cs Test1 Test2 Test3 Test4 Build
  29. 29. Dependency Kata: Refactor • Update the constructor of DoItAll with an ILogger private readonly ILogger _logger; public DoItAll(IConsoleAdapter consoleAdapter, ILogger logger) { _consoleAdapter = consoleAdapter; _logger = logger; } […] DoItAll.cs Test1 Test2 Test3 Test4 Build
  30. 30. Dependency Kata: Refactor • Update the Do() method public string Do() { […] _consoleAdapter.SetOutput(message); message = _logger.LogMessage(message); _consoleAdapter.GetInput(); return message; } DoItAll.cs Test1 Test2 Test3 Test4 Build
  31. 31. Dependency Kata: Refactor We have broken instantiations of DoItAll • Instantiate and pass in our new handler var doItAll = new DoItAll(new ConsoleAdapter(), new Logger()); Program.cs Test1 Test2 Test3 Test4 Build
  32. 32. Dependency Kata: Refactor Let’s update BOTH tests. • Create a new implementation of ILogger public class DoItAllTests { […] var doItAll = new DoItAll(new FakeConsoleAdapter(), new FakeLogger()); […] } […] public class FakeLogger : ILogger { public string LogMessage( string message) { return string.Empty; } } DoItAllTests.cs Test1 Test2 Test3 Test4 Build
  33. 33. Dependency Kata: Refactor • Create a third test by copying the second test and rename the copy DoItAll_Succeeds_WithFakeLogging • Update the copied assert Assert.That(doItAll.Do(), Is.EqualTo(string.Empty)); • Now change the second test DoItAll_DoesItAll_FailsToWriteToDB to be an integration test by depending on Logger and it will pass as well var doItAll = new DoItAll(new FakeConsoleAdapter(), new Logger()); DoItAllTests.cs DoItAllTests.cs Test1 Test2 Test3 Test4 Build DoItAllTests.cs
  34. 34. Dependency Kata: What’s Next? Remove static keyword from the Database class and use an interface and DI. New test file. With fake and with concrete. New interface for UserDetails. Extract password validation to UserDetails method: HasValidPassword Extract class UserRegistrationCommunication for Messages to users. Add IoC Container? Add Isolation Framework (ala Moq)? Show ReSharper/Rider common refactorings?
  35. 35. Dependency Kata: What’s Next? There is still plenty to refactor in this legacy code. • What would you do next?
  36. 36. William Munn twitter - @dubmun Github – dubmun BLOG – http://dubmun.com Resources Working Effectively with Legacy Code, Michael Feathers The Art Of Unit Testing, Roy Osherove Clean Code, Robert C. Martin Pragmatic Programmer, Dave Thomas & Andy Hunt Refactoring, Martin Fowler Legacy Code Kata – V3.0

×