เขียน Test Cases จาก JIRA น่าเบื่อ? ลองใช้ AI + Google Apps Script ช่วยสิ
เขียน Test Cases จาก JIRA น่าเบื่อ? ลองใช้ AI + Google Apps Script ช่วยสิ

เคยไหม…นั่งจ้อง requirement ใน JIRA แล้วต้องมานั่งไล่เขียน Test Case ทีละบรรทัด รู้สึกเหมือนโดนลงโทษมากกว่างาน QA 💀 แต่เดี๋ยวก่อน ไม่ต้องซึมอีกต่อไป! วันนี้เราจะชวนคุณมาลอง “โกงเวลา” กันหน่อย ด้วยสูตรลับ AI + JIRA + Google Script ที่จะช่วยดึง requirement มาเสิร์ฟตรงโต๊ะ แล้วปั่น Test Case ให้คุณอัตโนมัติแบบไม่ต้องปวดหัว แถมยังเร็วทันใจอีกต่างหาก 🚀
“เราสามารถให้ AI ช่วยเขียน Test Cases ได้นะ” หรือประโยคอื่นๆที่ความหมายประมาณนี้ ชาว QA อาจจะได้ยินกันบ่อยๆในช่วงที่ผ่านมา ซึ่งหลายๆคนก็ใช้ AI กันเป็นประจำอยู่แล้ว แต่จะทำยังไงให้ปลอดภัยและใกล้เคียงกับคำว่าใช้ได้จริง
*วิธีนี้เหมาะกับใคร?
- ใช้ Google Sheet ในการทำ Test Cases เป็นหลัก
- Google Account ไม่มี Feature ‘Try Gemini’ ใน Google Sheet
- Google Account ของที่ทำงานไม่ subscribe AI*
สิ่งที่ต้องเตรียม
- Google Sheet
- App Script
- Gemini
- JIRA
- Terminology/Jargon
ถ้าทุกอย่างพร้อมแล้ว ก็ลุยกันเลย
PART 1: เตรียม Gemini API Key
Steps
- ไปที่ https://aistudio.google.com/apikey
- คลิก Create API Key
- คลิก Create API Key in new project
- หลังจาก generated ให้คลิก copy เพื่อ copy ตัว API Key




โปรดตรวจสอบให้แน่ใจว่า API Key ที่คุณกำลังใช้งาน เป็นตัว Free
PART 2: เตรียม Google Sheet
Steps
- ไปที่ https://docs.google.com/spreadsheets
- คลิก New Blank Sheet
- เปิด File
- ไปที่ Extension > App Script

- Create ไฟล์ .gs
- configs.gs
- createDraft.gs
- wordcipher.gs
- wordmapper.gs
- genTC.gs

