Top Banner
Test Driven Development in the context of large legacy codebases
33

Tdd for legacy code

May 20, 2015

Download

Technology

Short introduction on how to mix TDD and working with legacy code for an effective process recipe.
Welcome message from author
This document is posted to help you gain knowledge. Please leave a comment to let me know what you think about it! Share it to your friends and learn new things together.
Transcript
Page 1: Tdd for legacy code

Test Driven Development in the context of large legacy codebases

Page 2: Tdd for legacy code

Summary

• TDD – general workflow• Applying TDD when working with legacy code• Methods for breaking dependencies in

existing code• Adding and testing new features

Page 3: Tdd for legacy code

Test Driven Development

1. Write a failing test case.2. Get it to compile.3. Make it pass (do notchange existing code). 4. Remove duplication.5. Repeat.

Design

Implement

TestTest

Page 4: Tdd for legacy code

Design

• We need a method that parses some file and creates an instance of a class based on the parsed information.

Page 5: Tdd for legacy code

Test [TestMethod()] public void ParseProcedureNameTest() { var target = new TCPHandler();

var filePath = Path.GetFullPath("D:\\Projects\\packages\\dutconfig\\main\\ bin\\wind\\Resources\\test_procedures.tcp");

ITCPStructure tcpStructure = target.Parse(filePath); Assert.AreEqual(tcpStructure .Name, "test_procedures.tcp"); }

Page 6: Tdd for legacy code

Implement public static TCPStructure Parse(string summaryFilePath) { TCPStructure tcpStructure = new TCPStructure();

reader = new StreamReader(summaryFilePath);

XmlSerializer tcpSerializer = new XmlSerializer(typeof(ComposerTestStorageSummary));

ComposerTestStorageSummary storageSummaryBase = (ComposerTestStorageSummary)tcpSerializer.Deserialize(reader);

//main procedure name tcpStructure.Name = storageSummaryBase.Name;

return tcpStructure; }

Page 7: Tdd for legacy code

Test

• Run the test• If the test fails, go back to implementation• If the test passes, continue.

Page 8: Tdd for legacy code

Repeat!

Design:We need more information from the file….

Design

Implement

TestTest

Page 9: Tdd for legacy code

Three laws of TDD

• First Law You may not write production code until you have written a failing unit test.

• Second Law You may not write more of a unit test than is sufficient to fail, and not compiling is failing.

• Third Law You may not write more production code than is sufficient to pass the currently failing test.

Page 10: Tdd for legacy code

TDD and Legacy Code

• 0. Get the code you want to change under test.• 1. Write a failing test case.• 2. Get it to compile.• 3. Make it pass. (Try not to change existing

code as you do this.)• 4. Remove duplication.• 5. Repeat.

Page 11: Tdd for legacy code

Design

Implement

Test

Find the code you need to change

Get this code in a test harness

Test

Page 12: Tdd for legacy code

Coping with Legacy Code

• Legacy code = code without tests• When we change code, we should have tests

in place. To put tests in place, we often have to change code => the legacy code dilemma

Page 13: Tdd for legacy code

How do we do it?

1. Identify change points.2. Find test points.3. Break dependencies.4. Write tests.5. Make changes and refactor.

Page 14: Tdd for legacy code

Break dependencies when..

• A method cannot be easily run in a test harness

• A class cannot be easily instantiated in a test harness

=>Fakes get too complex (we need to keep the test code as maintainable as production code)

Page 15: Tdd for legacy code

Dependency breaking techniques

Page 16: Tdd for legacy code

Extract Interface

• Create a new interface• Make the class that you are extracting from

implement this interface• Change the calling methods so that they use

this interface instead of the original class=> fakes can implement a much easier interface and the code gets cleaner

Page 17: Tdd for legacy code

