测试设计:如何在不重写的情况下正确测试方法?

时间:2012-09-07 18:48:32

标签: java junit mocking

我有以下类,其中包含永远不会更改的硬编码URL:

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

现在想象一下,我想在我的测试中期待IOException。在我看来,唯一的方法是在模拟对象中重写完整的类,因为最终的变量:

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

但这在某种程度上是牵强附会的。如果原始方法会被更改,测试结果仍然可能是正面的,因为通过这种尝试,我实际上不再测试原始类。

如何正确完成? (我不想仅仅为这一个测试使用框架,所以有没有设计尝试以一种常见的方式解决这个问题?)

3 个答案:

答案 0 :(得分:2)

许多人在开始测试驱动开发(或只是涉足其中)时面临的挑战之一是编写可测试的代码。这个概念背后的一个重要思想是编写模块化代码的离散单元,这些代码很容易测试并避免像上面那样的情况。

您不应在类定义中指定硬编码值,而应将其作为参数接受并将其传递到其他位置。如果需要此硬编码值,可以在类定义中将其指定为静态,并在生产使用中需要时将其作为参数传递。

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

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

        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(String url) throws IOException {
        URL jsonLocator = new URL(url);

        return jsonLocator.openConnection();
    }
}

...常规用例

public class SomeClass {
    public static void main(String[] args) {
        HttpClient client = new HttpClient();
        String result = client.readJsonDataFromUrl(HttpClient.DEFAULT_URL);
    }
}

...并测试你的代码:

public class HttpClientTest {
    // TODO expect exception
    @Test
    public void failDownload( ) {
        HttpClient client = new HttpClient();
        String result = client.readJsonDataFromUrl("wrong.url");
    }
}

答案 1 :(得分:1)

Gilbert Le Blanc建议的另一个问题是,通过构造函数注入它,使HttpClient完全不知道URL。

 public class HttpClient {
   private final String url;

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

 }

您可以在HttpClient外部的某处硬编码URL(或从配置中读取),并在实例化客户端的任何地方注入它。然后在你的测试中,注入一个糟糕的网址将是微不足道的。

答案 2 :(得分:0)

感谢大家,但我认为Gilbert Le Blanc的解决方案最适合这种情况,如下所示:

原班级:

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

模拟对象:

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

工作测试:

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