Google Sheet > Extension > App Script > + (New File) > Scripts
PART 3: เพิ่ม Function และ custom formulaให้ google sheet ด้วย Apps Script
Steps: ให้เพิ่ม code block แต่ละชุด ลงไฟล์ที่ระบุไว้
- configs.gs สำหรับจัดเก็บ API Key และ Model โดยแทนที่ API Key ที่ copy ไว้ มาแทนที่ในตัวแปร ‘GEMINI_API_KEY’
const GEMINI_API_KEY = "XXXX"; // แทนที่ด้วย API Key สร้างจาก PART1
const GEMINI_MODEL = "gemini-2.5-flash" // เลือก Model อื่นๆที่ https://ai.google.dev/gemini-api/docs/models?hl=th
- createDraft.gs สำหรับ function ในการ create draft ของ test cases จาก AI response ที่ decode มาแล้ว
function createSheetFromSelectedRawData() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getActiveSheet();
const activeRow = sheet.getActiveCell().getRow();
if (activeRow === 1) {
SpreadsheetApp.getUi().alert("Please select a row below the header.");
return;
}
// Find "Ready to Use" column
const headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
const colIndex = headers.map(h => String(h).trim().toLowerCase()).indexOf("ready to use") + 1;
if (colIndex === 0) {
SpreadsheetApp.getUi().alert("Column 'Ready to Use' not found.");
return;
}
// Get raw CSV text
let raw = sheet.getRange(activeRow, colIndex).getValue();
if (!raw || String(raw).trim() === "") {
SpreadsheetApp.getUi().alert("No CSV text found in 'Ready to Use'.");
return;
}
// Clean wrappers
raw = String(raw).replace(/^\s*```[a-zA-Z]*\s*|\s*```\s*$/g, "").trim();
// Try parsing with semicolon delimiter
let rows;
try {
rows = Utilities.parseCsv(raw, ";");
} catch (e) {
// Fallback to manual parser if Utilities.parseCsv fails
rows = robustParseCsv(raw, ";");
}
if (!rows || rows.length === 0) {
SpreadsheetApp.getUi().alert("CSV parsing failed or empty.");
return;
}
// Normalize to rectangle
const maxCols = rows.reduce((m, r) => Math.max(m, r.length), 0);
const rect = rows.map(r => {
const copy = r.slice();
while (copy.length < maxCols) copy.push("");
return copy;
});
// Sheet naming from column A
const colAValue = String(sheet.getRange(activeRow, 1).getValue() || "").trim();
const safeSuffix = sanitizeSheetName(colAValue || `Row${activeRow}`);
let baseName = `Draft_${safeSuffix}`;
let name = baseName, n = 1;
while (ss.getSheetByName(name)) {
name = `${baseName}_${n++}`;
}
const out = ss.insertSheet(name);
out.getRange(1, 1, rect.length, maxCols).setValues(rect);
out.autoResizeColumns(1, maxCols);
SpreadsheetApp.getUi().alert(`Created sheet "${name}" from row ${activeRow}.`);
}
function robustParseCsv(text, delimiter) {
const rows = [];
let current = [];
let field = "";
let inQuotes = false;
for (let i = 0; i < text.length; i++) {
const char = text[i];
if (char === '"') {
if (inQuotes && text[i + 1] === '"') {
field += '"'; // escaped quote
i++;
} else {
inQuotes = !inQuotes;
}
} else if (char === delimiter && !inQuotes) {
current.push(field);
field = "";
} else if ((char === "\n" || char === "\r") && !inQuotes) {
if (field !== "" || current.length > 0) {
current.push(field);
rows.push(current);
current = [];
field = "";
}
} else {
field += char;
}
}
if (field !== "" || current.length > 0) current.push(field);
if (current.length > 0) rows.push(current);
return rows;
}
function sanitizeSheetName(input) {
let s = String(input);
s = s.replace(/[:\\\/\?\*\[\]]/g, "-").replace(/\s+/g, " ").trim();
if (!s) s = "Untitled";
if (s.length > 80) s = s.slice(0, 80);
return s;
}
- wordcipher.gs function สำหรับเพิ่ม custom Formula ไว้สำหรับทำ substitution/encoding หรือเปลี่ยนคำเฉพาะในองค์กร เช่น ชื่อเฉพาะ, internal service, หรือ api เพื่อป้องกันข้อมูลรั่วไหล
function ENCODEWORDS(input) {
const pattern = new RegExp(Object.keys(wordMap).join('|'), 'gi');
const censor = (text) => {
if (typeof text !== 'string') return '';
return text.replace(pattern, (match) => {
const key = Object.keys(wordMap).find(k => k.toLowerCase() === match.toLowerCase());
return wordMap[key];
});
};
if (Array.isArray(input)) {
return input.map(row => [censor(row[0])]);
} else {
return censor(input);
}
}
function DECODEWORDS(input) {
const reverseMap = Object.fromEntries(
Object.entries(wordMap).map(([k, v]) => [v, k])
);
const pattern = new RegExp(Object.keys(reverseMap).join('|'), 'gi');
const decode = (text) => {
if (typeof text !== 'string') return '';
return text.replace(pattern, (match) => {
const key = Object.keys(reverseMap).find(k => k.toLowerCase() === match.toLowerCase());
return reverseMap[key];
});
};
if (Array.isArray(input)) {
return input.map(row => row.map(cell => decode(cell)));
} else {
return decode(input);
}
}
- wordmapper.gs list ของคำที่อยากให้เข้ารหัสหรือเปลี่ยนคำเฉพาะในองค์กร โดยทุกคนสามารถเพิ่มคำได้ง่ายตามตัวอย่างด้านล่าง
const wordMap = {
'คำที่ถูก': 'คำที่ต้องการเปลี่ยน เพื่อนำไปถาม AI',
'Microsoft': 'Mikrozov',
'Logistic-Service': 'Ligistoc-Service',
};
- genTC.gs
- function ที่จะไว้ผูกกับปุ่ม สำหรับให้ AI ทำการ generate test case ตาม prompt ที่กำหนดไว้ ในตัวอย่างจะได้เป็น format csv และอัด test cases เข้าไปใน cell เดียว โดย function ใน createDraft จะสร้าง sheet ใหม่ขึ้น โดยแก้ไข prompt ที่ const = prompt
function generateTestCasesFromActiveRow() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
const activeRow = sheet.getActiveCell().getRow();
if (activeRow === 1) {
SpreadsheetApp.getUi().alert("Please select a row below the header.");
return;
}
const headerRow = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
const censoredColIndex = headerRow.indexOf("Censored") + 1;
const resultColIndex = headerRow.indexOf("Raw Result") + 1;
if (censoredColIndex === 0) {
SpreadsheetApp.getUi().alert("Column 'Censored' not found.");
return;
}
let finalResultCol = resultColIndex;
if (resultColIndex === 0) {
finalResultCol = headerRow.length + 1;
sheet.getRange(1, finalResultCol).setValue("Raw Result");
}
const pmDescription = sheet.getRange(activeRow, censoredColIndex).getValue();
if (!pmDescription || pmDescription.trim() === "") {
SpreadsheetApp.getUi().alert("No PM description found in 'Censored' for this row.");
return;
}
//แก้ไข prompt ได้ที่นี่ vv
const prompt = `
You are a Senior Software Quality Assurance Engineer in a Thai E-Commerce company.
Generate detailed test cases that coverage Happy Cases and Edge Cases from the following PM description:
${pmDescription}
OUTPUT RULES (very important):
- Return ONLY raw CSV text (no markdown fences, no prose).
- Use a semicolon ';' as the delimiter.
- Include the header row.
- Enclose EVERY field in double quotes.
- If a field contains a double quote, escape it by doubling it ( " -> "" ).
- Newlines inside "Test Steps" are allowed; keep them within the quoted field.
- Do not include extra blank lines.
Columns (exact order):
TC. No.;Features;Test Case / Scenario (Unique);Test description;Pre-requisite;Test data;Test Steps;Expected result
Example (format pattern only):
"1";"Login";"Valid login with correct credentials";"Verify login works with valid user";"User must be registered";"username: testuser; password: pass123";"1. Go to login page
2. Enter valid username and password
3. Click login";"User is redirected to dashboard"
Now generate the CSV:
`;
//แก้ไข prompt ได้ที่นี่ ^^
const apiUrl = "https://generativelanguage.googleapis.com/v1beta/models/" + GEMINI_MODEL + ":generateContent?key=" + GEMINI_API_KEY;
const requestBody = {
"contents": [
{
"parts": [
{ "text": prompt }
]
}
]
};
const options = {
"method": "post",
"contentType": "application/json",
"payload": JSON.stringify(requestBody),
"muteHttpExceptions": true
};
try {
const response = UrlFetchApp.fetch(apiUrl, options);
const json = JSON.parse(response.getContentText());
let resultText = json?.candidates?.[0]?.content?.parts?.[0]?.text ?? "";
if (resultText.includes('","') && !resultText.includes('";"')) {
resultText = resultText.replace(/","/g, '";"');
}
// Remove accidental markdown code fences if any
resultText = resultText.replace(/^\s*```[a-zA-Z]*\s*|\s*```\s*$/g, "");
if (!resultText.trim()) resultText = '"";"";"";"";"";"";"";""';
sheet.getRange(activeRow, finalResultCol).setValue(resultText);
SpreadsheetApp.getUi().alert("Semicolon-delimited CSV saved to 'Raw Result' for row " + activeRow);
} catch (error) {
Logger.log("Error calling Gemini API: " + error);
SpreadsheetApp.getUi().alert("Error calling Gemini API: " + error.message);
}
}
PART 4: ดึง requirement มาจาก JIRA เข้าสู่ Google Sheet
- ในไฟล์ google sheet จาก PART 2 ให้ new sheet ใหม่ขึ้นมา ตั้งชื่ออะไรก็ได้ ส่วนผมจะตั้งว่า ‘GenerateTC’ 2. เริ่มใช้งาน ‘Jira Cloud for Sheets’ เพื่อข้อมูลจาก ticket มาไว้ที่ Google Sheet
- ไปที่ ‘Extensions’ > Jira Cloud for Sheets > Open…
- Login JIRA ให้เรียบร้อย

