Port of schuchert.wikispaces.com


JMockIt.AStoryAboutTooMuchPower

JMockIt.AStoryAboutTooMuchPower

A Story About Too Much Power

Notes:

I gave a talk in Berlin November 10, 2011. The subject was ostensibly about legacy refactoring. It was really about a lot of things, the starting point being legacy refactoring. Rather than using problems out of a class on legacy refactoring, I wanted something fresh and shiny. In the past, I started the talk with the following contrived example:

public SomeClass {
	public static void problemsAhead() {
		System.exit(0);
	}
}

public SomeClassTest {
	@Test
	public void foo() {
		SomeClass.problemsAhead().
	}
}

Calling System.exit(0) is an allegory for “something bad” you don’t want to have happen during a test. To make this even more of a problem, consider this class:

public DontEven {
	static {
		System.exit(0);
	}
}

Both of these are fun examples but I wanted something with a bit more teeth - though I really do like that second example because it requires some knowledge of class loading to appreciate why you end up doing what you do to fix it. While it is easily fixable, though as of this writing I don’t know how to do so with JMockIt. Just by hand.

My first idea was to write something with JPA. I figured database stuff is tough and it is a common request. I started downloading the latest jar files in anticipation of building a full JSE-Based JPA example that would then be used as a basis for the talk.

While waiting for that to download (my hotel at the time had decent internet but I ended up downloading 90 megs), I remembered the simple calculator used in The Cucumber Book and thought something like that would be cool and less hassle as I was waiting for my yaks to be shaved to get started.

That lead to thinking about something I often joke about when using an RPN calculator example in my classes: to perform calculation, write something that does a google search and then scrape the results for your implementation. Silly, yes.

I was working with a financial company that same week so my thoughts then lead to something financial, which lead to currency conversion. That resonated with me so I instead downloaded the Apache HttpClient and began to work on that instead. I spent more time trying to figure out the URL than writing the code. I stated with google and then finally gave up on google and found another site and wrote some ugly code to scrape conversion rates. I then wrote some tests on this code using JMockIt and found JMockIt to be powerful, a bit too powerful.

In any case, I wanted to know the valid currencies, so I wrote another static method (the first one was static as the basis for the development was something that typically occurs in legacy code bases). This one hit a different URL for currency names and the approach was somewhat copy-and-pasted but deliberately different to make refactoring and duplication removal more difficult.

