2

I have the following class which contains a hard coded URL that never changes:

    public class HttpClient {
        private final String DOWNLOAD_URL = "http://original.url.json";

        public String readJsonDataFromUrl() throws IOException {
            URLConnection urlConnection = getUrlConnection();

            BufferedReader reader = new BufferedReader(new InputStreamReader(urlConnection.getInputStream()));
            StringBuffer content = new StringBuffer();

            String readLine = "";
            while ((readLine = reader.readLine()) != null)  {
                content.append(readLine);
            }

            return content.toString();
        }

        private URLConnection getUrlConnection() throws IOException {
            URL jsonLocator = new URL(DOWNLOAD_URL);

            return jsonLocator.openConnection();
        }
    }

Now imagine that I'd like to expect the IOException in my test. In my opinion, the only way to do that is to rewrite the complete class in a mock object because of the final variable:

public class HttpClientMock extends HttpClient  {
    private final String DOWNLOAD_URL = "http://wrong.test.url.json";

    @Override
    public String readJsonDataFromUrl() throws IOException {
        URLConnection urlConnection = getUrlConnection();

        BufferedReader reader = new BufferedReader(new InputStreamReader(urlConnection.getInputStream()));
        StringBuffer content = new StringBuffer();

        String readLine = "";
        while ((readLine = reader.readLine()) != null)  {
            content.append(readLine);
        }

        return content.toString();
    }

    private URLConnection getUrlConnection() throws IOException {
        URL jsonLocator = new URL(DOWNLOAD_URL);
        URLConnection urlConnection = jsonLocator.openConnection();

        return urlConnection;
    }
}

But this is somehow far-fetched. If the original methods would be changed, the test results could still be positive because with this attempt, I don't actually test the original class anymore.

How can this be done properly? (I don't want to use a framework just for this one test, so are there any design attempts to solve this in a common way?)

Bevor
  • 8,396
  • 15
  • 77
  • 141
  • can you change the original code? – Imre L Sep 07 '12 at 19:03
  • Yes, but I don't want to make it less restrictive by making the private method protected or something. Or do you have any other ideas? – Bevor Sep 07 '12 at 19:08
  • 2
    Return the download URL from a one line get method and pass it to the other methods in the class. You can test the other methods by passing bad URLs. – Gilbert Le Blanc Sep 07 '12 at 19:18
  • Good idea, could you post this as answer. If this works fine, I will accept your answer. – Bevor Sep 07 '12 at 19:21
  • 1
    Making protected is actually opening up for changes not restricting. Able to write unit tests easy is a sign of a good design. However: http://stackoverflow.com/questions/4516381/changing-private-final-fields-via-reflection – Imre L Sep 07 '12 at 19:26
  • @Bevor I'm not quite sure if such a test even makes sense. Looking at the source code above, it actually does not create and throw a new _IOException_ on its one. Instead the code just passes along IOException to the caller through the _throws declaration_. So what the test case really does is to make sure that the used methods of the standard library throw IOExceptions as expected. Sure a unit test should not be aware of the implementation, but is this test case really worth the effort? – rmoestl Sep 07 '12 at 20:10
  • Basically you are right, but I wasn't exactly sure when the IOException is really thrown so I decided to pass some malformed urls or a supposed proper url like `http://foobarxyzxyzxyz.com` to see if this is really covered. Ok, I could rely on the javadoc, but I feel better to cover all possible errors in tests. – Bevor Sep 07 '12 at 20:21

2 Answers2

0

Another spin on Gilbert Le Blanc's suggestion, is to make the HttpClient totally ignorant of the URL by injecting it through the constructor.

 public class HttpClient {
   private final String url;

   public HttpClient(String url) { this.url = url; }

 }

You can hard code the URL (or read from a config) somewhere externally to HttpClient and inject it wherever you instantiate the client. Then in your test it will be trivial to inject a bad url.

jeff
  • 4,325
  • 16
  • 27
  • I already thought about that but this would offer an unnecessary weak point. – Bevor Sep 07 '12 at 19:39
  • One could parse here any url, and this is absolutely not necessary since the url never changes. – Bevor Sep 07 '12 at 19:43
  • The fact that you want to test a bad url means that the url can change. An HttpClient should be able to read from a URL. Whatever URL it ends up reading from is a dependency. Testable systems allow you to inject dependencies. One of the main reasons for this is so you can inject mock components where needed. – jeff Sep 07 '12 at 20:04
  • I know what you mean, but see my explanation at Jordan White's posting. – Bevor Sep 07 '12 at 20:10
0

Thanks to everybody, but I think that Gilbert Le Blanc's solution is the most preferable for that case which looks like this:

The original class:

public class HttpClient {
    private final String DOWNLOAD_URL = "http://my.original.json.url";

    public String readJsonDataFromUrl() throws IOException {
        URLConnection urlConnection = getUrlConnection();

        BufferedReader reader = new BufferedReader(new InputStreamReader(urlConnection.getInputStream()));
        StringBuffer content = new StringBuffer();

        String readLine = "";
        while ((readLine = reader.readLine()) != null)  {
            content.append(readLine);
        }

        return content.toString();
    }

    private URLConnection getUrlConnection() throws IOException {
        URL jsonLocator = new URL(getConnectionString());

        return jsonLocator.openConnection();
    }

    protected String getConnectionString()  {
        return DOWNLOAD_URL;
    }
}

The mock object:

public class HttpClientMock extends HttpClient  {
    private String downloadUrl = "http://my.original.json.url";

    public HttpClientMock()  {
        super();
    }

    public HttpClientMock(String downloadUrl)  { 
        this.downloadUrl = downloadUrl;
    }

    @Override
    protected String getConnectionString()  {
        return downloadUrl;
    }
}

And the working tests:

public class HttpClientTest {

    private JSONParser jsonParser = new JSONParser();

    @Test
    public void readJsonDataFromUrlSucceeds() throws IOException, ParseException {
        HttpClient httpClient = new HttpClientMock();

        String jsonString = httpClient.readJsonDataFromUrl();
        JSONObject jsonObject = (JSONObject)jsonParser.parse(jsonString);

        assertTrue(jsonObject.size() > 0);
    }

    @Test(expected = IOException.class)
    public void readJsonDataFromMalformedUrlFails() throws IOException, ParseException {
        HttpClient httpClient = new HttpClientMock("http://malformed");

        httpClient.readJsonDataFromUrl();
    }
}
Bevor
  • 8,396
  • 15
  • 77
  • 141