Legacy Code under Control – Part 1: Introduction to JMockit

At my current client we are using the tool “JMockit” so successfully that it has earned a proper place in my virtual toolbox. Even though the developers in my team try really hard to create good code with proper design, by now we have to wade through more than 1 million lines of Java code. And we can smell all the bad decisions of the last ten years. In essence – we have to fight with legacy code. In this fight, unit tests are an important ally, and JMockit plays a crucial role during the creation of these tests.

Because JMockit is still quite new and relatively unknown, I would like to introduce you to it in a series of blog posts:

Part 1: Introduction
Part 2: The main concepts of JMockit. A first test that would have been quite difficult to create without JMockit
Part 3: Two difficult cases “from the trenches”: A remote service call, and a leap-year logic

Why unit tests for legacy code?

Michael Feathers has expressed an important concept in his great book “Working Effectively With Legacy Code”: it is not enough to try to avoid the degeneration of code quality. These attempts will always be imperfect. Despite best efforts you will sometimes make mistakes that stack up over time and lead to a continuous decrease of code quality. This means you have to counteract proactively: you have to continuously make steps towards a simpler system that is easier to understand and has a better structure.

An important technique for these steps are automatic regression tests. For every tiny change to the structure you have to make sure that you didn’t accidentally break something. For this it is crucial to have completely automatic, fast tests that verify the I/O behavior of the many, many units of a system in isolation, the so-called unit tests. After every little change we execute these tests and thereby check that we haven’t inadvertently changed the behavior. A failing unit tests points to a local, clearly identifiable problem in one of those units.

The Legacy Code Dilemma

Unfortunately, because of the inadequate structure of legacy software, its units cannot be tested in isolation. And here we start going around in circles. In order to improve the structure of the system, we need unit tests. And in order to create unit tests, we need a better structure of the system: the legacy code dilemma.

Why is it so difficult to write unit tests for legacy code? Don’t we have years of experience and comfortable mocking frameworks like EasyMock or Mockito? With them, unit tests in which part of the system are replaced by place holders can be easily created. Have a look at this hypothetical InvoiceUpdateRecordere of which we want to test the method getInvoiceReport and the needed DbConnection (for readability reasons I omit the Import statements):

public class InvoiceUpdateRecorder {
 private DbConnection dbConnection;

 InvoiceUpdateRecorder(DbConnection aDbConnection)
 {
  dbConnection = aDbConnection;
 // ... many more lines of unreadable initialization
 }

 public String getInvoiceReport() {
  String result = "";
  // ... lots of incomprehensible code
  List invoices = dbConnection.fetchAllInvoices();
  if (invoices.isEmpty()) {
   result = "no invoices found";
  } else {
  // you really don't wanna know what happens here
  }
  return result;
 }
}

public class DbConnection {
 Connection jdbcConnection;

 public DbConnection(Connection ourJdbcConnection) {
  jdbcConnection = ourJdbcConnection;
  // more initialization
 }

 public List fetchAllInvoices() {
  List result = null;
  // JDBC code to fetch list from DB
  return result;
 }
}

And this would be a test method written using Mockito:

@Test public void newInvoiceUpdateRecorderHasNoInvoices() {
 // create mock and define behavior
 DbConnection mockedDbConnection = mock(DbConnection.class);
 when(mockedDbConnection.fetchAllInvoices()).
    thenReturn(Collections.EMPTY_LIST);

 // create system under test
 InvoiceUpdateRecorder recorder = new
    InvoiceUpdateRecorder(mockedDbConnection);

 // SUT has to work
 String result = recorder.getInvoiceReport();

 // check result
 assertTrue(result.contains("no invoices found"));
}

Wir erzeugen einen InvoiceUpdateRecorder, dem wir eine gemockte Datenbank-Verbindung untergeschieben, die immer eine leere Liste von Invoice-Objekten zurückgibt. So können wir leicht den Fall herbeigeführen, der uns interessiert, ohne tatsächlich eine Datenbank-Verbindung aufbauen zu müssen. Nachdem das SUT seine Arbeit getan hat, prüfen wir, ob das Ergebnis wie erwartet aussieht.
Und in gut strukturierten Systemen, in denen sich wie in meinem Beispiel das abhängige Objekt so einfach in das SUT per Dependency Injection hineinreichen lässt, funktioniert dieser Proxy-Ansatz auch wunderbar. Leider hat er einige, im Kontext von Legacy Code besonders nachteilige Einschränkungen:

