有没有办法阻止Java JUnit测试的所有外部网络访问?

有没有办法阻止Java JUnit测试的所有外部网络访问?,java,spring,spring-boot,unit-testing,networking,Java,Spring,Spring Boot,Unit Testing,Networking,我经常使用Java应用程序(使用或构建),这些应用程序对外部服务(例如,对类似外部的API)进行网络调用。在运行automated()测试时,我不希望发生这些调用,即使是偶然的。调用真正的端点(甚至是设计用于测试的“dev”环境)可能会影响外部状态,导致不必要的服务器工作负载,或者无意中将信息透露给端点。它还可能导致测试无意中依赖于正在运行的外部资源,如果资源在构建项目时或在脱机构建项目时宕机,则会导致构建失败 这些调用通常应该发生的情况是,它们要么不执行任何操作,要么应该指向同一台计算机上的服

我经常使用Java应用程序(使用或构建),这些应用程序对外部服务(例如,对类似外部的API)进行网络调用。在运行automated()测试时,我不希望发生这些调用,即使是偶然的。调用真正的端点(甚至是设计用于测试的“dev”环境)可能会影响外部状态,导致不必要的服务器工作负载,或者无意中将信息透露给端点。它还可能导致测试无意中依赖于正在运行的外部资源,如果资源在构建项目时或在脱机构建项目时宕机,则会导致构建失败

这些调用通常应该发生的情况是,它们要么不执行任何操作,要么应该指向同一台计算机上的服务(例如,由测试启动的本地主机上的端点,或者由测试使用的本地Docker容器启动的本地Docker容器)

在这些情况下,我倾向于创建一个特定于测试的测试,该测试使用一些明显虚假的东西覆盖所有HTTP端点,这些东西保证不会路由到某个地方,例如
http://example.invalid/bookSearch
。这样,如果开发人员没有在测试中模拟或更改端点,它就不会调用真正的端点并触发真正的副作用

但是,这种方法可能容易出错,并且在实现过程中的疏忽或将来的更改可能会导致仍然进行网络调用。例子包括:

  • 开发人员可以添加一个新的端点,而无需记住或知道他们需要覆盖测试路由
  • 可能会错过在第一时间被覆盖的端点
  • 第三方库可能会提出开发人员不知道或未说明的请求
  • 这主要是一个与Spring集成测试有关的问题,Spring集成测试会启动部分或全部环境(例如,使用
    @springbootest
    @extendedwith(SpringExtension.class)
    )。然而,如果一个组件使用某个组件作为连接到网络的非必需构造函数参数,或者如果一个开发人员传入了一个真正的网络连接组件,而他们本应该使用一个模拟组件或连接到
    localhost
    的某个组件,那么这可能适用于单元测试

    我突然想到,对于这种情况,可能有一种更为防弹的解决方案。例如,也许有一种方法可以告诉JVM或其底层HTTP/FTP/etc库阻止所有外部网络流量


    在Java Spring Boot JUnit测试中,有没有办法阻止非本地网络访问,或者以其他方式保证不会调用外部网络端点?

    我不知道在JVM环境中有什么办法可以做到这一点


    听起来您谈论的更多的是功能/集成测试,而不是单元测试。我猜想,docker可能会工作得很好,因为您可以明确地隔离容器的网络。我个人使用WireMock和docker compose来做一些非常类似的事情。

    将连接重定向到一个不存在的代理应该提供您所要求的行为

    使用:

    对于HTTP:

    • http.proxyHost
    • http.proxyPort
    对于HTTPS:

    • https.proxyHost
    • https.proxyPort
    对于FTP:

    • ftp.proxyHost
    • ftp.proxyPort
    SOCKS代理设置可用于拦截所有TCP流量:

    • SocksProxy主机
    • 插座式氧接端口
    为上面的任何
    *Host
    属性指定
    localhost
    ,将强制JVM尝试连接到运行构建的主机。只要端口上没有进程侦听,连接尝试就会失败。SOCKS端口1080对于上述任何
    *端口
    属性都是安全的

    另一种选择是使用类似于在构建主机上打开一个端口并重定向那里的所有TCP流量,但在建立连接后立即在WireMock端断开连接

    对于基于Maven的构建系统-检查有关如何为测试指定系统属性的文档:

      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>3.0.0-M5</version>
        <configuration>
          <systemPropertyVariables>
            <socksProxyHost>localhost</socksProxyHost>
            <socksProxyPort>1080</socksProxyPort>
            [...]
          </systemPropertyVariables>
        </configuration>
      </plugin>
    
    
    org.apache.maven.plugins
    maven surefire插件
    3.0.0-M5
    本地服务器
    1080
    [...]
    
    在Java中,所有TCP网络连接都是由javax.net.SocketFactory创建的。默认实例存储在javax.net.SocketFactory.theFactory中

    我将使用对字段的不安全访问(或Java 9+所需的相应sutff),并使用引发异常的内容替换默认套接字工厂,而不是创建套接字,或者至少在为“127.0.0.1”或“localhost”或映射到localhost的任何名称之外的其他名称创建套接字时


    我认为还有一个javax.net.ssl.SSLSocketFactory,它也必须被替换。之后你应该安全了。

    我想我应该再提出一个想法。如果您可以禁用/更改JVM的DNS设置,从而故意破坏地址解析,该怎么办

    主要的优点是不必断开JVM,也不必断开整个计算机网络

    主要的缺点是,任何直接IP可能仍能正常工作并访问其特定服务。(虽然这可能是一个专业版,但如果您发现有一两个服务确实需要通过您设置的破碎DNS世界。)

    考虑到您可以使用主机名在线解析URI而不是直接解析IP,这似乎是一种有意破坏连接的方法

    所以问题是,你如何打破DNS?可能有十几种方法可以做到这一点,但我想我应该尝试寻找可以动态更改这一点的网络设置,而不会影响测试环境JVM本身之外的任何东西import java.io.BufferedReader; import java.io.InputStreamReader; import java.net.URL; import java.net.URLConnection; import java.time.Duration; import java.time.Instant; public class BlockNetworkByProperty { public static void main(String[] args) throws Exception { System.setProperty("sun.net.spi.nameservice.nameservers", "192.168.0.254"); // Pick any dead IP. System.setProperty("sun.net.spi.nameservice.provider.1", "dns,sun"); Instant start = Instant.now(); System.out.println("Attempting connection.. "); try { URL oracle = new URL("https://www.cnet.com/"); URLConnection yc = oracle.openConnection(); BufferedReader in = new BufferedReader(new InputStreamReader( yc.getInputStream())); String inputLine; while ((inputLine = in.readLine()) != null) System.out.println(inputLine); in.close(); System.out.println("Should not have gotten here."); } finally { long secondsPassed = Duration.between(start, Instant.now()).getSeconds(); System.out.println(String.format("%s seconds have passed.", secondsPassed)); } } }
    Attempting connection.. 
    60 seconds have passed.
    Exception in thread "main" java.net.UnknownHostException: www.cnet.com
    at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:184)
    ...stack trace continues
    
    docker run --network none
    
        @Rule public SniffyRule sniffyRule = new SniffyRule();
    
        @Test
        @DisableSockets
        public void testDisableSockets() throws IOException {
            try {
                new Socket("google.com", 443);
                fail("Sniffy should have thrown ConnectException");
            } catch (ConnectException e) {
                assertNotNull(e);
            }
        }