Chapter 17 — Developing Your First Enterprise Skill: Unit Test Synthesizer
ทำไมต้องชื่อนี้และเรียนไปเพื่ออะไร?
ใน Day 16 เราสร้างเครื่องยนต์ SkillRegistry และ SkillLifecycleManager ไว้เรียบร้อยแล้ว แต่มันเป็นเพียง "เครื่องเล่นเกมที่ยังไม่มีตลับเกมเสียบ"
ปัญหาใหญ่ในโลกแห่งความเป็นจริง: หากคุณสั่งให้ AI ทั่วไปเขียน Unit Test มันจะเขียนแบบมือสมัครเล่นทันที—ไม่มีการ Mock ระบบภายนอก, ตั้งชื่อมั่ว, และเขียนทดสอบเฉพาะ Happy Path บรรทัดเดียว เมื่อรันจริงบน CI/CD ก็จะพังเพราะระบบไปแตะ Database จริง!
Day 17 เราจึงจะลงมือสร้าง Enterprise Skill ตัวจริงตัวแรก ในชื่อ unit_test_synthesizer ให้ครบเครื่องทั้ง 3 ชิ้นส่วน:
1. metadata.json: สารบัญย่อระดับ Stage 1 (~25 tokens)
2. instructions.md: กฎเหล็กระดับ Staff QA Architect (AAA Pattern, Isolated Mocking, Trio Coverage: Happy, Edge, Exception paths)
3. tools.py: Dedicated Tool สำหรับวัดเปอร์เซ็นต์ Test Coverage จริงผ่านคำสั่ง Maven โดยไม่ทำให้ Tool List หลักบวมพอง!
ใน Day 17 เราจะเติมเต็มโครงสร้างของตลับสกิล skills/unit_test_synthesizer/ ให้สมบูรณ์แบบครบทั้ง 3 ไฟล์ พร้อมสร้างสคริปต์ทดสอบ test_enterprise_skill.py:
บัตรประจำตัวสารบัญย่อสำหรับ Stage 1 ให้ Agent มองเห็นภาพรวมว่ามีทักษะนี้ในระบบ กินพื้นที่เพียง ~25 tokens
คัมภีร์กฎเกณฑ์ระดับ Enterprise คุมโมเดลให้เขียนเทส 3 มิติ (Happy, Boundary, Exception) และตัดขาด Real DB ด้วย Mock
เครื่องมือตรวจวัด Coverage เฉพาะทาง จะถูกเปิดเผยให้โมเดลมองเห็นเฉพาะตอนสกิลนี้กำลัง Active เพื่อรักษาความสะอาดของ Context
- จำลองข้อมูลนำเข้า (Mock Inputs)
- สกัดกั้น DB / Network ด้วย Mockito when(...).thenReturn(...)
- ประกาศ Expected Exception ถ้าเป็น Sad path
- สั่งรันเพียง 1 เมธอดเป้าหมายเท่านั้น
- เก็บผลลัพธ์ลงตัวแปรผลลัพธ์ (Actual Value)
- ไม่ผสมตรรกะอื่นในจังหวะนี้
- ตรวจสอบค่าผลลัพธ์ด้วย assertEquals()
- ตรวจสอบพฤติกรรม Mock ด้วย verify(mock, times(1))
- ป้องกัน Regression 100%
ทดสอบกรณีข้อมูลถูกต้อง 100% เพื่อยืนยันว่า Business Logic ทำงานตามเป้าหมาย
ทดสอบค่าว่าง (null, empty string, list ว่าง) หรือค่าขอบสุด (0, ค่าติดลบ, MAX_INT)
ทดสอบว่าเมื่อข้อมูลผิดปกติ ฟังก์ชันต้องโยน Domain Exception ที่ถูกต้อง (เช่น UserNotFoundException)
{
"id": "unit_test_synthesizer",
"name": "Enterprise Unit Test Synthesizer",
"description": "สร้าง Unit Test คุณภาพระดับ Enterprise ตามมาตรฐาน AAA Pattern, Mockito Isolation และครอบคลุมทั้ง Happy, Boundary และ Exception Paths",
"triggers": ["test", "unit test", "junit", "pytest", "test case", "mock", "coverage"],
"version": "1.0.0"
}
# Role: Senior QA Automation & Test Architect
You are operating in specialized 'Unit Test Synthesizer' mode.
Your objective is to generate bulletproof, enterprise-standard unit test suites.
## Mandatory Architectural Rules:
1. **Strict AAA Structure**: Every test method MUST be clearly partitioned into:
- `// Arrange`: Setup test doubles (Mocks), inputs, and expected outcomes.
- `// Act`: Execute the single target method under test.
- `// Assert`: Verify return values, exceptions, and verify mock invocation counts.
2. **Mocking & Isolation**: NEVER call real external databases, networks, or file systems. Mock all repositories and clients with Mockito.
3. **Naming Standard**: Test method names must strictly follow:
`should_[ExpectedBehavior]_When_[StateUnderTest]()`
Example: `should_ThrowUserNotFoundException_When_UserIdDoesNotExist()`
4. **Three Critical Paths (Mandatory Trio)**: Every tested method must have:
- Happy Path: Valid input, expected successful outcome.
- Boundary Path: Empty collections, null checks, edge values (0, MAX_INT).
- Exception Path: Verify expected domain exceptions with `assertThrows`.
5. **Behavior Verification**: Always verify mock interactions with:
`verify(repositoryMock, times(1)).findById(any());`
import subprocess
import shutil
from pathlib import Path
from typing import Dict, Any, Optional
def run_coverage_check(project_dir: str = "sample_maven_project") -> Dict[str, Any]:
"""
Dedicated Tool ประจำสกิล Unit Test Synthesizer:
สั่งรัน Maven Test เพื่อตรวจสอบผลลัพธ์และความพร้อมของชุดทดสอบ
:param project_dir: ไดเรกทอรีโปรเจกต์ที่มี pom.xml
:return: ผลการทดสอบและข้อความสรุป
"""
proj_path = Path(project_dir).resolve()
pom_file = proj_path / "pom.xml"
if not pom_file.exists():
return {
"status": "error",
"message": f"ไม่พบไฟล์ pom.xml ในโฟลเดอร์: {project_dir}"
}
mvn_bin = shutil.which("mvn")
if not mvn_bin:
return {
"status": "error",
"message": "ไม่พบคำสั่ง 'mvn' ในระบบ กรุณาตรวจสอบ PATH"
}
try:
# สั่งรันคำสั่ง mvn test ในโหมด batch
result = subprocess.run(
[mvn_bin, "test", "-B"],
cwd=str(proj_path),
capture_output=True,
text=True,
timeout=120,
encoding="utf-8",
errors="replace"
)
passed = (result.returncode == 0)
output = result.stdout or result.stderr or ""
# สกัดข้อความ 15 บรรทัดสุดท้ายของบันทึกผล
lines = [line.strip() for line in output.splitlines() if line.strip()]
summary = "\n".join(lines[-15:]) if len(lines) > 15 else "\n".join(lines)
return {
"status": "success" if passed else "failure",
"exit_code": result.returncode,
"passed": passed,
"summary": summary
}
except subprocess.TimeoutExpired:
return {"status": "error", "message": "Maven test timed out after 120s"}
except Exception as err:
return {"status": "error", "message": f"Execution failed: {str(err)}"}
import sys
from pathlib import Path
from skills.registry import SkillRegistry
from skills.lifecycle import SkillLifecycleManager
from skills.unit_test_synthesizer.tools import run_coverage_check
from core import OllamaLLMClient
def main():
print("=" * 70)
print("🚀 เริ่มทดสอบ Day 17: Developing Your First Enterprise Skill")
print("=" * 70)
# 1. โหลด SkillRegistry และตรวจสอบตลับสกิล
skills_folder = Path("./skills").resolve()
registry = SkillRegistry(skills_dir=str(skills_folder))
lifecycle = SkillLifecycleManager(registry)
skill_id = "unit_test_synthesizer"
assert registry.get_skill_info(skill_id) is not None, f"❌ ไม่พบสกิล {skill_id} ใน Registry!"
print(f"📦 1. ตรวจพบตลับสกิล '{skill_id}' ในระบบเรียบร้อย")
# 2. จำลองคำสั่งผู้ใช้ และสั่งให้ Lifecycle Manager ขยาย Instructions
user_prompt = "ช่วยสร้าง Unit Test ที่ได้มาตรฐานสำหรับ UserService คลาสจัดการผู้ใช้งาน"
matched_id = registry.find_skill_by_intent(user_prompt)
print(f"\n💬 คำสั่งผู้ใช้: \"{user_prompt}\"")
print(f"🎯 เจตนาตรงกับสกิล: {matched_id}")
assert matched_id == skill_id
# 3. สั่ง Activate สกิล
instructions = lifecycle.activate_skill(matched_id)
print("⚡ 2. ขยาย Instructions เข้าสู่ Working Memory สำเร็จ (ตัวอย่างกฎเหล็ก):")
print("-" * 50)
for line in instructions.splitlines()[:10]:
print(f" {line}")
print("-" * 50)
# 4. ทดสอบความพร้อมของโมเดล Local และสังเคราะห์โค้ดเทสตัวอย่าง
client = OllamaLLMClient(model_name="qwen2.5-coder:7b")
if not client.check_health():
print("⚠️ ไม่พบ Ollama Daemon ข้ามขั้นตอนสังเคราะห์โค้ดด้วยโมเดลสด")
else:
print("\n🧠 3. ส่งคำสั่งให้ qwen2.5-coder:7b สังเคราะห์ Unit Test ตามกฎเหล็ก...")
messages = [
{"role": "system", "content": instructions},
{"role": "user", "content": "เขียน Unit Test สำหรับฟังก์ชัน findById(Long id) ใน UserService ให้ครอบคลุม AAA Pattern"}
]
sample_test_output = client.chat(messages, temperature=0.1)
print("\n📝 ตัวอย่างโค้ดเทสที่สังเคราะห์ได้ (บางส่วน):")
print("-" * 50)
print(sample_test_output[:400] + "\n...")
print("-" * 50)
# 5. ทดสอบการเรียกใช้งาน Dedicated Tool ของสกิล
print("\n🧪 4. ทดสอบเรียกใช้ Dedicated Tool: 'run_coverage_check()'...")
tool_result = run_coverage_check(project_dir="sample_maven_project")
print(f"👉 ผลลัพธ์จาก Tool: สถานะ={tool_result['status']}")
print(f"📄 บันทึกผลลัพธ์:\n{tool_result.get('summary', tool_result.get('message'))}")
# 6. ทดสอบการ Evict ปลดคืนพื้นที่
print("\n🧹 5. งานเสร็จสิ้น สั่ง Evict สกิลออกจาก Working Memory...")
lifecycle.evict_active_skill()
assert lifecycle.active_skill_id is None
print("✅ Eviction สำเร็จ: เคลียร์ Context สะอาดเอี่ยม 100%!")
print("\n" + "=" * 70)
print("🎉 ผ่านการทดสอบ Enterprise Skill: Unit Test Synthesizer ครบ 100%!")
print("🏆 คุณมีตลับสกิลระดับอุตสาหกรรมพร้อมใช้งานจริงบน MacBook M3 แล้ว!")
print("=" * 70)
if __name__ == "__main__":
main()
ชำแหละไวยากรณ์สำคัญในตลับสกิล unit_test_synthesizer
"triggers": ["test", "unit test", "junit", "mock", ...]
Keyword-based Intent Binding: ช่วยให้ SkillRegistry.find_skill_by_intent() ตรวจพบเจตนาของผู้ใช้ได้ในระดับ Microseconds โดยไม่ต้องเสียเวลาถาม LLM ซ้ำ
should_[ExpectedBehavior]_When_[StateUnderTest]()
Living Documentation Standard: การบังคับไวยากรณ์การตั้งชื่อเทสผ่าน System Instructions ช่วยเปลี่ยนโค้ดเทสให้กลายเป็นเอกสารอธิบายระบบที่มีชีวิต อ่านปุ๊บรู้ปั๊บว่ากำลังทดสอบอะไร
subprocess.run([mvn_bin, "test", "-B"], cwd=...)
Sandboxed Process Execution: รันคำสั่งคอมไพล์ในโฟลเดอร์โปรเจกต์เป้าหมายแบบ Batch Mode เพื่อนำตัวเลขความสำเร็จและ Stack Trace กลับมาตรวจทานโค้ดที่เพิ่งสังเคราะห์
เปิด Terminal บน MacBook M3 แล้วทำตาม 5 ขั้นตอนนี้เพื่อติดตั้งตลับสกิล unit_test_synthesizer ให้สมบูรณ์แบบและสั่งรันการทดสอบทันที:
cd ~/se_agent
source .venv/bin/activate
mkdir -p skills/unit_test_synthesizer
touch skills/unit_test_synthesizer/metadata.json
touch skills/unit_test_synthesizer/instructions.md
touch skills/unit_test_synthesizer/tools.py
touch test_enterprise_skill.py
- นำโค้ดใน ส่วนที่ 4 (ไฟล์ที่ 1) ไปวางใน skills/unit_test_synthesizer/metadata.json
- นำโค้ดใน ส่วนที่ 4 (ไฟล์ที่ 2) ไปวางใน skills/unit_test_synthesizer/instructions.md
- นำโค้ดใน ส่วนที่ 4 (ไฟล์ที่ 3) ไปวางใน skills/unit_test_synthesizer/tools.py
- นำโค้ดใน ส่วนที่ 4 (ไฟล์ที่ 4) ไปวางใน test_enterprise_skill.py
python test_enterprise_skill.py
🎉 ผลลัพธ์: ระบบจะสแกนพบตลับสกิล ➔ ขยายคู่มือกฎเหล็ก AAA Pattern ➔ สั่งให้โมเดล Qwen2.5-Coder สังเคราะห์โค้ดเทสจริง ➔ รัน Dedicated Tool วัดผลผ่าน Maven ➔ และทำการ Evict ปลดคืนพื้นที่แรมบน Mac M3 ครบวงจร 100%!
ตอนนี้เรามีตลับสกิลสังเคราะห์ Unit Test ระดับ Enterprise เสียบเข้ากับสถาปัตยกรรม Microkernel แล้ว ใน Day 18 เราจะก้าวข้ามการมองโค้ดเป็นตัวอักษรดิบๆ (Raw Text) สู่การใช้ Tree-sitter AST Parsing เพื่อสกัดโครงกระดูกโค้ด ลด Token ลงได้ถึง 85% ครับ!
ข้อที่ 1: วัตถุประสงค์หลักของ Day 17: Developing Your First Enterprise Skill คืออะไร?
ข้อที่ 2: โครงสร้าง AAA Pattern ในการเขียน Unit Test ประกอบด้วย 3 ขั้นตอนใดบ้าง?
ข้อที่ 3: ทำไมคำสั่งใน `instructions.md` ของ Unit Test Synthesizer จึงต้องสั่งห้ามไม่ให้ Unit Test เรียกใช้งาน Real Database โดยเด็ดขาด?
ข้อที่ 4: การตั้งชื่อฟังก์ชันเทสตามมาตรฐาน `should_ExpectedBehavior_When_StateUnderTest` มีประโยชน์อย่างไร?
ข้อที่ 5: ในมาตรฐาน Enterprise Unit Testing "Three Critical Paths" หมายถึงการครอบคลุมเคสประเภทใดบ้าง?
ข้อที่ 6: ไฟล์ `tools.py` ที่อยู่ในโฟลเดอร์ของสกิล (Dedicated Tools) มีบทบาทต่างจาก Tools ทั่วไปใน Day 4-5 อย่างไร?
ข้อที่ 7: ฟังก์ชัน `run_coverage_check()` ใน `tools.py` ทำหน้าที่อะไรในกระบวนการทำงานของสกิลนี้?
ข้อที่ 8: เมื่อ Agent รันฟังก์ชัน `run_coverage_check()` แล้วพบว่า Coverage ได้เพียง 40% และมี 1 เทสพัง (Failed) สิ่งที่ Agent ควรทำต่อคืออะไร?
ข้อที่ 9: ข้อมูลในไฟล์ `metadata.json` ของสกิลนี้มีผลต่อการทำงานของ `SkillRegistry` อย่างไร?
ข้อที่ 10: หลังจากที่เขียน Unit Test เสร็จสิ้นและรันตรวจสอบผ่านเรียบร้อยแล้ว `SkillLifecycleManager` ควรดำเนินการสิ่งใด?
ข้อที่ 11: หากเราต้องการเพิ่ม Enterprise Skill ที่สอง เช่น `git_release_manager` ในสัปดาห์หน้า เราต้องทำอย่างไร?
ข้อที่ 12: การใช้ Mockito `verify(mock, times(1)).method()` ใน Unit Test มีจุดประสงค์เพื่ออะไร?
ข้อที่ 13: ทำไมการมี Skill Instructions ที่เฉพาะเจาะจง จึงช่วยให้ Local LLM ขนาด 7B–14B เขียนเทสได้ดีกว่าโมเดลทั่วไป?
ข้อที่ 14: หลังจากที่เราสร้างและทดสอบ Custom Skill แรกสำเร็จใน Day 17 แล้ว ใน Day 18 เราจะก้าวไปสู่เรื่องใดต่อ?
ข้อที่ 15: ในฐานะสถาปนิกผู้ออกแบบระบบ Unit Test Synthesizer Skill สำเร็จใน Day 17 คุณรู้สึกอย่างไรต่อสถาปัตยกรรมนี้?
🏁 สรุปหมุดเชื่อมโยง Day 17 (Chapter Wrap-up)
เนื้อหาใน Day 17 — Developing Your First Enterprise Skill (Unit Test Synthesizer) ได้พิสูจน์แล้วว่า การสร้าง Skill Cartridge แยกเป็นโมดูลอิสระ (Metadata + AAA Instructions + Dedicated Tools) ช่วยให้ Agent ผลิต Unit Test ที่มีคุณภาพระดับ Enterprise ได้จริง โดยคงความเร็วและความประหยัด Token สูงสุดบน MacBook M3 16GB พร้อมให้เราก้าวสู่ Day 18: Beyond Raw Text — AST Parsing with Tree-sitter ในวันถัดไปแล้วครับ!