Skip to content

Buổi 02: Prompting Cho QA — Nói Chuyện Với AI Như Senior Dev

Mục tiêu buổi học: Học cách viết prompt hiệu quả cho AI, áp dụng RARV Loop, và thực hành viết unit test + API test cho TaskFlow app bằng cách prompt AI.


📚 Phần 1: Lý Thuyết (35 phút)

1.1 Tại Sao Prompting Là Skill Quan Trọng

Trong AI-Native QA, prompt quality = test quality = code quality.

Nếu prompt bạn gửi cho AI:

  • ❌ Mơ hồ → AI generate test không cover edge case
  • ❌ Thiếu context → AI generate code không match requirement
  • ❌ Không có output format → AI trả lời random format

Nhưng nếu prompt bạn:

  • ✅ Rõ ràng, cụ thể → AI generate test coverage 95%+
  • ✅ Có context đầy đủ → Code match requirement 100%
  • ✅ Có output format → Output dùng ngay được

Thực tế: QA engineers giỏi prompting tiết kiệm 40% thời gian so với người không biết prompting.

1.2 RARV Loop — Vòng Lặp Lý Tưởng

RARV = Reason → Act → Reflect → Verify

Reason (Suy nghĩ):

  • Trước khi prompt AI, hãy suy nghĩ: "Mình cần gì từ AI?"
  • Cái gì đã có? Cái gì còn thiếu?
  • Output nên như thế nào?

Act (Hành động):

  • Viết prompt chi tiết (không chỉ 1-2 dòng)
  • Gửi đến AI
  • Nhận output

Reflect (Suy ngẫm):

  • Đọc output từ AI
  • So sánh với expectation
  • Có gì chưa đúng? Gì không match requirement?

Verify (Xác nhận):

  • Test output bằng cách chạy/kiểm tra
  • Nếu đúng → sử dụng, deploy
  • Nếu sai → Back to "Reason", refine prompt lần 2
┌─────────────────────────────────────┐
│ 1. REASON: Cần gì?                  │
│ 2. ACT: Prompt AI                   │
│ 3. REFLECT: Output có đúng không?   │
│ 4. VERIFY: Test output              │
└─────────────────────────────────────┘

      Sai? ↓ Đúng?
          ↓ ✓
       Refine Prompt

    (Loop lại bước 1)

1.3 5 Prompt Templates Cho QA

Dưới đây là 5 template prompt mà QA engineer thường dùng. Mỗi template có structure cụ thể.

Template 1: Viết Unit Test (Test-First)

Purpose: Viết test TRƯỚC code, để test FAIL trước (theo TDD Iron Law)

Structure:

[Context]
Tôi đang làm QA cho feature "Tạo Task Mới" trong TaskFlow app.

[Requirement]
- User nhập title, description
- App tạo task mới và lưu vào database
- Trả về task object với id, created_at, updated_at

[Edge Cases Cần Test]
1. Title là empty string → lỗi "Title is required"
2. Title là null → lỗi "Title is required"
3. Title > 255 characters → lỗi "Title too long"
4. Description là null/empty → OK (optional field)
5. Valid title + description → task được tạo thành công

[Output Format]
Viết unit test dùng Vitest framework. Test phải FAIL trước khi có implementation.