- เลือก ‘Get issues from Jira’

- เลือก sheet ปลายทางให้ถูกต้อง โดยอิงชื่อจาก PART4: Step 1

- Import ด้วย JQL ทุกคนสามารถปรับได้เป็นแบบที่ตัวเองชอบได้เลยครับ ส่วนตัวอย่างของผมเองจะเน้นเฉพาะ ticket ของ ตัวเอง ที่เป็น TO DO และมี label ของผมเองเท่านั้น

project = XXX
AND assignee = currentUser()
AND status = TO DO
AND labels = qa_ai_gen_tc
- คลิก ‘FIELDS’ โดยเลือก field ต่างๆที่อยากจะทำเป็น column ในชีทนี้
Field ที่สำคัญและอยากให้มี
- Key = JIRA Ticket
- Summary = Ticket Name
- Description = ข้อมูล requirement ที่ reporter เขียนไว้
- Status = Ticket Status i.e. ‘TO DO’ Prefer To Have เพื่อง่ายต่อการ filter ticket ด้วย JQL
- Priority: ใส่ไว้เพื่อที่เราจะได้ลำดับงานได้
- Sprint: เอาไว้แยกงานตาม sprint ที่ผ่านการ planing มาแล้ว
- Label: เอาไว้แยก ticket ที่เราต้องการสร้างด้วย AI โดยผมจะตั้งชื่อ label สักอันที่เอาไว้เพื่อให้ง่ายตอนใช้ JQL เช่น ‘qa_tc_ai_gen’

