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

目录
相关文章
|
1月前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
71 2
|
9天前
|
人工智能 自然语言处理 前端开发
CodeArena:在线 LLM 编程竞技场!用于测试不同开源 LLM 的编程能力,实时更新排行榜
CodeArena 是一个在线平台,用于测试和比较不同大型语言模型(LLM)的编程能力。通过实时显示多个 LLM 的代码生成过程和结果,帮助开发者选择适合的 LLM,并推动 LLM 技术的发展。
38 7
CodeArena:在线 LLM 编程竞技场!用于测试不同开源 LLM 的编程能力,实时更新排行榜
|
5天前
|
机器学习/深度学习 人工智能 自然语言处理
AI自己长出了类似大脑的脑叶?新研究揭示LLM特征的惊人几何结构
近年来,大型语言模型(LLM)的内部运作机制备受关注。麻省理工学院的研究人员在论文《The Geometry of Concepts: Sparse Autoencoder Feature Structure》中,利用稀疏自编码器(SAE)分析LLM的激活空间,揭示了其丰富的几何结构。研究发现,特征在原子、大脑和星系三个尺度上展现出不同的结构,包括晶体结构、中尺度模块化结构和大尺度点云结构。这些发现不仅有助于理解LLM的工作原理,还可能对模型优化和其他领域产生重要影响。
46 25
|
21天前
|
Java 开发者 微服务
Spring Boot 入门:简化 Java Web 开发的强大工具
Spring Boot 是一个开源的 Java 基础框架,用于创建独立、生产级别的基于Spring框架的应用程序。它旨在简化Spring应用的初始搭建以及开发过程。
38 6
Spring Boot 入门:简化 Java Web 开发的强大工具
|
10天前
|
人工智能 数据挖掘
AI长脑子了?LLM惊现人类脑叶结构并有数学代码分区,MIT大牛新作震惊学界!
麻省理工学院的一项新研究揭示了大型语言模型(LLM)内部概念空间的几何结构,与人脑类似。研究通过分析稀疏自编码器生成的高维向量,发现了概念空间在原子、大脑和星系三个层次上的独特结构,为理解LLM的内部机制提供了新视角。论文地址:https://arxiv.org/abs/2410.19750
51 12
|
1月前
|
存储 人工智能 Java
Spring AI Alibaba 配置管理,用 Nacos 就够了
本文通过一些实操案例展示了 Spring AI Alibaba + Nacos 在解决 AI 应用中一系列复杂配置管理挑战的方案,从动态 Prompt 模板的灵活调整、模型参数的即时优化,到敏感信息的安全加密存储。Spring AI Alibaba 简化了对接阿里云通义大模型的流程,内置 Nacos 集成也为开发者提供了无缝衔接云端配置托管的捷径,整体上极大提升了 AI 应用开发的灵活性和响应速度。
214 13
|
24天前
|
Java 测试技术 API
详解Swagger:Spring Boot中的API文档生成与测试工具
详解Swagger:Spring Boot中的API文档生成与测试工具
35 4
|
24天前
|
机器学习/深度学习 人工智能 自然语言处理
智能化软件测试:AI驱动的自动化测试策略与实践####
本文深入探讨了人工智能(AI)在软件测试领域的创新应用,通过分析AI技术如何优化测试流程、提升测试效率及质量,阐述了智能化软件测试的核心价值。文章首先概述了传统软件测试面临的挑战,随后详细介绍了AI驱动的自动化测试工具与框架,包括自然语言处理(NLP)、机器学习(ML)算法在缺陷预测、测试用例生成及自动化回归测试中的应用实例。最后,文章展望了智能化软件测试的未来发展趋势,强调了持续学习与适应能力对于保持测试策略有效性的重要性。 ####
|
1月前
|
人工智能 前端开发 Java
基于开源框架Spring AI Alibaba快速构建Java应用
本文旨在帮助开发者快速掌握并应用 Spring AI Alibaba,提升基于 Java 的大模型应用开发效率和安全性。
基于开源框架Spring AI Alibaba快速构建Java应用
|
1月前
|
人工智能 供应链 安全
AI辅助安全测试案例某电商-供应链平台平台安全漏洞
【11月更文挑战第13天】该案例介绍了一家电商供应链平台如何利用AI技术进行全面的安全测试,包括网络、应用和数据安全层面,发现了多个潜在漏洞,并采取了有效的修复措施,提升了平台的整体安全性。
下一篇
DataWorks