Tdd for legacy code

Post on 20-May-2015

1.073 views 2 download

Tags:

description

Short introduction on how to mix TDD and working with legacy code for an effective process recipe.

Transcript of Tdd for legacy code

Test Driven Development in the context of large legacy codebases

Summary

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

existing code• Adding and testing new features

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

Design

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

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"); }

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; }

Test

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

Repeat!

Design:We need more information from the file….

Design

Implement

TestTest

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.

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.

Design

Implement

Test

Find the code you need to change

Get this code in a test harness

Test

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

How do we do it?

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

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)

Dependency breaking techniques

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

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); ……

[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; }

  ……………

}

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

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}

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

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

eRegressionRunResult CalculatePassFail(IRegressionRun i_run)

=>

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

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

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

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

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() }}

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

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.

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); } }}

=>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;}

More info…

• Working Effectively with Legacy Code by Martin Feathers

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