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

Java 如何为使用网络连接的类编写jUnit测试,java,unit-testing,tdd,junit,Java,Unit Testing,Tdd,Junit,我想知道用jUnit测试在下面的类中测试方法“pushEvent()”的最佳方法是什么。 我的问题是,私有方法“callWebsite()”总是需要连接到网络。我如何避免这一要求,或者重构我的类,以便在没有网络连接的情况下对其进行测试 class MyClass { public String pushEvent (Event event) { //do something here String url = constructURL (event);

我想知道用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 ();
        }
    }

}
您需要一个双测试(存根)来允许独立、简单的单元测试。以下是未经测试的,但演示了该想法。使用将允许您在测试时注入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 */ }

使用您的测试类(an),您可以指定测试期间应该发生什么。模拟框架将使您能够用更少的代码完成这项工作,或者您可以手动将其连接起来。测试的最终结果是,您将为测试设置期望值,例如,在这种情况下,您可以将OpenConnection设置为返回一个真正的布尔值(顺便说一下,这只是一个示例)。然后,测试将断言,当该值为true时,PushEvent方法的返回值与某些预期结果匹配。我已经有一段时间没有正确地接触Java了,但是这里有一些是StackOverflow成员指定的。

可能的解决方案:您可以扩展这个类,覆盖callWebsite(您必须为此对它进行保护)-重写方法编写一些存根方法实现。

创建一个抽象类
WebsiteCaller
,它将是
ConcreteWebsiteCaller
WebsiteCallerStub
的父类

这个类应该有一个方法
callWebsite(stringurl)
。将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(...));
}
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; 
    }
}

从一个稍微不同的角度来看待事情

我不太担心测试这个特定的类。其中的代码非常简单,虽然功能测试可以确保它与连接一起工作,但单元级测试“可能”是不必要的

相反,我将重点测试它调用的方法,这些方法看起来确实做了些什么。具体地说

我将从以下行测试constructURL方法:

String url = constructURL (event);
确保它可以从不同的事件正确地构造URL,并在应该时抛出异常(可能是在无效事件或null上)

我将从以下行测试该方法:

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

String responseMessage = responseParser.getResponseMessage(responseParser.getResponseFromConnection(connection));
或者类似的东西


一个方法是“必须用一个方法处理外部数据源”代码,以及任何可以容易测试的方法中的任何代码逻辑。当我们对callWebsite的逻辑不像pushEvent()中调用的其他逻辑那样感兴趣时,这种方法非常有效。需要检查的一件重要事情是调用callWebsite时使用了正确的URL。因此,第一个更改是callWebsite()的方法签名变为:

protected String callWebsite(String url){...}
现在,我们创建一个存根类,如下所示:

@Test
public void testPushEvent() {
   MyClass mc = new MyClass (new WebsiteCallerStub());
   mc.pushEvent (new Event(...));
}
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);
    }
 }

我正要打一些非常相似的东西。我认为他需要像你提到的那样将他的类与url连接分离。然后他的单元测试将测试与模拟urlconnection的交互。他还需要一个集成测试来测试与url连接的具体实例的集成。从本质上讲,这一切归结为使用一个伪对象来启用独立测试,请查看此处的其他模拟问题,以便获得更广泛的概述。如果有人像我一样告诉你使用模拟,你可以用一个假的/stub/mock/hand rolled对象来这样做。+1,这是一个比公认的答案更普遍、更适用的解决方案。@Grundlefleck我同意,但同时公认的答案是一个完全有效的解决方案。事实上,我建议在单元测试时开始测试扩展而不是模拟框架。一旦测试扩展变得痛苦/和/或乏味-是时候转向mocking framework.Lol了,当mocking变得疲劳和昂贵时,该怎么办@Finglas?:)我认为您演示的是集成测试,因为您的测试依赖于外部资源。更好的单元测试将注入模拟/存根websitecaller接口。集成测试可以专门测试websitecaller类以及注入Mycalss的websitecaller