如何为使用网络连接的类编写jUnit测试

28

我想知道使用jUnit测试以下类中的"pushEvent()"方法的最佳方法是什么。我的问题是,私有方法"callWebsite()"总是需要连接到网络。如何避免这个要求或重构我的类以便在没有网络连接的情况下测试它?

class MyClass {

    public String pushEvent (Event event) {
        //do something here
        String url = constructURL (event); //construct the website url
        String response = callWebsite (url);

        return response;
    }

    private String callWebsite (String url) {
        try {
            URL requestURL = new URL (url);
            HttpURLConnection connection = null;
            connection = (HttpURLConnection) requestURL.openConnection ();


            String responseMessage = responseParser.getResponseMessage (connection);
            return responseMessage;
        } catch (MalformedURLException e) {
            e.printStackTrace ();
            return e.getMessage ();
        } catch (IOException e) {
            e.printStackTrace ();
            return e.getMessage ();
        }
    }

}
5个回答

28

桩对象

为了进行孤立、简单、单元测试,您需要使用一个测试替身(桩对象)。以下示例未经过测试,但演示了其想法。使用依赖注入可以允许您在测试时注入HttpURLConnection的测试版本。

public class MyClass() 
{
   private IHttpURLConnection httpUrlConnection;   

   public MyClass(IHttpURLConnection httpUrlConnection)
   {
       this.httpUrlConnection = httpUrlConnection;
   }

   public String pushEvent(Event event) 
   {
       String url = constructURL(event);
       String response = callWebsite(url);
       return response;
  }
}

然后你创建一个存根(有时称为模拟对象),作为具体实例的替身。

class TestHttpURLConnection : IHttpURLConnection { /* Methods */ }

你还需要构建一个具体版本,以供你的生产代码使用。

class MyHttpURLConnection : IHttpURLConnection { /* Methods */ }

使用测试类(一种适配器)可以指定测试过程中应发生的情况。使用模拟框架可以用更少的代码完成这项工作,或者您可以手动连接它们。测试的最终结果是您将为您的测试设置期望,例如在此情况下,您可能会将OpenConnection设置为返回true布尔值(这只是一个例子)。然后,您的测试将断言当该值为true时,PushEvent方法的返回值与某些预期结果匹配。我已经有一段时间没有好好接触Java了,但以下是一些由StackOverflow成员指定的推荐的模拟框架


我正要输入非常类似的内容。我认为他需要像你提到的那样将类与URL连接解耦,然后他的单元测试将测试与模拟URLConnection的交互。他还需要一个集成测试来测试与URLConnection的具体实例的集成。 - Athens Holloway
1
我应该提到模拟是一个多重载的术语。本质上,它归结为使用一个假对象来实现隔离测试,在SO上检查其他模拟问题以获得更广泛的概述。如果有人告诉你要使用模拟,就像我一样,你可以使用一个假/存根/模拟/手动滚动的对象来实现。 - Finglas
1
+1,这是一个比被接受的答案更普遍、适用的解决方案。 - Grundlefleck
1
@Grundlefleck 我同意你的观点,但同时被采纳的答案也是一个完全有效的解决方案。事实上,我建议在进行单元测试时首先使用测试扩展而不是模拟框架。一旦测试扩展变得繁琐和/或乏味,就是转向模拟框架的时候了。 - Finglas
哈哈,当嘲笑变得疲惫和昂贵时,@Finglas该怎么办? :) - Snicolas

10
可能的解决方案:您可以扩展这个类,重写callWebsite方法(需要将其设置为protected)- 并在重写方法中编写一些桩方法实现。

7

从略微不同的角度来看待事物...

我会更少担心测试这个特定的类。它里面的代码非常简单,虽然进行一个功能测试以确保它与连接一起工作会很有帮助,但单元级别的测试“可能”不是必要的。

相反,我会专注于测试它调用的那些似乎真正做了些什么的方法。具体来说...

我会测试从这一行开始的constructURL方法:

String url = constructURL (event);

确保它可以从不同的事件正确构建URL,并在必要时抛出异常(可能是无效的事件或空值)。

我将测试以下行中的方法:

String responseMessage = responseParser.getResponseMessage (connection);

可能将任何“从连接中获取信息”的逻辑提取到一个过程中,并仅在原始过程中保留“解析该信息”:
String responseMessage = responseParser.getResponseMessage(responseParser.getResponseFromConnection(connection));

或类似的东西。

这个想法是将任何“必须处理外部数据源”的代码放在一个方法中,而把任何逻辑代码放在单独的方法中,这些方法可以很容易地进行测试。


4

作为对Finglas有关模拟的有用回答的替代方案,请考虑使用存根方法,其中我们覆盖了callWebsite()的功能。这在我们对pushEvent()中调用的其他逻辑比对callWebsite的逻辑不那么感兴趣的情况下非常有效。检查一个重要的事情是callWebsite是否使用正确的URL进行调用。因此,第一次更改是将callWebsite()的方法签名更改为:

protected String callWebsite(String url){...}

现在我们创建一个类似这样的存根类:
class MyClassStub extends MyClass {
    private String callWebsiteUrl;
    public static final String RESPONSE = "Response from callWebsite()";

    protected String callWebsite(String url) {
        //don't actually call the website, just hold onto the url it was going to use
        callWebsiteUrl = url;
        return RESPONSE;
    }
    public String getCallWebsiteUrl() { 
        return callWebsiteUrl; 
    }
}

最后,在我们的JUnit测试中:

public class MyClassTest extends TestCase {
    private MyClass classUnderTest;
    protected void setUp() {
        classUnderTest = new MyClassStub();
    }
    public void testPushEvent() { //could do with a more descriptive name
        //create some Event object 'event' here
        String response = classUnderTest.pushEvent(event);
        //possibly have other assertions here
        assertEquals("http://some.url", 
                     (MyClassStub)classUnderTest.getCallWebsiteUrl());
        //finally, check that the response from the callWebsite() hasn't been 
        //modified before being returned back from pushEvent()
        assertEquals(MyClassStub.RESPONSE, response);
    }
 }

2
创建一个抽象类WebsiteCaller,作为ConcreteWebsiteCallerWebsiteCallerStub的父类。
这个类应该有一个方法callWebsite (String url)。将你的callWebsite方法从MyClass移动到ConcreteWebsiteCaller中。而MyClass将会是这样:
class MyClass {

    private WebsiteCaller caller;

    public MyClass (WebsiteCaller caller) {
        this.caller = caller;
    }

    public String pushEvent (Event event) {
        //do something here
        String url = constructURL (event); //construct the website url
        String response = caller.callWebsite (url);

        return response;
    }
}

并以某种适合测试的方式在您的WebsiteCallerStub中实现callWebsite方法。

然后,在您的单元测试中,可以按以下方式操作:

@Test
public void testPushEvent() {
   MyClass mc = new MyClass (new WebsiteCallerStub());
   mc.pushEvent (new Event(...));
}

我认为你展示的是一个集成测试,因为你的测试依赖于外部资源。更好的单元测试方法是注入一个模拟/存根websitecaller接口。你的集成测试可以专门测试websitecaller类以及注入到Mycalss中的websitecaller。 - Athens Holloway

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接