- คลิก ‘GET ISSUES NOW’ หลังจากนั้น column และรายละเอียดใน ticket ออกมาหน้าตาแบบรูปของด้านล่าง *หมายเหตุรูปด้านล่างเป็น mock data

PART 5: สับขาหลอก AI และเพิ่มปุ่มสร้าง Test cases

ก่อนที่เราจะโยนข้อมูลให้ AI เราควรระมัดวังเรื่องข้อมูลต่างๆ เราควรโยกหลอกมันซะหน่อย “แบบดิจิตอล ดิจิตอล ไฮเทค”
- เพิ่ม 3 Column ใหม่ ต่อท้าย ‘label’
- ‘Censored’ — เอาเปลี่ยนคำจาก column ‘Description’ โดยอิงจาก wordmapper.gs
- ‘Raw Result’ — เอาไว้รองรับ Test Cases จาก AI
- ‘Ready to Use’ — เอาไว้เปลี่ยนที่หลอก AI กลับมาเป็นคำปกติ
- เพิ่มสูตรใน cell ของ ‘Censored’ โดยอิง data จาก ‘Descripion’
=ARRAYFORMULA(ENCODEWORDS(C2:C))
- เพิ่มสูตรใน cell ของ ‘Ready To Use’ โดยอิง data จาก ‘Raw Result’
=ARRAYFORMULA(DECODEWORDS(J2:C))

- วาดปุ่ม และ assign function ไปที่ปุ่ม
- ไปที่ Insert > Drawing > วาดปุ่มเรียบร้อย > คลิก ‘Save and close’

คลิกขวา ที่ปุ่มที่วาดไว้ > Kebab Menu > Assign Script > OK

โดยให้ใส่เป็นชื่อ function ลงไป ทั้ง 2 ปุ่ม
- ปุ่มที่ให้ AI generate เทสเคส
generateTestCasesFromActiveRow
- ปุ่มที่สั่งให้ create draft template ตารางตาม csv ที่ได้จาก AI response โดยอิงจาก column ‘Ready To Use’ (ผ่านการ decode คำกลับมาเป็นคำที่เรารู้จักแล้ว)
createSheetFromSelectedRawData

หน้าตาของตาราง ที่พร้อมสร้าง Test Cases จาก AI
PART 6: เริ่มให้ AI คิด Test Cases และเราจะสร้างให้พร้อมใช้งาน
มาถึง Part สุดท้าย ที่เราจะได้ใช้งาน AI สร้าง test ให้สักที
- ให้เพื่อนคลิกที่ column ‘Censored’ ในแถวที่เราจะสร้าง Test case แบบรูปด้านล่าง

- คลิก cell อะไรก็ได้ที่อยู่แถวที่เราต้องการให้ AI สร้าง test cases และคลิกที่ปุ่ม Generate Test Cases ที่เราผูก Function ไว้ (ตัวอย่าง ปุ่มสีส้ม) (สำหรับครั้งแรก ถ้ามีการถามถึง authorize/access ของ scriptsให้คลิก ‘Allow’ แล้วคลิกปุ่มใหม่อีกครั้ง)

รอ script run ไปสักครู่ โดยเบื้องหลัง function นี้จะไปยิง API ไปถาม Gemini ตาม prompt ที่เรากำหนด
เมื่อเสร็จแล้วจะได้ Pop up แบบด้านล่าง โดยจะแจ้งแถวที่ response จาก API ไปแปะ (เช่น ในรูปจะอยู่ที่ row 3) ให้เราคลิกปุ่ม OK

เมื่อคลิก OK
- ‘Raw Response’: AI Response จะถูกแปะลง
- ‘Ready to Use’: จะใช้สูตรอิงข้อมูล แล้วถอดคำกลับไปเป็นคำที่เราใช้ในองค์กร