Here is the first method in its original form:

	public static List<String> currencySymbols() {
	// ... <snip>
	}

	public static BigDecimal convertFromTo(String fromCurrency,
			String toCurrency) {
		List<String> valid = currencySymbols();
		if (!valid.contains(fromCurrency))
			throw new IllegalArgumentException(String.format(
					"Invalid from currency: %s", fromCurrency));
		if (!valid.contains(toCurrency))
			throw new IllegalArgumentException(String.format(
					"Invalid to currency: %s", toCurrency));
		String url = String.format("http://www.gocurrency.com/v2/dorate.phpo"
				+ "?inV=1&from=%s&to=%s&Calculate=Convert", toCurrency,
				fromCurrency);
		try {
			HttpClient httpclient = new DefaultHttpClient();
			HttpGet httpget = new HttpGet(url);
			HttpResponse response = httpclient.execute(httpget);
			HttpEntity entity = response.getEntity();
			StringBuffer result = new StringBuffer();
			if (entity != null) {
				InputStream instream = entity.getContent();
				InputStreamReader irs = new InputStreamReader(instream);
				BufferedReader br = new BufferedReader(irs);
				String l;
				while ((l = br.readLine()) != null) {
					result.append(l);
				}
			}
			String theWholeThing = result.toString();
			int start = theWholeThing
					.lastIndexOf("<div id=\"converter_results\"><ul><li>");
			String substring = result.substring(start);
			int startOfInterestingStuff = substring.indexOf("<b>") + 3;
			int endOfIntererestingStuff = substring.indexOf("</b>",
					startOfInterestingStuff);
			String interestingStuff = substring.substring(
					startOfInterestingStuff, endOfIntererestingStuff);
			String[] parts = interestingStuff.split("=");
			String value = parts[1].trim().split(" ")[0];
			BigDecimal bottom = new BigDecimal(value);
			return bottom;
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

Note, all of this code is avaiable for download on github. Also, it does do some basic currency conversion.

This code is traditionally to hard to test without changing it somewhat. However, I wanted to give a try and I managed to execute this test without changing it, and without an internet connection, with the following code:

	@Test
	public void convertsCorrectly() throws Exception {
		final ByteArrayInputStream bais = new ByteArrayInputStream(
				"<div id=\"converter_results\"><ul><li><b>5 x = 42 Y</b>"
						.getBytes());

		new NonStrictExpectations() {
			DefaultHttpClient httpclient;
			HttpResponse response;
			HttpEntity entity;
			{
				httpclient.execute((HttpUriRequest) any);
				result = response;
				response.getEntity();
				result = entity;
				entity.getContent();
				result = bais;
			}
		};

		new NonStrictExpectations(CurrencyConversion.class) {
			{
				CurrencyConversion.currencySymbols();
				result = Arrays.asList(new String[] { "X", "Y" });
			}
		};

		BigDecimal result = CurrencyConversion.convertFromTo("X", "Y");
		assertEquals(new BigDecimal(42), result);
	}

This code performs vertical mocking (or nested mocking). Here is a quick description of what this actually does (as far as I understand the model of JMockIt):

This is a pretty ugly test but it is maintainable? In fact, it’s not too bad. While I took this further in the talk that evening, the next day in class I used it to perform a more traditional legacy refactoring.

During the talk, I made the class an override-able singleton and then introduced an instance delegator to support override-ability by hand: ** Override-able Singleton**

public class CurrencyConversion {
	static CurrencyConversion instance;

	public static CurrencyConversion getInstance() {
		if (instance == null) {
			instance = new CurrencyConversion();
		}
		return instance;
	}

	static public CurrencyConversion reset(CurrencyConversion newConversion) {
		CurrencyConversion old = instance;
		instance = newConversion;
		return old;
	}
	// ...

Introduce an Instance Delegator

	public List<String> getCurrencySymbols() {
		// the same code that was originally in the 
		// static method called currencySymbols
	}

	// the instance delegator
	public static List<String> currencySymbols() {
		return getInstance().getCurrencySymbols();
	}

This makes static method call instance methods, which can be overridden using standard inheritance, while maintaining the public API for backwards compatibility. I could still use JMockIt, but now I can use what has now become a traditional mocking library like my favorite, Mockito. Note, this change did not in any way break the JMockIt example above.

In the class, we reviewed one the of the methods and noticed a few things:

	public static BigDecimal convertFromTo(String fromCurrency,
			String toCurrency) {
		List<String> valid = currencySymbols();
		// ...
		if (!valid.contains(fromCurrency))
			throw new IllegalArgumentException(String.format(
					"Invalid from currency: %s", fromCurrency));
		if (!valid.contains(toCurrency))
			throw new IllegalArgumentException(String.format(
					"Invalid to currency: %s", toCurrency));
		try {
			HttpClient httpclient = new DefaultHttpClient();
			HttpGet httpget = new HttpGet(url);
			HttpResponse response = httpclient.execute(httpget);
			HttpEntity entity = response.getEntity();
			StringBuffer result = new StringBuffer();
			if (entity != null) {
				InputStream instream = entity.getContent();

With these considerations in mind, we changed the code before extracting any methods:

	public static BigDecimal convertFromTo(String fromCurrency,
			String toCurrency) {
		List<String> valid = currencySymbols();
		if (!valid.contains(fromCurrency))
			throw new IllegalArgumentException(String.format(
					"Invalid from currency: %s", fromCurrency));
		if (!valid.contains(toCurrency))
			throw new IllegalArgumentException(String.format(
					"Invalid to currency: %s", toCurrency));
		String url = String.format("http://www.gocurrency.com/v2/dorate.phpo"
				+ "?inV=1&from=%s&to=%s&Calculate=Convert", toCurrency,
				fromCurrency);
		try {
			HttpClient httpclient = new DefaultHttpClient();
			HttpGet httpget = new HttpGet(url);
			HttpResponse response = httpclient.execute(httpget);
			HttpEntity entity = response.getEntity();
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
		StringBuffer result = new StringBuffer();
		InputStream instream = entity.getContent();
		InputStreamReader irs = new InputStreamReader(instream);
		BufferedReader br = new BufferedReader(irs);
		String l;
		while ((l = br.readLine()) != null) {
			result.append(l);
		}
		String theWholeThing = result.toString();
		int start = theWholeThing
				.lastIndexOf("<div id=\"converter_results\"><ul><li>");
		String substring = result.substring(start);
		int startOfInterestingStuff = substring.indexOf("<b>") + 3;
		int endOfIntererestingStuff = substring.indexOf("</b>",
				startOfInterestingStuff);
		String interestingStuff = substring.substring(
				startOfInterestingStuff, endOfIntererestingStuff);
		String[] parts = interestingStuff.split("=");
		String value = parts[1].trim().split(" ")[0];
		BigDecimal bottom = new BigDecimal(value);
		return bottom;
}

Next, we performed a few extract method refactorings to end up with the following:

	public static BigDecimal convertFromTo(String fromCurrency,
			String toCurrency) {
		getInstance().checkParameters(fromCurrency, toCurrency);
		String url = String.format("http://www.gocurrency.com/v2/dorate.phpo"
				+ "?inV=1&from=%s&to=%s&Calculate=Convert", toCurrency,
				fromCurrency);
		String theWholeThing = getInstance().getContent(url);
		int start = theWholeThing
				.lastIndexOf("<div id=\"converter_results\"><ul><li>");
		String substring = theWholeThing.substring(start);
		int startOfInterestingStuff = substring.indexOf("<b>") + 3;
		int endOfIntererestingStuff = substring.indexOf("</b>",
				startOfInterestingStuff);
		String interestingStuff = substring.substring(startOfInterestingStuff,
				endOfIntererestingStuff);
		String[] parts = interestingStuff.split("=");
		String value = parts[1].trim().split(" ")[0];
		BigDecimal bottom = new BigDecimal(value);
		return bottom;
	}

	protected void checkParameters(String fromCurrency, String toCurrency) {
		List<String> valid = currencySymbols();
		if (!valid.contains(fromCurrency))
			throw new IllegalArgumentException(String.format(
					"Invalid from currency: %s", fromCurrency));
		if (!valid.contains(toCurrency))
			throw new IllegalArgumentException(String.format(
					"Invalid to currency: %s", toCurrency));
	}

	protected String getContent(String url) {
		StringBuffer result = new StringBuffer();
		try {
			HttpClient httpclient = new DefaultHttpClient();
			HttpGet httpget = new HttpGet(url);
			HttpResponse response = httpclient.execute(httpget);
			HttpEntity entity = response.getEntity();
			if (entity != null) {
				InputStream instream = entity.getContent();
				InputStreamReader irs = new InputStreamReader(instream);
				BufferedReader br = new BufferedReader(irs);
				String l;
				while ((l = br.readLine()) != null) {
					result.append(l);
				}
			}
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
		return result.toString();
	}
}

With these changes in mind, we wrote the same test that is above in JMockIt, but we did everything using boring old java:

package com.schuchert.welc;

import static org.junit.Assert.assertEquals;

import java.math.BigDecimal;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;

public class CurrencyConvertion_Test {
	private CurrencyConversion original;

	class CurrencyConversionTestingSubclass extends CurrencyConversion {
		@Override
		protected String getContent(String url) {
			return "<div id=\"converter_results\"><ul><li><b>5 x = 42 Y</b>";
		}

		@Override
		protected void checkParameters(String fromCurrency, String toCurrency) {
		}
	}

	@Before
	public void init() {
		original = CurrencyConversion
				.reset(new CurrencyConversionTestingSubclass());
	}

	@After
	public void restore() {
		CurrencyConversion.reset(original);
	}

	@Test
	public void foo() {
		BigDecimal result = CurrencyConversion.convertFromTo(null, null);
		assertEquals(new BigDecimal(42), result);
	}

}

Here are some observations about this refactoring exercise:

I speculated that the existing JMockIt tests would still work based on my understanding of how it work. To its credit, the JMockIt tests will ran. This speaks well to its model of operation. You can write tests using it that can withstand certain kinds of change.

To be fair, after this refactoring we can make the JMockIt test look much better:

	@Test
	public void postRefactoringCurrencyConversion() {
		new NonStrictExpectations(CurrencyConversion.class) {
			CurrencyConversion c;
			{
				c.checkParameters(anyString, anyString);
				c.getContent(anyString);
				result = "<div id=\"converter_results\"><ul><li><b>5 x = 42 Y</b>";
			}
		};
		BigDecimal result = CurrencyConversion.convertFromTo(null, null);
		assertEquals(new BigDecimal(42), result);
	}

This ismuch better. In fact, I prefer this over the prior test that hand-rolled a testing subclass.However, JMockIt did not force us to do the work necessary to clean up the production code. As a result, the code remained ugly when we first used JMockIt to “test” the code.

Here’s the thing: In teams, most people, most of the time, use existing code examples as a the basis of new work. That means if the code is left ugly, more ugly code will be written. Sure, we can “test” it using JMockIt, and JMockIt seems to age fairly well across certain kinds of refactorigns. However, in this example, so would any mocking framework supporting either nice or non-strict solutions. (That’s a key lesson-learned, most of the time use non strict mocks. Ask if you would like a more precise set of recommendations.)

So while I think JMockIt is powerful and I want to make sure to keep it in my back pocket, I think it requires more disciplinebecause of its flexibility. There’s little it cannot do, and as a result there’s little it will force you to do to recognize poor coding practices, which I fear will actually restrict growth and learning. You cannot learn without failure. With a tool like JMockIt, it’s harder to notice a failure unless you can observe that something like this screams “crappy design”:

		new NonStrictExpectations() {
			DefaultHttpClient httpclient;
			HttpResponse response;
			HttpEntity entity;
			{
				httpclient.execute((HttpUriRequest) any);
				result = response;
				response.getEntity();
				result = entity;
				entity.getContent();
				result = bais;
			}
		};

I still want JMockIt for cases where I’m working with a library and it has things like final classes that are hard to construct. I could see using it instead of Mockito. Knowing what I now know, using it test-first would not in itself force too many bad practices - there are some related to its use of anonymous inner classes, which encourages temporal coupling to save a few lines of code, and which is encouraged in their exercises (ask if you want to know what I mean). Even so, I could easily see moving to it as my primary mocking tool.

Summary

Legacy refactoring is hard. It takes time. Tools like JMockIt can get you to a running test faster, but that presumes that your goal is to get a test running.

It isn’t.

The reason to write a test for legacy code is because it is about to change and you don’t want to do any harm while making that change. The test helps with that, but what helps even more is a clearer/cleaner design with fewer unnecessary dependencies. JMockIt, with all of its power, does not force those changes so while you will get tests and you might be able get tests initially written faster, you’ll still have a mess of production code.

It seems that when starting with a section of legacy code, the early work is based on learning what it does. In this respect, JMockIt can speed things up. So if you take the approach of using it as a tool for building understanding, it can help. If, however, as a result of learning something, you don’t do anything, while you might apply that learning in the future, it seems less likely because there was less pain suffered.

Growth requires failure and feedback. If you ease the pain of failure, you lessen the feedback. It seems to follow that you’ll learn less and grow slower. There are lots of examples of this in nature. Consider the Monarch butterfly. It must out of its cocoon to grow. If they are let out without that struggle, their wings never form as the pressure from the escape forces fluid out of their bodies and into their wings. It seems like JMockIt may prevent us from growing our programming wings unless we are not careful.


Comments

" Creative Commons License
This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.