Having trouble wrapping you mind around unit testing in legacy code? Practice this kata and you'll have a good understanding of some basics. Break dependencies, inject stubs, write meaningful tests. Refactor with confidence. Version 2 is a complete overhaul to make the kata more readable and usable.
3. Seed Code is written in C#
Get it from Github:
https://github.com/KatasForLegacyCode/kCSharp/archive/Step0.zip
Any version of Visual Studio 2012/2013/2015 that can run
console apps and unit tests.
An nUnit test runner.
I recommend nCrunch.
9. 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.
10. Legacy Dependency Kata: V2.0
Written in C#
Less tool dependencies
Better theme
Easier to read and follow
Bug fixes
12. Let’s start by getting the existing test running!
Running the integration test FAILS
Begin by abstracting and breaking the dependency on Console.Readline()
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
DoItAll.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.
13. Create new constructor to accept IConsoleAdapter & set private variable
private readonly IConsoleAdapter _consoleAdapter;
public DoItAll(IConsoleAdapter consoleAdapter)
{
_consoleAdapter = consoleAdapter;
}
Replace 4 calls to Console.ReadLine() WITH _consoleAdapter.GetInput()
Replace 2 calls to Console.ReadKey() WITH _consoleAdapter.GetInput()
DoItAll.cs
DoItAll.cs
DoItAll.cs
14. We have broken instantiations of DoItAll
Instantiate and pass in our new handler
var doItAll = new DoItAll(new ConsoleAdapter());
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;
}
}
Program.cs
DoItAllTests.cs
15. 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”;
}
Run the test
AND IT SHOULD BE GREEN!
DoItAllTests.cs
16. 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
17. 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()
{
[…]
}
Change the assert
Assert.That(doItAll.Do(), Is.StringContaining("Database.SaveToLog Exception:”));
Build will fail because our Do() method returns void.
DoItAllTests.cs
DoItAllTests.cs
18. 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
19. 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 = string.Format("{0} - Database.SaveToLog Exception: rn{1}",
message, ex.Message);
[…]
}
}
[…]
}
DoItAll.cs
20. Return the new values as appropriate
public string Do()
{
[…]
if (_userDetails.PasswordEncrypted […])
{
[…]
Console.WriteLine(passwordsDontMatch);
Console.ReadKey();
return passwordsDontMatch;
}
[…]
{
[…]
using (var writer = new StreamWriter("log.txt", true))
{
[…]
writer.WriteLine(errorMessage);
return errorMessage;
}
}
}
Our new DoItAll_Fails_ToWriteToDB() test should pass
DoItAll.cs
21. Abstract Console completely
Add a new method stub
void SetOutput(string output);
Update ConsoleAdapter implementation
public void SetOutput(string output)
{
Console.WriteLine(output);
}
Update FakeConsoleAdapter implementation
public void SetOutput(string output)
{}
Update DoItAll Implementation
Replace 6 instances of Console.WriteLine AND Console.Write WITH
_consoleAdapter.SetOutput
ConsoleAdapter.cs
ConsoleAdapter.cs
DoItAllTests.cs
DoItAll.cs
OUR TESTS SHOULD STILL PASS
22. 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();
}
}
Now extract the code in the try/catch block in DoItAll.Do() into the implementation of ILogger.LogMessage()
Make sure all paths return a message
Now update the constructor of DoItAll with an ILogger AND REPLACE TRY/CATCH:
message = _logger.LogMessage(message);
Logger.cs
Logger.cs
23. Extract the code in the try/catch block in DoItAll.Do()
into the implementation of Logger.LogMessage()
Make sure all paths return a message
public string LogMessage(string message)
{
try
{
Database.SaveToLog(message);
}
catch (Exception ex)
{
// If database write fails, write to file
using (var writer = new StreamWriter("log.txt", true))
{
message = message + "nDatabase.SaveToLog Exception: " + ex.Message;
writer.WriteLine(message);
}
}
return message;
}
Logger.cs
24. Update the constructor of DoItAll with an ILogger
public DoItAll(IOutputInputAdapter ioAdapter, ILogger logger)
{
_ioAdapter = ioAdapter;
_logger = logger;
}
[…]
private readonly ILogger _logger;
Update the Do() method
public string Do()
{
[…]
_ioAdapter.SetOutput(message);
message = _logger.LogMessage(message);
return message;
}
DoItAll.cs
DoItAll.cs
25. We have broken instantiations of DoItAll
Instantiate and pass in our new handler
var doItAll = new DoItAll(new ConsoleAdapter(), new Logger());
Let’s update our tests or they will also fail.
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;
}
}
Program.cs
DoItAllTests.cs
OUR FIRST TEST PASSES, BUT THE SECOND FAILS
26. Copy the second test and rename the copy
DoItAll_Succeeds_WithMockLogging
Update the copied assert
Assert.That(doItAll.Do(), Is.EqualTo(string.Empty));
Let the original test remain an integration test by depending on
Logger and it will pass as well
DoItAllTests.cs
DoItAllTests.cs
27. There is still plenty to refactor in this legacy code.
What would you do next?
The code is written in C#, so anyone who can write C++ or Java shouldn’t have any trouble following along. However, I don’t have seed code for you.
I only bought 5 flash drives in case on network issues… I only expected maybe 10-15 people…
So what IS legacy code? As I gained experience I came to think of legacy code as COBOL, Fortran, PHP… (kidding I know people love PHP somewhere out there). They seemed to fit… How many of you are concerned with maintaining written on COBOL or FORTRAN? 10% 25% 50% MORE?
I have a slightly wider and maybe 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.
Dependency can have a quasi-positive meaning even though Google Image Search disagrees. If you are depending on well maintained libraries, abstract classes and interfaces, professional tooling with good history of support; that isn’t necessarily a bad thing.
The alternative is 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. It doesn’t require as much thought, but the effects are disastrous to maintainability and extensibility. Not to mention the fact that it is basically impossible to unit test.
This is a dependency graph of a codebase with no abstractions. We can’t easily make sense of this…
Who has seen a dependency graph that looks like this or worse (this isn’t HealthEquity’s dependency graph btw). This exists, I’ve seen a couple that were considerably worse at companies I’ve worked for.
This is what we are trying to prevent or move away from.
Unfortunately, this isn’t likely to happen.
Go home and practice this kata 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.