将 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 的字符,如果不了解语言语法,结果就不会令人印象深刻。开发人员必须能够修复测试中的所有错误。这意味着它只是节省了一些键入测试的时间。

目录
打赏
0
0
1
0
94
分享
相关文章
Java也能快速搭建AI应用?一文带你玩转Spring AI可落地性
Java语言凭借其成熟的生态与解决方案,特别是通过 Spring AI 框架,正迅速成为 AI 应用开发的新选择。本文将探讨如何利用 Spring AI Alibaba 构建在线聊天 AI 应用,并实现对其性能的全面可观测性。
Spring AI,搭建个人AI助手
本期主要是实操性内容,聊聊AI大模型,并使用Spring AI搭建属于自己的AI助手、知识库。本期所需的演示源码笔者托管在Gitee上(https://gitee.com/catoncloud/spring-ai-demo),读者朋友可自行查阅。
1273 43
Spring AI,搭建个人AI助手
Potpie.ai:比Copilot更狠!这个AI直接接管项目代码,自动Debug+测试+开发全搞定
Potpie.ai 是一个基于 AI 技术的开源平台,能够为代码库创建定制化的工程代理,自动化代码分析、测试和开发任务。
157 19
Potpie.ai:比Copilot更狠!这个AI直接接管项目代码,自动Debug+测试+开发全搞定
OctoTools:斯坦福开源AI推理神器!16项测试准确率碾压GPT-4o,一键搞定复杂任务
OctoTools 是斯坦福大学推出的开源智能体框架,通过标准化工具卡片和自动化工具集优化算法,显著提升复杂推理任务的解决效率,支持多领域应用。
52 3
OctoTools:斯坦福开源AI推理神器!16项测试准确率碾压GPT-4o,一键搞定复杂任务
【AI程序员】通义灵码 AI 程序员全面上线JAVA使用体验
通过 AI 程序编写一个JAVA后台项目登陆页面
370 42
Spring AI与DeepSeek实战一:快速打造智能对话应用
在 AI 技术蓬勃发展的今天,国产大模型DeepSeek凭借其低成本高性能的特点,成为企业智能化转型的热门选择。而Spring AI作为 Java 生态的 AI 集成框架,通过统一API、简化配置等特性,让开发者无需深入底层即可快速调用各类 AI 服务。本文将手把手教你通过spring-ai集成DeepSeek接口实现普通对话与流式对话功能,助力你的Java应用轻松接入 AI 能力!虽然通过Spring AI能够快速完成DeepSeek大模型与。
159 11
AI做数学学会动脑子! UCL等发现LLM程序性知识,推理绝不是背答案
大型语言模型(LLM)在数学推理中的表现一直备受争议。伦敦大学学院等机构的研究发现,LLM可能通过综合程序性知识而非简单检索来解决数学问题。研究分析了7B和35B参数模型在三个简单数学任务中的数据依赖,表明模型更关注解决问题的过程和方法,而非答案本身。这一发现为改进AI系统提供了新思路,但也指出LLM在复杂问题处理上仍存在局限。论文地址:https://arxiv.org/abs/2411.12580
11 2
Spring AI Alibaba 应用框架挑战赛圆满落幕,恭喜获奖选手
第二届开放原子大赛 Spring AI Alibaba 应用框架挑战赛决赛于 2 月 23 日在北京圆满落幕。
DeepSeek + Higress AI 网关/Spring AI Alibaba 案例征集
诚挚地感谢每一位持续关注并使用 Higress 和 Spring AI Alibaba 的朋友,DeepSeek + Higress AI 网关/Spring AI Alibaba 案例征集中。
Java 也能快速搭建 AI 应用?一文带你玩转 Spring AI 可观测性
Java 也能快速搭建 AI 应用?一文带你玩转 Spring AI 可观测性

热门文章

最新文章

AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等