将 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://github.com", 
    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://github.com/Angular2Guy/MovieManager/blob/master/backend/src/main/java/ch/xxx/moviemanager/adapter/controller/ActorController.java&testUrl=https://github.com/Angular2Guy/MovieManager/blob/master/backend/src/test/java/ch/xxx/moviemanager/adapter/controller/MovieControllerTest.java
http://localhost:8080/rest/code-generation/test?url=https://github.com/Angular2Guy/MovieManager/blob/master/backend/src/main/java/ch/xxx/moviemanager/usecase/service/ActorService.java&testUrl=https://github.com/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 的字符,如果不了解语言语法,结果就不会令人印象深刻。开发人员必须能够修复测试中的所有错误。这意味着它只是节省了一些键入测试的时间。

目录
相关文章
|
3天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
16 2
|
27天前
|
机器学习/深度学习 人工智能 自然语言处理
探索AI在软件测试中的转型力量###
本文深入探讨了人工智能(AI)技术在软件测试领域的应用现状与未来趋势,通过分析AI如何优化测试流程、提高测试效率与质量,揭示了AI赋能下软件测试行业的转型路径。传统测试方法面临效率低、成本高、覆盖率有限等挑战,而AI技术的引入正逐步改变这一格局,为软件测试带来革命性的变化。 ###
|
24天前
|
Java 测试技术 开发者
必学!Spring Boot 单元测试、Mock 与 TestContainer 的高效使用技巧
【10月更文挑战第18天】 在现代软件开发中,单元测试是保证代码质量的重要手段。Spring Boot提供了强大的测试支持,使得编写和运行测试变得更加简单和高效。本文将深入探讨Spring Boot的单元测试、Mock技术以及TestContainer的高效使用技巧,帮助开发者提升测试效率和代码质量。
131 2
|
15天前
|
人工智能 前端开发 Java
基于开源框架Spring AI Alibaba快速构建Java应用
本文旨在帮助开发者快速掌握并应用 Spring AI Alibaba,提升基于 Java 的大模型应用开发效率和安全性。
基于开源框架Spring AI Alibaba快速构建Java应用
|
23天前
|
前端开发 Java 数据库连接
Spring 框架:Java 开发者的春天
Spring 框架是一个功能强大的开源框架,主要用于简化 Java 企业级应用的开发,由被称为“Spring 之父”的 Rod Johnson 于 2002 年提出并创立,并由Pivotal团队维护。
43 1
Spring 框架:Java 开发者的春天
|
15天前
|
Java 测试技术 Maven
Java一分钟之-PowerMock:静态方法与私有方法测试
通过本文的详细介绍,您可以使用PowerMock轻松地测试Java代码中的静态方法和私有方法。PowerMock通过扩展Mockito,提供了强大的功能,帮助开发者在复杂的测试场景中保持高效和准确的单元测试。希望本文对您的Java单元测试有所帮助。
29 2
|
16天前
|
人工智能 测试技术 Windows
Windows 竞技场:面向下一代AI Agent的测试集
【10月更文挑战第25天】随着人工智能的发展,大型语言模型(LLMs)在多模态任务中展现出巨大潜力。为解决传统基准测试的局限性,研究人员提出了Windows Agent Arena,一个在真实Windows操作系统中评估AI代理性能的通用环境。该环境包含150多个多样化任务,支持快速并行化评估。研究团队还推出了多模态代理Navi,在Windows领域测试中成功率达到19.5%。尽管存在局限性,Windows Agent Arena仍为AI代理的评估和研究提供了新机遇。
37 3
|
23天前
|
Java 数据库连接 开发者
Spring 框架:Java 开发者的春天
【10月更文挑战第27天】Spring 框架由 Rod Johnson 在 2002 年创建,旨在解决 Java 企业级开发中的复杂性问题。它通过控制反转(IOC)和面向切面的编程(AOP)等核心机制,提供了轻量级的容器和丰富的功能,支持 Web 开发、数据访问等领域,显著提高了开发效率和应用的可维护性。Spring 拥有强大的社区支持和丰富的生态系统,是 Java 开发不可或缺的工具。
|
22天前
|
Java 程序员 测试技术
Java|让 JUnit4 测试类自动注入 logger 和被测 Service
本文介绍如何通过自定义 IDEA 的 JUnit4 Test Class 模板,实现生成测试类时自动注入 logger 和被测 Service。
21 5
|
21天前
|
机器学习/深度学习 数据采集 人工智能
探索AI驱动的自动化测试新纪元###
本文旨在探讨人工智能如何革新软件测试领域,通过AI技术提升测试效率、精准度和覆盖范围。在智能算法的支持下,自动化测试不再局限于简单的脚本回放,而是能够模拟复杂场景、预测潜在缺陷,并实现自我学习与优化。我们正步入一个测试更加主动、灵活且高效的新时代,本文将深入剖析这一变革的核心驱动力及其对未来软件开发的影响。 ###