A code kata in C# to help practice techniques for safely removing dependencies form legacy code and creating unit tests. Questions? Suggestions? Contact @dubmun.
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
7. 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
8. Dependency Kata: Initial Structure
DoItAllTests.cs
Database.cs
DoItAll.cs
UserDetails.cs
ConsoleAdapter.cs
Program.cs
Logger.cs
Existing Files
We Will
Create
10. 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
11. 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
12. 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
13. 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
14. 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
15. 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
16. 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
17. 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
18. 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
19. 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
20. 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
21. 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
22. 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
23. 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
24. Dependency Kata: More Abstraction
Update FakeConsoleAdapter implementation
public void SetOutput(string output)
{}
DoItAllTests.cs
Test1 Test2 Test3 Test4 Build
25. 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
27. 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
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;
[…]
return outputMessage;
}
Logger.cs
Test1 Test2 Test3 Test4 Build
29. 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
30. 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
32. 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
33. 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
34. 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
35. 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?
36. Dependency Kata: What’s Next?
There is still plenty to refactor in this legacy code.
• What would you do next?
37. 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
Notas del editor
Renamed for your pleasure.
Mandatory Dilbert. Is legacy code, just code someone else wrote? Nah. So what IS legacy code? As I gained experience I came to think of legacy code as COBOL, Fortran, PHP… They seemed to fit the description my professors gave. Today have a much wider and scarier definition. According to Working Effectively with Legacy Code by Michael Feathers: legacy code is code without unit tests.
SO… if you have code without unit tests, it is legacy code because you cannot modify, refactor, or fix it without the very real danger of breaking it. Unit tests add a layer of safety to enable these actions.
The word “dependency” can refer to code that may be completely procedural and has little/no abstractions. This kind of dependent code can be addictive to write because it is often quicker and easier to write—at first. It doesn’t require as much thought, but the effects are disastrous to maintainability and extensibility. Not to mention the fact that it is usually impossible to unit test.
Unfortunately, this isn’t likely to happen before lunch. Removing dependencies from existing code so that it can be unit tested is hard.
A kata is based on the martial arts kata or form. It is training through repetition and guidance. Go home and practice this kata once a and your brain will learn how to deal with legacy dependencies. You’ll get so good at it that is will seem like second nature.
At first glance, the class structure may not seem too unreasonable. Data access has it’s own class and so do the UserDetails.
Then we see the DoItAll class with the Do() method. Naming aside, this is unfortunately pretty common in several codebases I’ve seen.
Someone who touched this code had good intentions. There is a unit test project with a single failing unit test. Probably because they weren’t sure how to deal with the dependency on the console.
You’ll see this barometer throughout the kata. It will help you know what state your code should be in after each step.