```javascript
import { describe, it, expect } from 'vitest';
import { createTask } from './taskModel.js';

describe('createTask()', () => {
  // Test cases ở đây
});

[Requirements Khác]

  • Mỗi test tên rõ ràng, mô tả chính xác behavior
  • Không mock database (sử dụng in-memory hoặc real database)
  • Expect clear error messages

**Ví dụ Prompt Thực Tế:**

Tôi là QA engineer cho TaskFlow app. Tôi cần viết unit test cho function createTask(title, description).

Requirement:

  • createTask nhận 2 parameter: title (string) và description (optional string)
  • Nếu title empty hoặc null → throw error 'Title is required'
  • Nếu title > 255 chars → throw error 'Title too long'
  • Nếu valid → return object

Viết unit test dùng Vitest. Test phải FAIL ngay (vì code chưa có implementation).

Test theo Arrange-Act-Assert pattern. Tên test phải rõ ràng.

Output format: code block javascript dùng ngay được.


#### Template 2: Tìm Edge Case (Brainstorming)

**Purpose:** AI giúp phát hiện edge case mà con người dễ bỏ sót

**Structure:**

[Feature] Feature: Tạo Task Mới Input: title, description Output: task object (id, title, description, status='pending', created_at, updated_at)

[Current Test Cases]

  1. Valid title + description
  2. Valid title, empty description

[Ask AI] Tìm thêm 10 edge case khác. Cho mỗi case, viết:

  • Input
  • Expected Output
  • Tại sao nó quan trọng

[Output Format]

#InputExpected OutputWhy Important
1.........

**Ví dụ Prompt Thực Tế:**

Feature: createTask(title, description) trong TaskFlow

Current test cases:

  1. Valid title, valid description
  2. Valid title, empty description

Tìm 10 edge case khác. Mỗi case format:

  • Input: [specific value]
  • Expected: [behavior]
  • Why: [1-2 dòng]

Ưu tiên: security, data validation, edge case dễ bỏ sót.


#### Template 3: Viết API Test (Integration Test)

**Purpose:** Test API endpoint, không chỉ function

**Structure:**

[API Endpoint] POST /api/tasks Body: { title: string, description?: string } Response:

[Test Cases Cần]

  1. Happy path: valid title + description → 201, task object
  2. Missing title → 400, error "Title is required"
  3. Title too long → 400, error "Title too long"
  4. Description is null → 201, description stored as empty string

[Output Format] Dùng Jest/Vitest, dùng supertest library để test HTTP request.

javascript
import request from 'supertest';
import app from '../src/app.js';

describe('POST /api/tasks', () => {
  // Test cases ở đây
});

[Note]

  • Before each test: clear database
  • After each test: close database connection

**Ví dụ Prompt Thực Tế:**

API endpoint: POST /api/tasks Body: { title, description } Response: { success: true, task } hoặc

Test cases:

  1. Valid input → status 201, task object returned
  2. Empty title → status 400, error "Title is required"
  3. Title > 255 → status 400, error "Title too long"
  4. No title field → status 400, error "Title is required"
  5. Database connection error → status 500, error message

Viết test dùng Vitest + supertest. Mỗi test:

  • Setup: clear DB
  • Act: HTTP request
  • Assert: status + response body

#### Template 4: Tạo Bug Report (From Test Failure)

**Purpose:** Khi test FAIL, AI generate bug report tự động

**Structure:**

[Test Failure Info] Test Name: rejects title > 255 chars Expected: throw error 'Title too long' Actual: no error thrown, task created anyway Error: none

[Code Being Tested]

javascript
function createTask(title, description) {
  return { id: '123', title, description, created_at: new Date() };
}

[Generate Bug Report With]

  • Title: [brief, actionable]
  • Severity: [Critical/High/Medium/Low]
  • Steps to Reproduce
  • Expected Result
  • Actual Result
  • Root Cause Analysis
  • Suggested Fix

[Format] Markdown format, dùng ngay để submit lên Jira


**Ví dụ Prompt Thực Tế:**

Test failure: Test: 'rejects title > 255 chars' Expected: error 'Title too long' Actual: task created with long title (500+ chars)

Function:

javascript
function createTask(title, description) {
  return { id: random(), title, description, created_at: new Date() };
}

Create bug report:

  • Title (brief, specific)
  • Severity (Critical/High/Medium)
  • Reproduce steps
  • Expected vs Actual
  • Impact analysis
  • Suggested fix code

Format: markdown, production-ready.


#### Template 5: Review Code (Quality Gate)

**Purpose:** AI review code trước deploy

**Structure:**

[Code to Review]

javascript
function createTask(title, description) {
  if (!title || title.length === 0) throw new Error('Title is required');
  if (title.length > 255) throw new Error('Title too long');
  return { 
    id: Math.random().toString(36).substr(2, 9),
    title, 
    description,
    created_at: new Date() 
  };
}

[Review Against]

  • CodyMaster 7-Point Checklist (clean code)
  • SOLID principles
  • Security best practices
  • Performance
  • Testability

[Output Format]

## ✅ PASS / ❌ FAIL

### 1. Code Clarity [✅/❌]
- Comment: [analysis]

### 2. Error Handling [✅/❌]
- Comment: [analysis]

### 3. Security [✅/❌]
- Comment: [analysis]

... (7 points total)

### Issues Found
1. [Issue]
2. [Issue]

### Suggested Improvements
```javascript
// Better code