- คลิก cell อะไรก็ได้ที่อยู่แถวที่เราต้องการใช้สร้าง draft จากนั้นคลิกปุ่ม ‘Create Draft’ (ปุ่มม่วง) เพื่อสร้าง Test Cases แบบตารางที่เรากำหนดไว้ (กำหนดไว้ใน prompt ตอน generate AI)

เมื่อสร้างเสร็จ จะมี pop up แจ้งเตือนชื่อชีทโดยอิงจากเลข ticket ให้กด OK ปิด pop up ไป

เมื่อเราเปิด sheet Draft_{{ticket no.}} ที่สร้างมา จะหน้าตาเป็นตารางแบบด้านล่าง โดยสามารถ copy ไปแปะที่ sheet template ที่ตกแต่งรอไว้ หรือจะใช้ sheet นี้และตกแต่งเพิ่ม option ลงไปก็ได้เช่นกัน

🎉 จบแล้ว ขอบคุณที่อดทนกันมาหลายนาทีครับ 🎉 หวังว่าบทความนี้ จะสามารถเอาไปต่อยอด หรือแก้ไข ปรับปรุง script ให้เหมาะกับงานทุกคนได้ครับ
สรุปหลังจากลองใช้งานมาระยะนึง:
- AI ช่วยลดเวลา?— การใช้ Gemini + Google Sheet + JIRA ทำให้เราสามารถสร้าง Test Case ได้เร็วขึ้น ครอบคลุม scenario มากขึ้น รวมถึงมี edge case ติดมาด้วย แต่ยังไงแล้วต้องตรวจสอบทุกครั้ง เพราะ AI ยังไม่เข้าใจ context ทางธุรกิจทั้งหมด
- Prompt ยืดหยุ่น — แม้ตัวอย่างจะใช้ CSV เพื่อความสะดวกในการนำไปใช้กับ Excel/Numbers/Google Sheet แต่จริง ๆ แล้วสามารถปรับเป็น JSON หรือรูปแบบอื่นที่ทีมถนัดได้
- เลือกเครื่องมือที่เหมาะสม — Gemini ตอบโจทย์ทั้งด้านต้นทุนและการเชื่อมกับ Google Sheet ได้ง่าย ทำให้ workflow ไม่ซับซ้อน แต่ถ้าม subscribe AI อื่นๆอยู่ แนะนำให้ลองใช้ตัวอื่นที่เหมาะสมได้ครับ
จงใช้ AI อย่างมีสติ อย่าพึ่งพา 100% ควรใช้ AI เป็น “ผู้ช่วย” แล้วตัวเราเอง ต้องตรวจสอบ/ปรับแก้เพิ่มเติม เพื่อให้ได้ Test Case ที่ตรงกับระบบจริงมากที่สุด
메타데이터
- post_id
- 320ef5fb8ffd
- slug
- เขียน-test-case-น่าเบื่อ-ให้-ai-jira-google-script-มาช่วยปั่นให้แทน-320ef5fb8ffd
- url
- https://medium.com/@mytanakrit/%E0%B9%80%E0%B8%82%E0%B8%B5%E0%B8%A2%E0%B8%99-test-case-%E0%B8%99%E0%B9%88%E0%B8%B2%E0%B9%80%E0%B8%9A%E0%B8%B7%E0%B9%88%E0%B8%AD-%E0%B9%83%E0%B8%AB%E0%B9%89-ai-jira-google-script-%E0%B8%A1%E0%B8%B2%E0%B8%8A%E0%B9%88%E0%B8%A7%E0%B8%A2%E0%B8%9B%E0%B8%B1%E0%B9%88%E0%B8%99%E0%B9%83%E0%B8%AB%E0%B9%89%E0%B9%81%E0%B8%97%E0%B8%99-320ef5fb8ffd
- canonical_url
- https://medium.com/@mytanakrit/%E0%B9%80%E0%B8%82%E0%B8%B5%E0%B8%A2%E0%B8%99-test-case-%E0%B8%99%E0%B9%88%E0%B8%B2%E0%B9%80%E0%B8%9A%E0%B8%B7%E0%B9%88%E0%B8%AD-%E0%B9%83%E0%B8%AB%E0%B9%89-ai-jira-google-script-%E0%B8%A1%E0%B8%B2%E0%B8%8A%E0%B9%88%E0%B8%A7%E0%B8%A2%E0%B8%9B%E0%B8%B1%E0%B9%88%E0%B8%99%E0%B9%83%E0%B8%AB%E0%B9%89%E0%B9%81%E0%B8%97%E0%B8%99-320ef5fb8ffd
- author_url
- https://medium.com/@mytanakrit
- status
- ok
- fetched_at
- 2026-08-10 11:00:44