# 如何偷懶給AI寫測試 AI prompt for writing tests in Golang ChatGPT已經問世一陣子,開始把ChatGPT融入我們日常的開發 今天會提到讓ChatGPT幫你產生測試,並用最便宜的方法去使用它,以及額外推薦一些小工具。 在進入讓AI寫測試之前,先來了解可以用哪些形式的Prompt(咒語)讓ChatGPT幫你做事 ## Prmpting principles ### Principle 1: 寫下明確的指示 Write clear and specific instructions ``` You will be provided with text delimited by triple quotes. If it contains a sequence of instructions, \ re-write those instructions in the following format: Step 1 - ... Step 2 - … … Step N - … If the text does not contain a sequence of instructions, \ then simply write \"No steps provided.\ ``` #### 少量樣本提示 "Few-shot" prompting [少量樣本提示](https://promptingguide.azurewebsites.net/techniques/fewshot)可以用作一種技術,以實現上下文學習,其中我們在提示中提供範例以引導模型實現更好的結果 ```python prompt = f""" Your task is to answer in a consistent style. <child>: Teach me about patience. <grandparent>: The river that carves the deepest \ valley flows from a modest spring; the \ grandest symphony originates from a single note; \ the most intricate tapestry begins with a solitary thread. <child>: Teach me about resilience. """ response = get_completion(prompt) print(response) ``` ### Principle 2: 讓模型去思考 Give the model time to “think”** #### Tactic 1: 明確指定步驟,去完成任務 Specify the steps required to complete a task ```python text = f""" In a charming village, siblings Jack and Jill set out on \ a quest to fetch water from a hilltop \ well. As they climbed, singing joyfully, misfortune \ struck—Jack tripped on a stone and tumbled \ down the hill, with Jill following suit. \ Though slightly battered, the pair returned home to \ comforting embraces. Despite the mishap, \ their adventurous spirits remained undimmed, and they \ continued exploring with delight. """ # example 1 prompt_1 = f""" Perform the following actions: 1 - Summarize the following text delimited by triple \ backticks with 1 sentence. 2 - Translate the summary into French. 3 - List each name in the French summary. 4 - Output a json object that contains the following \ keys: french_summary, num_names. Separate your answers with line breaks. Text: ``` """ response = get_completion(prompt_1) print("Completion for prompt 1:") print(response) ``` #### 以制定的格式輸出 Ask for output in a specified format ```python prompt_2 = f""" Your task is to perform the following actions: 1 - Summarize the following text delimited by <> with 1 sentence. 2 - Translate the summary into French. 3 - List each name in the French summary. 4 - Output a json object that contains the following keys: french_summary, num_names. Use the following format: Text: <text to summarize> Summary: <summary> Translation: <summary translation> Names: <list of names in Italian summary> Output JSON: <json with summary and num_names> Text: <{text}> """ response = get_completion(prompt_2) print("\nCompletion for prompt 2:") print(response) ``` ## ChatGPT generates test 使用上面的技巧,可以寫出template讓ChatGPT學習,並指定格式輸出 就可以在短時間內,讓ChatGPT幫我們產生立即可以使用的測試,通常都只需要做微調即可 ### Table driven tests template in Golang #### 為何要寫 Table-Driven test? 那為什麼這邊要寫table driven test? ~~因為VScode按下右鍵產生Golang test就是table driven~~ Table-Driven test的目的是擷取出function內部的模式(pattern) **好處**是之後要加上新的測試用例很方便,減少測試程式碼的重複性 例如,要測試 `sum.go` ```go package sum func Sum(a, b int) int { return a + b } ``` 傳統的測試會這樣寫(`sum_test.go`) 根據不同test case 會新增新的test function (Golang的test function要用 `Test` 作為前綴(prefix)) ```go package sum import ( "testing" ) func TestSum_One(t *testing.T) { result := Sum(1, 2) if result != 3 { t.Errorf("expected 3, but got %d", result) } } func TestSum_Two(t *testing.T) { result := Sum(3, 4) if result != 7 { t.Errorf("expected 7, but got %d", result) } } ``` Table-driven test則是可以這樣寫,把測試用例寫進slice 未來要新增用例的話,可以很方便就依序加在下面 ```go package sum import ( "testing" ) func TestSum(t *testing.T) { cases := []struct{ description string num1 int num2 int expected int }{ { description: "1 + 2", num1: 1, num2: 2, expected: 3, }, { description: "3 + 4", num1: 3, num2: 4, expected: 7, }, } for _, tt := range cases { t.Run(tt.description, func(t *testing.T){ result := Sum(tt.num1, tt.num2) if result != tt.expected { t.Errorf("expected %d, but got %d", tt.expected, result) } } } } ``` 當然這樣的寫法還是有人會質疑,例如: Q: 你的測試用例太多的話,不就列超長一串,無法見底? A: 真的太多太長的話,可以用[golden file](https://gitlab.smart-aging.tech/go/go-go-monorepo/-/merge_requests/120)的方法,把test case的input/output,都放到`.golden`檔案當中 Q: 這樣我不就要在一開始寫測試前花很多時間,去設計思考table內要放哪些參數? A: 這就是今天的主題,請ChatGPT幫我們處理的部分,節省時間 ### Table-Driven unit testing prompt for Golang 原本寫測試要多久?這部分因人而異,也要看測試程式碼的複雜程度,少則10分鐘,多至1小時 也因為是使用table driven的template,所以ChatGPT產出蠻一致的 基本上,要填寫很多input/output的測試,節省的時間蠻多的 #### 咒語呢? 咒語包含兩部分,第一個是Table-Driven 的 template template: ```go func TestConvertToFHIR(t *testing.T) { type args struct { req *CreateVitalSignsRequest references []repo_fhir.Reference } // Define the test cases testCases := []struct { name string args args want repo_fhir.Observation wantErr error }{ { name: "Empty CreateVitalSignsRequest", args: args{ req: nil, references: emptyReferences, }, want: repo_fhir.Observation{}, wantErr: fmt.Errorf(REQUEST_REQUIRED), }, { name: "With Note", args: args{ req: vitalsSignsRequestWithNoteAndID, references: emptyReferences, }, want: expectedVitalSignsWithoutMembersWithNote, wantErr: nil, }, // Add more test cases... } // Iterate through the test cases for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { // Call the function with the test case inputs give := &repo_fhir.Observation{} err := tc.args.req.ConvertToFHIR(give, tc.args.references) // Verify the output matches the expected result if tc.wantErr != nil && err.Error() != tc.wantErr.Error() { t.Errorf("Expected error to be '%v', but got '%v'", tc.wantErr, err) } if diff := cmp.Diff(*give, tc.want); diff != "" { t.Errorf("[%s] expected result (-got +want):\n%s", tc.name, diff) } }) } } ``` prompt就會是:請ChatGPT用template的形式,將input的code產生table-driven的測試 ``` template: ```go func TestConvertToFHIR(t *testing.T) { type args struct { req *CreateVitalSignsRequest references []repo_fhir.Reference } // Define the test cases testCases := []struct { name string args args want repo_fhir.Observation wantErr error }{ { name: "Empty CreateVitalSignsRequest", args: args{ req: nil, references: emptyReferences, }, want: repo_fhir.Observation{}, wantErr: fmt.Errorf(REQUEST_REQUIRED), }, { name: "With Note", args: args{ req: vitalsSignsRequestWithNoteAndID, references: emptyReferences, }, want: expectedVitalSignsWithoutMembersWithNote, wantErr: nil, }, // Add more test cases... } // Iterate through the test cases for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { // Call the function with the test case inputs give := &repo_fhir.Observation{} err := tc.args.req.ConvertToFHIR(give, tc.args.references) // Verify the output matches the expected result if tc.wantErr != nil && err.Error() != tc.wantErr.Error() { t.Errorf("Expected error to be '%v', but got '%v'", tc.wantErr, err) } if diff := cmp.Diff(*give, tc.want); diff != "" { t.Errorf("[%s] expected result (-got +want):\n%s", tc.name, diff) } }) } ``` \n Implement table driven tests with the template for the following code ``` #### 實際操作一遍 ![gen test by ChatGPT](https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExNW5jOHphNGtqNjBvZHBvNGszdTg5bnRsNDBuOGd2YjZ5MDI5ODdueSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/1cI93O7vTtESXkZLa1/giphy.gif) ### Template in Javascript/TypeScript [prompt for unit test](https://gist.github.com/kostysh/dbd1dfb2181b96563754222903bf67e7) 這個我沒用過,想用的可以試試看 但我覺得不一定要用別人定好的,可以自己要使用的template需要是什麼樣子的 ### 還有什麼用途 [example: test cases with API document](https://chat.openai.com/share/f1232bd7-795b-4c56-a1d5-67b4acf39a75) #### test case的產生? 沒問題,很簡單 ``` Write a test cases for the API contract given below https://xxx.xxx.xx/swagger.json ``` 沒公開swagger文件?直接貼上的swagger.json也可以(前提是不要太多字) ![image](https://hackmd.io/_uploads/H1r8-mjLT.png) ``` show results in the table ``` ![show results in the table ](https://hackmd.io/_uploads/HyASM7j8a.png) ``` create more coverage test cases in table ``` ![create more coverage test cases in table ](https://hackmd.io/_uploads/ryahGQoLa.png) ``` write a edge cases and security cases in table ``` ![write a edge cases and security cases in table ](https://hackmd.io/_uploads/BJ7fX7i8T.png) #### Test data的產生? 已經都用ChatGPT幫我產生test了,那為什麼還要自己寫,在那邊對格式真的很累,也可以請GPT順便幫你產一下 這句咒語也可接在我們原本產生的test咒語之後唸出 ``` provide test data ``` ![provide test data](https://hackmd.io/_uploads/BJP4Ems8T.png) ### 客家ㄉ使用ChatGPT Use ChatGPT with 1/10 of monthly billing * [Genie AI in Vscode](https://marketplace.visualstudio.com/items?itemName=genieai.chatgpt-vscode) * [api key](https://help.openai.com/en/articles/4936850-where-do-i-find-my-api-key) #### 可以做到什麼 * 把咒語存在你的VSCode設定裡,不用每次都重打 ![image](https://hackmd.io/_uploads/SJgj0XjIp.png) * 月費只要繳不到1元美金 ![image](https://hackmd.io/_uploads/ryFcpQoUT.png) * 可以選擇你要用哪個model(gpt-4肯定是比較貴的,可以用3.5就好) ![image](https://hackmd.io/_uploads/SyrWCQo8p.png) 有時用移用會秀抖,沒辦法使用,這時只要去open AI設定中把token revoke,再重新註冊就好 VSCode裡面 `Command+Shift+P` 選擇**Genie: Clear API Key** ![image](https://hackmd.io/_uploads/B1H-6XjI6.png) #### 缺點 以下需求只能乖乖付300元台幣了 * 沒辦法存下你在ChatGPT裡面的對話 * 沒辦法分享對話 ### Extra to recommend-[CodeGPT](https://github.com/appleboy/CodeGPT) * Help you to write the [commit message](https://github.com/appleboy/CodeGPT?tab=readme-ov-file#cli-mode) * [Code review](https://github.com/appleboy/CodeGPT?tab=readme-ov-file#code-review)w ## References - [ChatGPT Prompt Engineering for Developers](https://www.deeplearning.ai/short-courses/chatgpt-prompt-engineering-for-developers/) - [Prompt Engineering Guide](https://promptingguide.azurewebsites.net/)