1 of 69

LEGACY CODE AND TEST-DRIVEN DEVELOPMENT

Andrea De Franceschi

2 of 69

Teaches you how to prevent and reverse code rot, turning systems that gradually degrade into systems that gradually improve.

3 of 69

4 of 69

It is about common problems that need to be overcome in order to be able to successfully apply the strategy defined in Part I

5 of 69

Describes 24 techniques in total, some of them not applicable to the Java language

6 of 69

PART I��General concepts

7 of 69

8 of 69

9 of 69

Four reasons/types of software change

10 of 69

11 of 69

Three Questions of software change

  1. What changes do we have to make?
  2. How will we know that we’ve done them correctly?
  3. How will we know that we haven’t broken anything?

12 of 69

LEGACY CODE!

13 of 69

What is Legacy Code

  • Legacy code is somebody else’s code
  • Legacy code is code that you have to change but do not really understand

  • To me, legacy code is simply code without tests

14 of 69

Working with feedback

  • Edit and pray: the industry standard. Changes are carefully planned, code is understood. A system run should show if the change broke something

  • Cover and modify: Regression tests used as a safety net, against the traditional “correctness testing”

15 of 69

Feathers’ Legacy Change Algorithm

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

16 of 69

17 of 69

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.

18 of 69

Feathers’ Legacy Change Algorithm

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

19 of 69

Why unit tests?

20 of 69

Unit Tests Golden Rule

21 of 69

NOT unit tests!

22 of 69

Higher-level tests

  • Tests that cover scenarios and interactions in an application
  • Sometimes called functional or integration tests
  • Longer execution time
  • Can be used to pin down behavior for a set of classes at a time
  • Can make unit test writing easier

23 of 69

Feathers’ Legacy Change Algorithm

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

24 of 69

Test doubles: Fake and Mock Objects

25 of 69

Java�IDEs

26 of 69

Test / Mock frameworks

27 of 69

PART II��A TDD Example

28 of 69

How Do I Add a Feature?

29 of 69

Test-Driven Development (TDD)

  1. Write a failing test case.
  2. Get it to compile.
  3. Make it pass.
  4. Remove duplication.
  5. Repeat.

30 of 69

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

}

}

31 of 69

2. Get It to Compile

public class InstrumentCalculator {

public void addElement(double d) { }

double firstMomentAbout(double point) {

return Double.NaN;

}

}

32 of 69

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

}

}

33 of 69

4. Remove Duplication

34 of 69

5. Repeat

35 of 69

1. Write a Failing Test Case

[...]

@Test

public void testFirstMomentNOK() {

try {

new InstrumentCalculator().firstMomentAbout(0.0);

fail("expected InvalidBasisException");

} catch (InvalidBasisException e) {

}

}

36 of 69

2. Get It to Compile (1/2)

public class InvalidBasisException extends Exception {

public InvalidBasisException(String message) {super(message); }

}

37 of 69

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

}

38 of 69

3. Make It Pass

39 of 69

4. Remove Duplication

40 of 69

5. Repeat

41 of 69

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

}

42 of 69

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

}

43 of 69

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

}

44 of 69

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

}

45 of 69

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

}

46 of 69

5. Repeat

47 of 69

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.

48 of 69

PART III��I can’t get this class into a test harness

49 of 69

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.

50 of 69

Irritating parameter (1/8)

public class CreditValidatorTest {

@Test

void testCreate() {

CreditValidator validator = new CreditValidator();

}

}

51 of 69

Irritating parameter (2/8)

public class CreditValidator {

public CreditValidator(

RGHConnection connection,

CreditMaster master,

String validatorID

) {

[...]

}

Certificate validateCustomer(Customer customer) throws InvalidCredit {

[...]

}

[...]

}

52 of 69

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) {

[...]

}

[...]

}

53 of 69

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

}

}

54 of 69

Irritating parameter (5/8)

public interface IRGHConnection {

void connect();

void disconnect();

RFDIReport RFDIReportFor(int id);

ACTIOReport ACTIOReportFor(int customerID);

}

55 of 69

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

}

56 of 69

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

}

57 of 69

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

}

58 of 69

How to solve Construction Blob?

1. Parameterize Constructor

2. Extract and Override Factory Method

3. Supersede Instance Variable

4. Extract Interface

5. Extract Implementer

59 of 69

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

[...]

}

[...]

}

60 of 69

Construction Blob (2/3)

public class WatercolorPane {

[...]

public WatercolorPane(Form border, WashBrush brush, Pattern backdrop) {

[...]

}

void supersedeCursor(FocusWidget newCursor) {

cursor = newCursor;

}

[...]

}

61 of 69

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

}

}

62 of 69

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

63 of 69

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

64 of 69

The Case of the Horrible Include Dependencies

65 of 69

The Case of the Onion Parameter

66 of 69

The Case of the Aliased Parameter

67 of 69

CONCLUSIONS

68 of 69

QUESTIONS?

69 of 69

THANK YOU