**Ví dụ Prompt Thực Tế:**

Review this createTask() function:

javascript
function createTask(title, description) {
  if (!title || title.length === 0) throw new Error('Title is required');
  if (title.length > 255) throw new Error('Title too long');
  
  return { 
    id: Math.random().toString(36).substr(2, 9),
    title, 
    description,
    created_at: new Date() 
  };
}

Check against:

  1. Code clarity & readability
  2. Error handling
  3. Input validation
  4. Performance (e.g., random ID generation)
  5. Testability
  6. Security
  7. Edge cases

Output:

  • Pass/Fail for each point
  • Issues found
  • Improved code if needed

### 1.4 Prompt Anti-Patterns (Avoid These)

❌ **Bad Prompt 1: Too Vague**

"Write a test for createTask"

→ AI không biết test cái gì, output sẽ generic, không useful

✅ **Better:**

"Write unit test cho createTask(title, description) function. Test edge case: empty title, null title, title > 255 chars. Format: Vitest, Arrange-Act-Assert pattern."


---

❌ **Bad Prompt 2: No Context**

"Generate API test"

→ AI không biết API là gì, body format, response format

✅ **Better:**

"API endpoint: POST /api/tasks Body: { title: string, description?: string } Response: { success: bool, task: object } or

Test 3 cases: valid input, missing title, title too long. Use Vitest + supertest."


---

❌ **Bad Prompt 3: No Output Format**

"Create bug report for this test failure"

→ Output format không consistent, hard to parse

✅ **Better:**

"Create bug report, markdown format:

  • Title
  • Severity
  • Steps
  • Expected
  • Actual
  • Root Cause
  • Suggested Fix"

---

## 🛠️ Phần 2: Thực Hành (50 phút)

### 2.1 Bài Tập 1: Prompt AI để Tìm Edge Case (15 phút)

**Scenario:** Bạn làm QA cho function createTask(). Dùng RARV loop để tìm edge case.

**Bước 1: REASON (2 phút)**
- Cần: Danh sách 10 edge case cho createTask()
- Context: Function nhận title (string) + description (optional string)
- Output format: Table gồm Input, Expected, Why

**Bước 2: ACT (3 phút)**

Gửi prompt này cho AI (ChatGPT, Claude, hoặc tool khác):

Feature: createTask(title, description) trong TaskFlow QA app

Current implementation:

  • Input: title (required), description (optional)
  • Output:
  • Validation: title not empty, title <= 255 chars

Current test cases (happy path):

  1. createTask('Buy milk', 'Go to store')
  2. createTask('Call mom', '')

Task: Tìm 10 edge case khác. Mỗi case gồm:

  • Input value (cụ thể)
  • Expected behavior
  • Why it's important (1 dòng)

Output format: Markdown table, có thể copy-paste ngay.

#InputExpectedWhy
1.........

Focus: data validation, security, performance, database constraints.


**Bước 3: REFLECT (5 phút)**

AI sẽ trả về danh sách như:

| # | Input | Expected | Why |
|---|---|---|---|
| 1 | title = "  " (spaces only) | Reject, "Title is required" | Trimming check |
| 2 | title = null | Reject, "Title is required" | Null check |
| 3 | title = undefined | Reject, "Title is required" | Undefined check |
| 4 | title = 256 chars | Reject, "Title too long" | Boundary test |
| 5 | title with SQL injection: `"; DROP TABLE tasks;--` | Sanitized, no SQL executed | Security |
| 6 | description = very long (1MB text) | Stored as-is (or truncated) | Performance |
| 7 | Concurrent calls same title | Unique ID for each | Race condition |
| 8 | title with emoji: "📋 Task" | Accepted (UTF-8) | Character encoding |
| 9 | title = "" (empty string) | Reject | Edge case |
| 10 | Database down during insert | Throw error, don't silently fail | Error handling |

**Bước 4: VERIFY (5 phút)**

