|
1 | 1 | { |
2 | 2 | "cells": [ |
| 3 | + { |
| 4 | + "cell_type": "markdown", |
| 5 | + "source": [ |
| 6 | + "Generating unit tests for code is a very tedious task and often takes a significant effort from the developers to write good test cases. There are various tools that are available for automated test generation, such as, EvoSuite, which uses evolutionary algorithm to generate test cases. However, the test cases that are being generated are not natural and often developers do not prefer to add them to their test suite. Whereas, Large Language Models (LLM) being trained with developer-written code, it has better affinity towards generating more natural code--more readable, maintainable code. In this excercise, we will show we can leverage LLMs to generate test cases with the help of CLDK. \n", |
| 7 | + "\n", |
| 8 | + "For simplicity, we will cover certain aspects of test generation and provide some context information to LLM for better quality of test cases. In this excercise, we will generate unit test for non-private method from a Java class and provide the focal method body and the signature of all the constructors of the class so that LLM can understand how to create object of the focal class during the setup phase of the tests. Also, we will ask LLMs to generate ```N``` number of test cases, where ```N``` is the cyclomatic complexity of the focal method. The intuition is that one test may not be sufficient for covering fairly complex method and cyclomatic complexity score can provide some guidance towards that. \n", |
| 9 | + "\n", |
| 10 | + "(Step 1) First, we will import all the neccessary libraries" |
| 11 | + ], |
| 12 | + "metadata": { |
| 13 | + "collapsed": false |
| 14 | + }, |
| 15 | + "id": "5856baff4aa64ed7" |
| 16 | + }, |
3 | 17 | { |
4 | 18 | "cell_type": "code", |
5 | 19 | "execution_count": null, |
6 | | - "id": "initial_id", |
| 20 | + "outputs": [], |
| 21 | + "source": [ |
| 22 | + "from pathlib import Path\n", |
| 23 | + "import ollama\n", |
| 24 | + "from cldk import CLDK\n", |
| 25 | + "from cldk.analysis import AnalysisLevel" |
| 26 | + ], |
7 | 27 | "metadata": { |
8 | | - "collapsed": true |
| 28 | + "collapsed": false |
9 | 29 | }, |
| 30 | + "id": "b3d2498ae092fcc" |
| 31 | + }, |
| 32 | + { |
| 33 | + "cell_type": "markdown", |
| 34 | + "source": [ |
| 35 | + "(Step 2) Second, we will form the prompt for the model, which will include all the constructor signarures, and the body of the focal method." |
| 36 | + ], |
| 37 | + "metadata": { |
| 38 | + "collapsed": false |
| 39 | + }, |
| 40 | + "id": "67eb24b29826d730" |
| 41 | + }, |
| 42 | + { |
| 43 | + "cell_type": "code", |
| 44 | + "execution_count": null, |
10 | 45 | "outputs": [], |
11 | | - "source": [] |
| 46 | + "source": [ |
| 47 | + "def format_inst(focal_method_body, focal_method, focal_class, constructor_signatures, cyclomatic_complexity, language):\n", |
| 48 | + " \"\"\"\n", |
| 49 | + " Format the instruction for the given focal method and class.\n", |
| 50 | + " \"\"\"\n", |
| 51 | + " inst = f\"Question: Can you generate {cyclomatic_complexity} unit tests for the method `{focal_method}` in the class `{focal_class}` below?\\n\"\n", |
| 52 | + "\n", |
| 53 | + " inst += \"\\n\"\n", |
| 54 | + " inst += f\"```{language}\\n\"\n", |
| 55 | + " inst += \"```\\n\"\n", |
| 56 | + " inst += \"public class {focal_class} {\"\n", |
| 57 | + " inst += f\"<|constructors|>\\n{constructor_signatures}\\n<|constructors|>\\n\"\n", |
| 58 | + " inst += f\"<|focal method|>\\n {focal_method_body} \\n <|focal method|>\\n\" \n", |
| 59 | + " inst += \"}\"\n", |
| 60 | + " inst += \"```\\n\"\n", |
| 61 | + " inst += \"Answer:\\n\"\n", |
| 62 | + " return inst" |
| 63 | + ], |
| 64 | + "metadata": { |
| 65 | + "collapsed": false |
| 66 | + }, |
| 67 | + "id": "d7bc9bbaa917df24" |
| 68 | + }, |
| 69 | + { |
| 70 | + "cell_type": "markdown", |
| 71 | + "source": [ |
| 72 | + "(Step 3) Third, use ollama to call LLM (in case Granite 8b)." |
| 73 | + ], |
| 74 | + "metadata": { |
| 75 | + "collapsed": false |
| 76 | + }, |
| 77 | + "id": "ae9ceb150f5efa92" |
| 78 | + }, |
| 79 | + { |
| 80 | + "cell_type": "code", |
| 81 | + "execution_count": null, |
| 82 | + "outputs": [], |
| 83 | + "source": [ |
| 84 | + "def prompt_ollama(message: str, model_id: str = \"granite-code:8b-instruct\") -> str:\n", |
| 85 | + " \"\"\"Prompt local model on Ollama\"\"\"\n", |
| 86 | + " response_object = ollama.generate(model=model_id, prompt=message)\n", |
| 87 | + " return response_object[\"response\"]" |
| 88 | + ], |
| 89 | + "metadata": { |
| 90 | + "collapsed": false |
| 91 | + }, |
| 92 | + "id": "52634feae7374599" |
| 93 | + }, |
| 94 | + { |
| 95 | + "cell_type": "markdown", |
| 96 | + "source": [ |
| 97 | + "(Step 3) Third, collect all the information needed for each method. " |
| 98 | + ], |
| 99 | + "metadata": { |
| 100 | + "collapsed": false |
| 101 | + }, |
| 102 | + "id": "308c3325116b87d4" |
| 103 | + }, |
| 104 | + { |
| 105 | + "cell_type": "code", |
| 106 | + "execution_count": null, |
| 107 | + "outputs": [], |
| 108 | + "source": [ |
| 109 | + "# Create a new instance of the CLDK class\n", |
| 110 | + "cldk = CLDK(language=\"java\")\n", |
| 111 | + "# Create an analysis object over the java application. Provide the application path using JAVA_APP_PATH\n", |
| 112 | + "analysis = cldk.analysis(project_path=\"JAVA_APP_PATH\", analysis_level=AnalysisLevel.symbol_table)\n", |
| 113 | + "# Go through all the classes in the application\n", |
| 114 | + "for class_name in analysis.get_classes():\n", |
| 115 | + " class_details = analysis.get_class(qualified_class_name=class_name)\n", |
| 116 | + " # Generate test cases for non-interface and non-abstract classes\n", |
| 117 | + " if not class_details.is_interface and 'abstract' not in class_details.modifiers:\n", |
| 118 | + " # Get all constructor signatures\n", |
| 119 | + " constructor_signatures = ''\n", |
| 120 | + " for method in analysis.get_methods_in_class(qualified_class_name=class_name):\n", |
| 121 | + " method_details = analysis.get_method(qualified_class_name=class_name, qualified_method_name=method)\n", |
| 122 | + " if method_details.is_constructor:\n", |
| 123 | + " constructor_signatures += method_details.signature + '\\n'\n", |
| 124 | + " # If no constructor present, then add the signature of the default constructor\n", |
| 125 | + " if constructor_signatures=='':\n", |
| 126 | + " constructor_signatures = f'public {class_name} ()'\n", |
| 127 | + " # Go through all the methods in the class\n", |
| 128 | + " for method in analysis.get_methods_in_class(qualified_class_name=class_name):\n", |
| 129 | + " # Get the method details\n", |
| 130 | + " method_details = analysis.get_method(qualified_class_name=class_name, qualified_method_name=method)\n", |
| 131 | + " # Generate test cases for non-private methods\n", |
| 132 | + " if 'private' not in method_details.modifiers and not method_details.is_constructor:\n", |
| 133 | + " # Gather all the information needed for the prompt, which are focal method body, focal method name, focal class name, constructor signature, and cyclomatic complexity\n", |
| 134 | + " prompt = format_inst(focal_method_body=method_details.code,\n", |
| 135 | + " focal_method=method,\n", |
| 136 | + " focal_class=class_name,\n", |
| 137 | + " constructor_signatures=constructor_signatures,\n", |
| 138 | + " cyclomatic_complexity=method_details.cyclomatic_complexity)\n", |
| 139 | + " # Prompt the local model on Ollama\n", |
| 140 | + " llm_output = prompt_ollama(\n", |
| 141 | + " message=prompt,\n", |
| 142 | + " model_id=\"granite-code:20b-instruct\",\n", |
| 143 | + " )\n", |
| 144 | + " \n", |
| 145 | + " # Print the instruction and LLM output\n", |
| 146 | + " print(f\"Instruction:\\n{prompt}\")\n", |
| 147 | + " print(f\"LLM Output:\\n{llm_output}\")" |
| 148 | + ], |
| 149 | + "metadata": { |
| 150 | + "collapsed": false |
| 151 | + }, |
| 152 | + "id": "65c9558e4de65a52" |
12 | 153 | } |
13 | 154 | ], |
14 | 155 | "metadata": { |
|
0 commit comments