LEGACY CODE AND TEST-DRIVEN DEVELOPMENT
Andrea De Franceschi
Teaches you how to prevent and reverse code rot, turning systems that gradually degrade into systems that gradually improve.
It is about common problems that need to be overcome in order to be able to successfully apply the strategy defined in Part I
Describes 24 techniques in total, some of them not applicable to the Java language
PART I��General concepts
Four reasons/types of software change
Three Questions of software change
LEGACY CODE!
What is Legacy Code
Working with feedback
Feathers’ Legacy Change Algorithm
The Legacy Code Dilemma
When we change code, we should have tests in place.
To put tests in place, we often have to change code.
Feathers’ Legacy Change Algorithm
Why unit tests?
Unit Tests Golden Rule
NOT unit tests!
Higher-level tests
Feathers’ Legacy Change Algorithm
Test doubles: Fake and Mock Objects
Java�IDEs
Test / Mock frameworks
PART II��A TDD Example
How Do I Add a Feature?
Test-Driven Development (TDD)
1. Write a Failing Test Case
[...]
public class InstrumentCalculatorTest {
private static final double TOLERANCE = 0.001;
@Test
public void testFirstMomentOK() {
InstrumentCalculator calculator = new InstrumentCalculator();
calculator.addElement(1.0);
calculator.addElement(2.0);
assertEquals(-0.5, calculator.firstMomentAbout(2.0), TOLERANCE);
}
}
2. Get It to Compile
public class InstrumentCalculator {
public void addElement(double d) { }
double firstMomentAbout(double point) {
return Double.NaN;
}
}
3. Make It Pass
public class InstrumentCalculator {
private Collection<Double> elements = new ArrayList<>();
public void addElement(double d) {elements.add(d); }
public double firstMomentAbout(double point) {
double numerator = 0.0;
for (Iterator it = elements.iterator(); it.hasNext(); ) {
double element = ((Double)(it.next())).doubleValue();
numerator += element - point;
}
return numerator / elements.size();
}
}
4. Remove Duplication
5. Repeat
1. Write a Failing Test Case
[...]
@Test
public void testFirstMomentNOK() {
try {
new InstrumentCalculator().firstMomentAbout(0.0);
fail("expected InvalidBasisException");
} catch (InvalidBasisException e) {
}
}
2. Get It to Compile (1/2)
public class InvalidBasisException extends Exception {
public InvalidBasisException(String message) {super(message); }
}
2. Get It to Compile (2/2)
[...]
public double firstMomentAbout(double point)
throws InvalidBasisException {
if (elements.size() == 0)
throw new InvalidBasisException("no elements");
double numerator = 0.0;
for (Iterator it = elements.iterator(); it.hasNext(); ) {
double element = ((Double)(it.next())).doubleValue();
numerator += element - point;
}
return numerator / elements.size();
}
3. Make It Pass
4. Remove Duplication
5. Repeat
1. Write a Failing Test Case
[...]
@Test
public void testSecondMoment() throws Exception {
InstrumentCalculator calculator = new InstrumentCalculator();
calculator.addElement(1.0);
calculator.addElement(2.0);
assertEquals(0.5, calculator.secondMomentAbout(2.0), TOLERANCE);
}
2. Get It to Compile
[...]
public double secondMomentAbout(double point)
throws InvalidBasisException {
if (elements.size() == 0)
throw new InvalidBasisException("no elements");
double numerator = 0.0;
for (Iterator it = elements.iterator(); it.hasNext(); ) {
double element = ((Double)(it.next())).doubleValue();
numerator += element - point;
}
return numerator / elements.size();
}
3. Make It Pass
[...]
public double secondMomentAbout(double point)
throws InvalidBasisException {
if (elements.size() == 0)
throw new InvalidBasisException("no elements");
double numerator = 0.0;
for (Iterator it = elements.iterator(); it.hasNext(); ) {
double element = ((Double)(it.next())).doubleValue();
numerator += Math.pow(element - point, 2.0);
}
return numerator / elements.size();
}
4. Remove Duplication (1/2)
[...]
private double nthMomentAbout(double point, double n)
throws InvalidBasisException {
if (elements.size() == 0)
throw new InvalidBasisException("no elements");
double numerator = 0.0;
for (Iterator it = elements.iterator(); it.hasNext(); ) {
double element = ((Double)(it.next())).doubleValue();
numerator += Math.pow(element - point, n);
}
return numerator / elements.size();
}
4. Remove Duplication (2/2)
[...]
public double firstMomentAbout(double point)
throws InvalidBasisException {
return nthMomentAbout(point, 1.0);
}
public double secondMomentAbout(double point)
throws InvalidBasisException {
return nthMomentAbout(point, 2.0);
}
5. Repeat
TDD and Legacy code
0. Get the class 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.
PART III��I can’t get this class into a test harness
Most common problems
1. Objects of the class can’t be created easily.
2. The test harness won’t easily build with the class in it.
3. The constructor we need to use has bad side effects.
4. Significant work happens in the constructor, and we need to sense it.
Irritating parameter (1/8)
public class CreditValidatorTest {
@Test
void testCreate() {
CreditValidator validator = new CreditValidator();
}
}
Irritating parameter (2/8)
public class CreditValidator {
public CreditValidator(
RGHConnection connection,
CreditMaster master,
String validatorID
) {
[...]
}
Certificate validateCustomer(Customer customer) throws InvalidCredit {
[...]
}
[...]
}
Irritating parameter (3/8)
public class RGHConnection {
public RGHConnection(int port, String Name, String passwd)
throws IOException {
[...]
}
[...]
}
public class CreditMaster {
public CreditMaster(String filename, boolean isLocal) {
[...]
}
[...]
}
Irritating parameter (4/8)
public class CreditValidatorTest {
private static final int DEFAULT_PORT = 2323;
@Test
public void testCreate() throws Exception {
RGHConnection connection =
new RGHConnection(DEFAULT_PORT, "admin", "rii8ii9s");
CreditMaster master = new CreditMaster("crm2.mas", true);
CreditValidator validator =
new CreditValidator(connection, master, "a");
}
}
Irritating parameter (5/8)
public interface IRGHConnection {
void connect();
void disconnect();
RFDIReport RFDIReportFor(int id);
ACTIOReport ACTIOReportFor(int customerID);
}
Irritating parameter (6/8)
public class FakeConnection implements IRGHConnection {
public RFDIReport report;
public void connect() {}
public void disconnect() {}
public RFDIReport RFDIReportFor(int id) { return report; }
public ACTIOReport ACTIOReportFor(int customerID) { return null; }
}
Irritating parameter (7/8)
[...]
@Test
public void testValidCustomer() throws Exception {
FakeConnection connection = new FakeConnection();
connection.report = new RFDIReport([...]);
CreditMaster master = new CreditMaster("crm2.mas", true);
CreditValidator validator =
new CreditValidator(connection, master, "a");
Certificate result = validator.validateCustomer(new Customer([...]));
assertEquals(Certificate.VALID, result.getStatus());
}
Irritating parameter (8/8)
[...]
private static final double THRESHOLD = 0.001;
@Test
public void testAllPassed100Percent() throws Exception {
FakeConnection connection = new FakeConnection();
connection.report = new RFDIReport([...]);
CreditMaster master = new CreditMaster("crm2.mas", true);
CreditValidator validator =
new CreditValidator(connection, master, "a");
Certificate result = validator.validateCustomer(new Customer([...]));
assertEquals(100.0, validator.getValidationPercent(), THRESHOLD);
}
How to solve Construction Blob?
1. Parameterize Constructor
2. Extract and Override Factory Method
3. Supersede Instance Variable
4. Extract Interface
5. Extract Implementer
Construction Blob (1/3)
public class WatercolorPane {
[...]
public WatercolorPane(Form border, WashBrush brush, Pattern backdrop) {
[...]
anteriorPanel = new Panel(border);
anteriorPanel.setBorderColor(brush.getForeColor());
backgroundPanel = new Panel(border, backdrop);
cursor = new FocusWidget(brush, backgroundPanel);
[...]
}
[...]
}
Construction Blob (2/3)
public class WatercolorPane {
[...]
public WatercolorPane(Form border, WashBrush brush, Pattern backdrop) {
[...]
}
void supersedeCursor(FocusWidget newCursor) {
cursor = newCursor;
}
[...]
}
Construction Blob (3/3)
class WatercolorPaneTest {
@Test
void supersededTest() {
[...]
TestingFocusWidget widget = new TestingFocusWidget();
WatercolorPane pane(form, brush, backdrop);
pane.supersedeCursor(widget);
assertEquals(0, pane.getComponentCount());
}
}
How to solve Hidden Dependency?
1. Extract Interface
2. Parameterize Constructor
3. Preserve Signatures
4. Extract and Override Getter
5. Extract and Override Factory Method
6. Supersede Instance Variable
How to solve Irritating Global Dependency?
1. Parameterize Constructor
2. Parameterize Method
3. Extract and Override Call
4. Singleton Design Pattern
5. Introduce Static Setter
6. Subclass and Override Method
7. Extract Interface
8. Lean on the Compiler
9. Extract Implementer
The Case of the Horrible Include Dependencies
The Case of the Onion Parameter
The Case of the Aliased Parameter
CONCLUSIONS
QUESTIONS?
THANK YOU