private string BuildCompositeExpression(VariableInfo variable){ … foreach (VariableInfo varInfo in variable.statistics) { if (varInfo != null) { // create tcl variable value if (varInfo.scalarValue != null) { varValue.Append(formatVariableValueForTcl(varInfo.scalarValue.ToString())); } else if (varInfo.vectorValue != null) { … foreach (Object element in varInfo.vectorValue) { varValue.AppendFormat(“{0} ”,formatVariableValueForTcl(element.ToString())); } } if (varInfo.group != "") { varName.Append(variable.name); varName.Append("."); varName.Append(varInfo.group); ……

Page 18: Tdd for legacy code

[Serializable]

public class VariableInfo : Value, ILightVariableInfo

{

public VariableInfo();

public VariableInfo(eValueType i_valueType);

public VariableInfo(string i_name, eValueType i_valueType);

public VariableInfo(VariableInfo i_variableInfo);

public VariableInfo(IVariableInfo i_varInfo);

public VariableInfo(IValue i_value);

public VariableInfo(IComposite i_composite);

public virtual string name { get; set; }

public virtual string description { get; set; }

public virtual string units { get; set; }

public virtual string group { get; set; }

public virtual string fullName { get; }

public eStatisticType statisticType { get; set; }

public List<VariableInfo> statistics { get; set; }

public new eValueType valueType { get; set; }

  ……………

}

Page 19: Tdd for legacy code

interface ILightVariableInfo{ string Name { get; set; } string Group{ get; set; } object scalarValue { get; } IList vectorValue { get; } List<ILightVariableInfo> Statistics { get; set; }}

Page 20: Tdd for legacy code

class LightVariableInfo : Ixia.TestComposer.Interpreter.api.ILightVariableInfo{ #region Constructors

public LightVariableInfo(string i_name, string i_group, object i_scalarValue, IList i_vectorValue) { name = i_name; group = i_group; scalarValue = i_scalarValue; vectorValue = i_vectorValue; }

#endregion

#region ILightVariableInfo Members

public List<ILightVariableInfo> Statistics { get; set; }

public string group {get; set; }

public string name {get; set; }

public object scalarValue{get; private set; } public IList vectorValue {get; private set; }

#endregion}

Page 21: Tdd for legacy code

Subclass and Override

• Find the smallest set of methods that you need to override to achieve the test’s goal

• Make each of these methods overridable. If required, adjust visibility.

• Create a subclass that overrides these methods and implement the behavior you need

=> simple Fake objects

Page 22: Tdd for legacy code
Page 23: Tdd for legacy code
Page 24: Tdd for legacy code

Break Out Method Object

• Extract the method in a new class. • Define a constructor for this class with the

same signature as the method => parameters become class members

• Lean on the compiler• Replace the code in the original method to use

an instance of the new class.=> Tests are easier to write

Page 25: Tdd for legacy code

eRegressionRunResult CalculatePassFail(IRegressionRun i_run)

=>

class PassFailCalculator{ private IRegressionRun m_regressionRun; private int m_failedTestsCount; ...

public PassFailCalculator(IRegressionRun i_run) { … } public eRegressionRunResult CalculateResult() { … }}

Page 26: Tdd for legacy code

Introduce Static Setter

• Test == mini-application: totally isolated from the other tests => we need to relax the singleton property– Introduce static setter– Add a new method that resets the singleton

property and gets called when the tests are being initialized

Page 27: Tdd for legacy code

public sealed partial class ModuleManager{ public static ModuleManager Instance{…} …}=>public sealed partial class ModuleManager{ public static ModuleManager Instance{…} public static void ResetInstance() { instance = null; }}

Page 28: Tdd for legacy code

public sealed partial class ModuleManager{ private ModuleManager() { … } public static ModuleManager Instance{…} …}=> public sealed partial class ModuleManager{ public static ModuleManager Instance{…} public static void SetInstance (ModuleManager i_instance) { instance = i_instance; }

public static ModuleManager CreateNewInstance() { return new ModuleManager() }}

Page 29: Tdd for legacy code

We need the change but we don’t have the time to refactor at all!

Page 30: Tdd for legacy code

Sprout Method (Class)

When the change can be formulated as a single sequence of statements in one place in a method• Identify where you need to make a change• Create a new method; required local variables

become arguments for this method.• Develop the method using TDD.• Make a call to this new method where the

change is needed.

Page 31: Tdd for legacy code

private List<string> GetVariablesMarkedForExport(IVariableList varList, ref StatisticsCsvGenerator csvGenerator) { List<string> statisticNames = new List<string>(); foreach (VariableInfo var in varList) { if (var.Export) { if (var.statistics.Count == 0) { csvGenerator.SetStatisticValues (convertToCsvGeneratorStructure(var, string.Empty)); statisticNames.Add(var.fullName); } CheckStatisticsForExport(var, ref csvGenerator); } } }

if (var.statistics.Count > 0) { foreach (VariableInfo subvar in var.statistics) { if (subvar.Export) { statisticsCsvGenerator.SetStatisticValues(convertToCsvGeneratorStructure(subvar, var.fullName)); statisticNames.Add(var.fullName + "." + subvar.fullName); } }}

Page 32: Tdd for legacy code

=>private List<string> CheckStatisticsForExport(VariableInfo var,

ref StatisticsCsvGenerator statisticsCsvGenerator) {List<string> result = new List<string>(); if (var.statistics.Count > 0) { foreach (VariableInfo subvar in var.statistics) { if (subvar.Export) {…} } return result;}

Page 33: Tdd for legacy code

More info…

• Working Effectively with Legacy Code by Martin Feathers

• Clean Code – a handbook of agile software craftsmanship by Robert Martin