Es können nur Abhängigkeiten des SUTs gemockt werden, die von außen (er)setzbar sind, denn irgendwie muss ja der im Test erzeugte Mock seinen Weg in das SUT finden. Oft erzeugen in Legacy-Systemen Klassen aber die Objekte, von denen sie abhängen, selbst, oder sie holen sie sich, indem sie statische Methoden konkreter Klassen aufrufen. Das Singleton-Pattern spielt hier oft eine unrühmliche Rolle.
Allgemein können mit dem Proxy-Ansatz statische, finale oder private Methoden, die das SUT aufruft, nicht gemockt werden.
Auch finale Klassen können nicht gemockt werden. Darunter sind auch viele Systemklassen, wie z.B. java.lang.System.

JMockit: Das Schweizer Messer unter den Test-Tools für Legacy Code

JMockit bietet eine erstaunliche Alternative ohne diese Einschränkungen. Gemockt werden hier grundsätzlich Klassen und alle ihre Instanzen, egal wer sie erzeugt. Und auch wenn die Syntax anfangs etwas gewöhnungsbedürftig ist, so ist sie doch verblüffend konsistent und mächtig.
Hier ein einfaches Beispiel, in dem wir einen etwas widerspenstigeren InvoiceUpdateRecorder testen wollen, nämlich einen, der seine DbConnection selbst erzeugt. Hier der relevante Ausschnitt aus unserem SUT:

public class LegacyInvoiceUpdateRecorder {
private DbConnection dbConnection;

public LegacyInvoiceUpdateRecorder()
{
// ACHTUNG: Wir erzeugen uns die DbConnection selbst über einen Konstruktor-Aufruf!
dbConnection = new DbConnection();
// …
}

// getInvoiceReport() wie wie oben
}

Und hier der entsprechende Test mit JMockit, der so mit Mockito nicht möglich wäre:

@Test public void newInvoiceUpdateRecorderHasNoInvoicesWithJMockit() {
new NonStrictExpectations() {
@Mocked DbConnection mockedDbConnection;
{
// Liefere bei Aufrufen dieser Methode eine leere Liste
mockedDbConnection.fetchAllInvoices(); result = Collections.EMPTY_LIST;
}};

// System-Under-Test erzeugen. Dieses erzeugt selbst seine DbConnection über
// new DbConnection(), bekommt aber dank JMockit nur einen Mock.
LegacyInvoiceUpdateRecorder recorder = new LegacyInvoiceUpdateRecorder();

// SUT arbeiten lassen. Dabei wird die Methode fetchAllInvoices aufgerufen
String result = recorder.getInvoiceReport();

// Ergebnis prüfen
assertTrue(result.contains(“no invoices found”));
}

In diesem Test geschieht folgendes: Zunächst wird für die Dauer des Tests die Klasse DbConnectiongemockt. Alle Aufrufe von Konstruktoren von DbConnection liefern nun Mocks. So auch der hier nicht sichtbare Aufruf innerhalb des Konstrukturs von InvoiceUpdateRecorder. Außerdem liefern nun alle Aufrufe von Klassen- oder Instanzmethoden von DbConnection Default-Werte: null, false, 0,… . Nur fetchAllInvoices() liefert das explizit vorgegebene Ergebnis (Bitte nicht stören lassen durch die eigenartige Syntax, in der diese Vorgabe erfolgt. In der nächsten Folge dieser kleinen Serie erkläre ich, was es damit auf sich hat und warum das gar keine so schlechte Idee ist). Der Rest entspricht 1:1 dem vorigen Beispiel: wieder rufen wir das SUT auf und prüfen das Ergebnis.

Für erste eigene Experimente mit JMockit empfehle ich die Lektüre des Getting-Started Artikel auf der JMockit-Homepage unter http://jmockit.googlecode.com/svn/trunk/www/gettingStarted.html.

Mein Fazit

Die Ausrede „Dieser Code ist untestbar!“ gilt mit JMockit nicht mehr. In den allermeisten Fällen lassen sich Abhängigkeiten in Java-basierten Legacy-Systemen damit aufbrechen. So war es uns in vielen Fällen möglich, die für eine verantwortliche Umstrukturierung erforderlichen Unit-Tests zu erstellen, ohne in den eigentlichen Code des SUT eingreifen zu müssen Dank JMockitkonnten wir tatsächlich den langen, mühsamen Weg aus der Legacy-Code-Hölle in Angriff nehmen.

In der nächsten Folge schauen wir uns dann einige zentrale Konzepte von JMockit und seine verblüffende Syntax etwas näher an.