Chọn 3 edge case và kiểm tra code current có handle không:

```javascript
// Test: spaces only
createTask('   ', 'desc')
// Current code:
// if (!title || title.length === 0) → PASS (title.length = 3, không fail!)
// ❌ BUG: Không trim trước check

// Test: SQL injection
createTask('"; DROP TABLE tasks;--', 'desc')
// Không sanitize input
// ❌ BUG: Vulnerable to SQL injection

// Test: Concurrent calls
Promise.all([
  createTask('Same title', 'desc1'),
  createTask('Same title', 'desc2')
])
// Should create 2 different tasks dengan 2 different IDs
// ✅ OK (ID random, unlikely collision)

2.2 Bài Tập 2: Prompt AI để Viết Unit Test (TDD) (20 phút)

Scenario: Viết unit test cho createTask(), test phải FAIL trước (TDD).

Bước 1: REASON (2 phút)

Cần: Unit test cho createTask(), test phải FAIL ngay (vì code chưa có validation)

Bước 2: ACT (8 phút)

Prompt AI:

I'm a QA engineer practicing TDD for createTask() function.

Current state:
- Function signature: createTask(title, description)
- Database: SQLite, table "tasks"
- Framework: Vitest
- NO implementation yet (just empty stub)

Requirements:
1. Validate title: not empty, not null, <= 255 chars
2. Description optional (default to '')
3. Return: { id, title, description, status: 'pending', created_at, updated_at }
4. Throw error for invalid input

Write unit tests using Vitest. IMPORTANT: Tests must FAIL right now (because implementation doesn't validate yet).

Tests to write:
- rejects empty title
- rejects null title
- rejects title > 255 chars
- accepts valid title + description
- accepts valid title without description

Format: Complete Vitest code block, ready to run.

```javascript
import { describe, it, expect } from 'vitest';
import { createTask } from './taskModel.js';

describe('createTask()', () => {
  // Your tests here
});

**Bước 3: REFLECT (5 phút)**

AI trả về test code như:

```javascript
import { describe, it, expect } from 'vitest';
import { createTask } from './taskModel.js';

describe('createTask()', () => {
  it('should reject empty title', () => {
    expect(() => createTask('', 'description')).toThrow('Title is required');
  });

  it('should reject null title', () => {
    expect(() => createTask(null, 'description')).toThrow('Title is required');
  });

  it('should reject title > 255 chars', () => {
    const longTitle = 'a'.repeat(256);
    expect(() => createTask(longTitle, 'desc')).toThrow('Title too long');
  });

  it('should accept valid title and description', () => {
    const task = createTask('Buy milk', 'Go to store');
    expect(task.id).toBeDefined();
    expect(task.title).toBe('Buy milk');
    expect(task.description).toBe('Go to store');
    expect(task.status).toBe('pending');
    expect(task.created_at).toBeInstanceOf(Date);
  });

  it('should accept title without description', () => {
    const task = createTask('Call mom');
    expect(task.description).toBe('');
  });
});

Bước 4: VERIFY (5 phút)

Chạy test để verify nó FAIL:

bash
# Tạo stub function
echo "export function createTask(title, description) {
  return { id: '123', title, description, status: 'pending', created_at: new Date() };
}" > src/taskModel.js

# Chạy test
npm test

# Output sẽ FAIL:
# ✗ should reject empty title
#   Expected to throw [Title is required]
#   but function returned: { id: '123', title: '', ... }
#
# ✗ should reject null title
#   ...
#
# ✓ should accept valid title (this passes because stub just returns object)

# Điều này ĐÚNG! Test FAIL = chúng ta sẵn sàng implement.

2.3 Bài Tập 3: Prompt AI để Viết API Test (15 phút)

Scenario: Viết API test cho POST /api/tasks endpoint.

Bước 1: REASON (2 phút)

Cần: API test, cover 5 scenarios: valid input, missing title, title too long, DB error, ...

Bước 2: ACT (6 phút)

Prompt AI:

I need to write API test for POST /api/tasks endpoint.

API Details:
- Endpoint: POST /api/tasks
- Body: { title: string, description?: string }
- Response on success: { success: true, task: { id, title, description, status, created_at, updated_at } }
- Response on error: { success: false, error: "error message" }
- HTTP Status: 201 (created) or 400 (validation error) or 500 (server error)

Backend: Express.js, SQLite database

Test cases needed:
1. Valid title + description → 201, task object
2. Valid title, no description → 201, description empty
3. Missing title field → 400, "Title is required"
4. Empty title → 400, "Title is required"
5. Title > 255 chars → 400, "Title too long"

Write test using Vitest + supertest library.

Format: Complete code block, ready to npm test.

```javascript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import request from 'supertest';
import app from '../src/app.js';
import Database from 'better-sqlite3';

