You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何对调用同服务网格内部非暴露API的Scala函数做单元测试?

如何Mock同一服务网格内未暴露的API来测试Scala函数

当然可以通过Mock实现这类测试,下面是两种最可行的解决方案:

方案一:依赖注入+Mock框架(推荐)

原代码的问题在于直接硬编码URL并创建HttpURLConnection,导致无法直接替换依赖。解决思路是把HTTP请求逻辑抽离成可注入的依赖,测试时用Mock框架替换真实实现。

步骤1:抽离HTTP客户端接口

先定义一个抽象的HTTP客户端 trait,封装请求逻辑:

trait HttpClient {
  def sendRequest(url: String, requestPayload: String): HttpURLConnection
}

步骤2:重构原函数依赖注入

修改internalApiCall,让它依赖这个trait,而不是直接创建连接:

// 生产环境实现真实的HttpClient
class RealHttpClient extends HttpClient {
  override def sendRequest(url: String, requestPayload: String): HttpURLConnection = {
    val conn = new URL(url).openConnection().asInstanceOf[HttpURLConnection]
    // 补充设置请求方法、写入payload等逻辑
    conn
  }
}

// 原函数改为默认依赖注入,不影响生产调用
def internalApiCall(payload: String, client: HttpClient = new RealHttpClient()): Unit = {
  val request = buildRequestForApi(payload)
  val response = client.sendRequest("http://service.namespace.svc.cluster.local:8080/path", request)
  // 响应码断言逻辑
  assert(response.getResponseCode == 200)
}

步骤3:编写Mock测试

用ScalaMock或Mockito创建HttpClient的Mock实例,预设返回值并验证调用:

import org.scalamock.scalatest.MockFactory
import org.scalatest.funsuite.AnyFunSuite

class InternalApiTest extends AnyFunSuite with MockFactory {
  test("internalApiCall should pass when API returns 200") {
    // 创建Mock的HttpClient和HttpURLConnection
    val mockClient = mock[HttpClient]
    val mockConn = mock[HttpURLConnection]

    // 预设Mock连接返回200响应码
    (mockConn.getResponseCode _).returns(200)
    // 预设sendRequest返回Mock连接
    (mockClient.sendRequest(_: String, _: String)).returns(mockConn)

    // 执行测试
    internalApiCall("test-payload", mockClient)

    // 验证HttpClient的sendRequest是否被正确调用
    (mockClient.sendRequest _).verify(
      "http://service.namespace.svc.cluster.local:8080/path",
      buildRequestForApi("test-payload")
    )
  }

  test("internalApiCall should fail when API returns 500") {
    val mockClient = mock[HttpClient]
    val mockConn = mock[HttpURLConnection]

    (mockConn.getResponseCode _).returns(500)
    (mockClient.sendRequest(_: String, _: String)).returns(mockConn)

    // 断言抛出预期的断言错误
    assertThrows[AssertionError] {
      internalApiCall("test-payload", mockClient)
    }
  }
}

方案二:嵌入式Mock HTTP服务器(最小代码改动)

如果不想大规模重构原代码,可以用嵌入式HTTP服务器(如WireMock、OkHttp MockWebServer)在测试时启动一个本地Mock服务,替换原函数的目标URL。

步骤1:引入WireMock依赖

在sbt构建文件中添加测试依赖:

libraryDependencies += "com.github.tomakehurst" % "wiremock-jre8" % "2.35.0" % Test

步骤2:编写测试用例

启动本地Mock服务,配置预期的响应,并替换原函数的URL:

import com.github.tomakehurst.wiremock.WireMockServer
import com.github.tomakehurst.wiremock.client.WireMock
import org.scalatest.BeforeAndAfterAll
import org.scalatest.funsuite.AnyFunSuite

class InternalApiTest extends AnyFunSuite with BeforeAndAfterAll {
  private val mockServer = new WireMockServer(8081) // 本地测试端口

  override def beforeAll(): Unit = {
    mockServer.start()
    WireMock.configureFor("localhost", 8081)
    // 配置Mock服务接收POST请求并返回200
    WireMock.stubFor(WireMock.post(WireMock.urlEqualTo("/path"))
      .willReturn(WireMock.aResponse().withStatus(200)))
  }

  override def afterAll(): Unit = {
    mockServer.stop()
  }

  test("internalApiCall handles successful response from mock API") {
    // 临时替换原函数中的硬编码URL(建议把URL改成可配置项,测试时注入本地地址)
    // 示例:假设原URL从配置读取,这里临时修改配置
    // Config.apiUrl = "http://localhost:8081/path"

    // 执行测试
    internalApiCall("test-payload")

    // 验证Mock服务是否收到了预期的请求
    WireMock.verify(WireMock.postRequestedFor(WireMock.urlEqualTo("/path"))
      .withRequestBody(WireMock.equalTo(buildRequestForApi("test-payload"))))
  }
}

总结

  • 依赖注入+Mock框架的方式更灵活,适合长期维护的代码,能完全隔离外部依赖;
  • 嵌入式Mock服务器适合快速测试,无需大规模重构,但需要启动本地服务,测试速度略慢。

内容的提问来源于stack exchange,提问作者Abhishek

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.17 06:30:41