AIDocumentLibraryChat 项目已扩展为生成测试代码(Java 代码已经过测试)。该项目可以为公开可用的 GitHub 项目生成测试代码。可以提供要测试的类的 URL,然后加载该类,分析导入,并加载项目中的依赖类。这使 LLM 有机会在为测试生成 mock 时考虑导入的源类。可以提供 for 为 LLM 提供示例,以作为生成的测试的基础。granite-code 和 deepseek-coder-v2 模型已使用 Ollama 进行了测试。testUrl
目标是测试 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-thread
spring.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 以及要测试的类的源代码、它在项目中的依赖项以及测试示例。CodeGenerationController
getSources(...)
testUrl
createTestSources(...)
GithubSources
该方法获取 for the test 类和 optional to be decode,并调用 .getGenerateTests(...)
url
testUrl
url
generateTests(...)
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 接口。CodeGenerationService
GithubClient
ChatClient
GithubClient
ChatClient
这是上下文窗口为 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(...)
url
referencedSources
GithubSource
为此,创建 是为了获取类的原始源代码。然后 the 用于将源文件作为字符串读取。然后,源字符串在源行中上交,而不使用方法进行格式设置和注释。myUrl
githubClient
filterComments(...)
要读取项目中的依赖类,请使用基本包。例如,在包中,基本包为 .该方法用于为基本包中的依赖类创建记录。该参数用于筛选出类,然后递归调用该方法,并将参数设置为 false 以停止递归。这就是创建依赖类记录的方式。ch.xxx.aidoclibchat.usecase.service
ch.xxx
createDependencies(...)
GithubSource
basePackage
createTestSources(...)
referencedSources
GithubSource
该方法用于使用 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}
testUrl
classToTest
{classToTest}
调用 以将提示发送到 AI/LLM。将根据属性中上下文窗口的大小选择提示。这会将占位符替换为准备好的字符串。chatClient
contextWindowSize
PromptTemplate
这用于记录提示令牌、生成令牌和总令牌的数量,以便能够检查上下文窗口边界是否得到遵守。然后,记录生成测试源的时间并返回测试源。如果测试源的生成时间超过 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:20b
contextClasses
Ollama 上的 LLM 具有超过 64k 个令牌的上下文窗口。这样就可以将 the 添加到提示符中,并且它能够与思维链提示一起使用。deepseek-coder-v2:16b
contextClasses
结果
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 的字符,如果不了解语言语法,结果就不会令人印象深刻。开发人员必须能够修复测试中的所有错误。这意味着它只是节省了一些键入测试的时间。