describe('POST /api/tasks', () => {
  let db;

  beforeEach(() => {
    // Setup: create fresh database
  });

  afterEach(() => {
    // Cleanup: close database
  });

  // Your tests here
});

**Bước 3: REFLECT (4 phút)**

AI trả về test code:

```javascript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import request from 'supertest';
import app from '../src/app.js';
import Database from 'better-sqlite3';
import path from 'path';

describe('POST /api/tasks', () => {
  let db;

  beforeEach(() => {
    // Create in-memory database for testing
    db = new Database(':memory:');
    db.exec(`
      CREATE TABLE tasks (
        id TEXT PRIMARY KEY,
        title TEXT NOT NULL,
        description TEXT,
        status TEXT DEFAULT 'pending',
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP
      )
    `);
  });

  afterEach(() => {
    db.close();
  });

  it('should create task with valid title and description', async () => {
    const response = await request(app)
      .post('/api/tasks')
      .send({ title: 'Buy milk', description: 'Go to store' })
      .expect(201);

    expect(response.body.success).toBe(true);
    expect(response.body.task.id).toBeDefined();
    expect(response.body.task.title).toBe('Buy milk');
    expect(response.body.task.description).toBe('Go to store');
    expect(response.body.task.status).toBe('pending');
  });

  it('should create task with title only', async () => {
    const response = await request(app)
      .post('/api/tasks')
      .send({ title: 'Call mom' })
      .expect(201);

    expect(response.body.success).toBe(true);
    expect(response.body.task.description).toBe('');
  });

  it('should reject missing title', async () => {
    const response = await request(app)
      .post('/api/tasks')
      .send({ description: 'No title' })
      .expect(400);

    expect(response.body.success).toBe(false);
    expect(response.body.error).toBe('Title is required');
  });

  it('should reject empty title', async () => {
    const response = await request(app)
      .post('/api/tasks')
      .send({ title: '', description: 'Empty title' })
      .expect(400);

    expect(response.body.success).toBe(false);
    expect(response.body.error).toBe('Title is required');
  });

  it('should reject title > 255 chars', async () => {
    const longTitle = 'a'.repeat(256);
    const response = await request(app)
      .post('/api/tasks')
      .send({ title: longTitle })
      .expect(400);

    expect(response.body.success).toBe(false);
    expect(response.body.error).toBe('Title too long');
  });
});

Bước 4: VERIFY (3 phút)

Copy code, install dependencies, run:

bash
npm install supertest
npm test -- api.test.js

# Output:
# ✓ should create task with valid title and description
# ✓ should create task with title only
# ✓ should reject missing title
# ✓ should reject empty title
# ✓ should reject title > 255 chars
#
# 5 passed

📝 Phần 3: Bài Tập Về Nhà

Task 1: Viết API Test cho GET /api/tasks (20 phút)

Dùng RARV loop để:

  1. REASON: Định scope test cases cho GET /api/tasks endpoint
  2. ACT: Prompt AI để generate test code (no parameters, should return all tasks)
  3. REFLECT: Đọc output AI
  4. VERIFY: Chạy test, đảm bảo PASS

Test cases cần cover:

  • Empty database → return empty array
  • Database has 3 tasks → return 3 tasks
  • Tasks ordered by created_at DESC → verify order

Gợi ý prompt:

Write Vitest + supertest test for GET /api/tasks endpoint.

Endpoint: GET /api/tasks
Response: { success: true, tasks: [...] }

Test cases:
1. Empty database → []
2. Has 3 tasks → return all 3, ordered by created_at DESC
3. Verify task fields: id, title, description, status, created_at

Format: Complete Vitest code, ready to run.

Task 2: Tìm 5 Edge Case Cho updateTask() (15 phút)

Function: updateTask(id, { title?, description?, status? })

Prompt AI để tìm 5 edge case. Output format: Table gồm Input, Expected, Why.

Gợi ý:

  • Không tìm task với id đó
  • Update title → cũng validate empty/length
  • Update status → chỉ allow 'pending' | 'completed'
  • Update non-existent field
  • Concurrent updates

Task 3: Review createTask() Code (10 phút)

Prompt AI để review function createTask() code bạn viết.

javascript
function createTask(title, description) {
  if (!title || title.length === 0) throw new Error('Title is required');
  if (title.length > 255) throw new Error('Title too long');
  
  return { 
    id: Math.random().toString(36).substr(2, 9),
    title, 
    description,
    created_at: new Date() 
  };
}

Prompt:

Code review against:
1. Clarity
2. Error handling
3. Input validation
4. Performance
5. Security
6. Testability
7. Best practices

Output:
- Pass/Fail for each point
- Issues found
- Improvements

✅ Phần 4: Kiểm Tra Kiến Thức

Quiz (20 phút)

Câu 1: RARV Loop là gì?

  • A) Route → API → Request → Verify
  • B) Reason → Act → Reflect → Verify
  • C) Read → Analyze → Review → Validate
  • D) Return → Append → Render → View
  • Đáp án đúng: B

Câu 2: Tại sao prompt AI cần có output format?

  • A) Để AI bớt output
  • B) Để output consistent, dễ parse
  • C) Để server không overload
  • D) Không cần thiết
  • Đáp án đúng: B

Câu 3: Khi viết unit test, test FAIL là tốt hay xấu (trong TDD)?

  • A) Xấu, test phải PASS
  • B) Tốt, FAIL = implementation chưa có
  • C) Không quan trọng
  • D) Tùy thuộc vào framework
  • Đáp án đúng: B

Câu 4: Prompt template nào thiếu context nhất?

  • A) "Write a test for createTask"
  • B) "Write unit test for createTask(title, description), test empty title, format: Vitest"
  • C) "Generate test for POST /api/tasks endpoint, body: {title, description}, response format: {success, task/error}"
  • D) "Review code against SOLID and security"
  • Đáp án đúng: A & D

Câu 5: SQL injection vulnerability trong createTask(), fix nào đúng?

  • A) Chỉ validate title length
  • B) Use prepared statements (bind parameters)
  • C) Reject title có special characters
  • D) Trim title trước lưu
  • Đáp án đúng: B

Tiêu Chí Đánh Giá

Tiêu ChíĐiểm
Quiz: 5/5 đúng2 điểm
Bài tập về nhà hoàn thành2 điểm
RARV loop thực hành thành công2 điểm
Edge case detection skills2 điểm
Hiểu prompt templates2 điểm
Tổng cộng10 điểm

🔗 Liên Kết & Tài Liệu


💡 Ghi Chú Cho Giáo Viên

  • AI Tool: Khuyến khích học viên dùng ChatGPT, Claude, hoặc Copilot
  • Timing: Nếu chậm, bỏ phần code review (Template 5)
  • Supertest Issue: Cần mock database hoặc dùng in-memory SQLite cho test
  • Security: Nhấn mạnh SQL injection risk — discuss parameterized queries
  • Follow-up: Buổi tiếp theo sẽ implement code từ test này

📊 Mẹo Prompting Nâng Cao (Optional)

Nếu thời gian cho phép, giáo viên có thể dạy thêm:

  1. Chain-of-Thought Prompting
"Let's think step by step.
Step 1: What are the inputs?
Step 2: What are the expected outputs?
Step 3: What are edge cases?
Step 4: Generate test code"
  1. Few-Shot Prompting (ví dụ)
"Here's an example test:

test('rejects empty input', () => {
  expect(() => func('')).toThrow('Input required');
});

test('accepts valid input', () => {
  expect(func('valid')).toReturn(expected);
});

Now write similar tests for createTask()..."
  1. Role-Based Prompting
"You are a QA engineer reviewing code.
Focus on: security, performance, testability.
Output: markdown report."

Powered by CodyMaster × VitePress