Skip to main content

Unit Test Generation

Exercise A
⏱ 25 min🟡 Intermediate

Unit tests are where Copilot delivers the most immediate value — they are self-contained, predictable in structure, and follow well-known patterns. In this exercise you will use Copilot to generate a full suite for calculateDiscount(), review every suggestion critically, and expose three bugs that weak assertions miss.

Practice repo for this stepExercise A

Work from master in copilot-testing-workshop. Use the solution checkpoints only if you need to catch up or compare your result after the exercise.

Participant goalGenerate Jest unit tests for calculateDiscount() using Copilot. Discover that weak prompts produce tests that pass against buggy code; strong prompts expose all 3 bugs.
Run commandnpm test -- tests/unit/calculateDiscount.test.ts

Primary files

  • workshop-exercises/src/services/calculateDiscount.ts
  • workshop-exercises/tests/unit/calculateDiscount.test.ts
  • workshop-exercises/.copilot/context/domain-rules.md

Recovery checkpoint

  • solutions
Unit Test Generation Flow
📄calculateDiscount.ts
context for▸
🤖Copilot Chat
generates▸
🧪Generated Tests
reviewed by▸
👀Review & Fix
run in▸
✅Jest Runner

The Prompt-First Approach​

The quality of Copilot's output depends heavily on the quality of your prompt. Vague prompts produce vague tests.

Prompt QualityExampleResult
❌ Too vague"Write tests for calculateDiscount"Happy-path only, toBeTruthy() assertions that miss bugs
⚠️ Better"Write Jest unit tests for calculateDiscount with SAVE10 and FLAT5"Correct structure, may miss boundary and negative cases
✅ Best"Write Jest unit tests for calculateDiscount. Cover: SAVE10 (10% off), FLAT5 ($5 off orders ≥ $20), unknown codes, case-insensitivity, and the negative-total clamp."Targeted, boundary-aware, ready to review

Step 1 — Explore the Function Under Test​

// src/services/calculateDiscount.ts
export interface DiscountInput {
subtotal: number;
code: string;
}

export interface DiscountResult {
discountAmount: number;
finalTotal: number;
}

// Active discount codes — see .copilot/context/domain-rules.md for full rules
// SAVE10 : 10% off, no minimum
// FLAT5 : $5 off, $20 minimum order
// EXPIRED: 15% off, expired — no longer applied
export function calculateDiscount({ subtotal, code }: DiscountInput): DiscountResult {
// implementation hidden — your tests will verify the contract
}
💡 Context tip

Attach #file:.copilot/context/domain-rules.md to your Copilot Chat prompt so it knows the discount rules before generating assertions.

Step 2 — Generate Tests with Copilot​

Select calculateDiscount.ts, open Copilot Chat, and use this prompt:

You are a senior engineer writing Jest unit tests for a TypeScript Node.js project.

Context files:
- #file:src/services/calculateDiscount.ts
- #file:.copilot/context/domain-rules.md

Write a complete Jest test suite for `calculateDiscount` that:

1. Tests the golden path (valid inputs → expected outputs with exact values)
2. Tests every boundary condition listed in the domain rules
3. Tests every error path (invalid input, edge cases, constraint violations)
4. Uses the AAA pattern (Arrange / Act / Assert) with a blank line between sections
5. Names each test: "<subject> <condition> <expected outcome>"
6. Uses .toBe() for primitives and .toEqual() for objects — never toBeDefined() or toBeTruthy()
7. Covers negative cases — what happens when the function receives invalid or extreme input?

Do NOT:
- Mock the function under test
- Write tests that always pass regardless of the implementation
- Use toBeTruthy() or toBeDefined() as the primary assertion

Paste the output into tests/unit/calculateDiscount.test.ts

Step 3 — Review the Generated Output​

Copilot should produce tests that look something like this. Read them carefully before accepting — there are common problems to look for.

// tests/unit/calculateDiscount.test.ts (Copilot-generated — review before accepting)
import { calculateDiscount } from '../../src/services/calculateDiscount';

describe('calculateDiscount', () => {
it('SAVE10 deducts exactly 10% of the subtotal', () => {
const result = calculateDiscount({ subtotal: 100, code: 'SAVE10' });
expect(result.discountAmount).toBe(10);
expect(result.finalTotal).toBe(90);
});

it('FLAT5 deducts $5 from a $50 order', () => {
const result = calculateDiscount({ subtotal: 50, code: 'FLAT5' });
expect(result.discountAmount).toBe(5);
expect(result.finalTotal).toBe(45);
});

it('FLAT5 is not applied when subtotal is below the $20 minimum', () => {
const result = calculateDiscount({ subtotal: 15, code: 'FLAT5' });
expect(result.discountAmount).toBe(0);
expect(result.finalTotal).toBe(15);
});

it('returns zero discount for an unknown code', () => {
const result = calculateDiscount({ subtotal: 100, code: 'INVALID' });
expect(result.discountAmount).toBe(0);
expect(result.finalTotal).toBe(100);
});

it('codes are case-insensitive', () => {
const result = calculateDiscount({ subtotal: 100, code: 'save10' });
expect(result.discountAmount).toBe(10);
expect(result.finalTotal).toBe(90);
});

it('finalTotal is never negative when discount exceeds subtotal', () => {
const result = calculateDiscount({ subtotal: 3, code: 'FLAT5' });
expect(result.finalTotal).toBeGreaterThanOrEqual(0);
expect(result.discountAmount).toBeLessThanOrEqual(3);
});
});
🔍 Common issues to look for in generated unit tests:

toBeTruthy() or toBeDefined() instead of exact value assertions — these mask bugs

  • Tests that only check the happy path — boundaries and edge cases are where bugs hide
  • Missing the negative-total invariant test — the clamp to 0 is a real business rule
  • No case-insensitivity test — easy to miss, commonly broken

Run the Tests​

npx jest tests/unit/calculateDiscount.test.ts --verbose
💡 Expect failures, not green

calculateDiscount.ts still has its 3 seeded bugs. Strong, exact-value assertions like the ones above should fail against it. If you see all green here, your assertions are too loose to catch the bugs — go back and tighten them.

Expected output
FAIL tests/unit/calculateDiscount.test.ts
calculateDiscount
✕ SAVE10 deducts exactly 10% of the subtotal (2 ms)
✓ FLAT5 deducts $5 from a $50 order (1 ms)
✕ FLAT5 is not applied when subtotal is below the $20 minimum (1 ms)
✓ returns zero discount for an unknown code (1 ms)
✕ codes are case-insensitive (1 ms)
✕ finalTotal is never negative when discount exceeds subtotal (1 ms)

● calculateDiscount › SAVE10 deducts exactly 10% of the subtotal

expect(received).toBe(expected)

Expected: 10
Received: 100 // BUG 1 — divides by 10 instead of 100

● calculateDiscount › FLAT5 is not applied when subtotal is below the $20 minimum

expect(received).toBe(expected)

Expected: 0
Received: 5 // BUG 2 — missing $20 minimum-order guard

● calculateDiscount › codes are case-insensitive

expect(received).toBe(expected)

Expected: 10
Received: 100 // BUG 1 again, via the lowercase path

● calculateDiscount › finalTotal is never negative when discount exceeds subtotal

expect(received).toBeGreaterThanOrEqual(expected)

Expected: >= 0
Received: -2 // BUG 3 — finalTotal is never clamped to 0

Tests: 4 failed, 2 passed, 6 total
🏋️Hands-on Challenge — Find and Fix Weak Tests
⏱15 min

Open tests/unit/calculateDiscount.weak.test.ts. These tests were AI-generated with a vague prompt. Each one passes today — but none of them would catch the three bugs hidden in the implementation. Find all three bugs and rewrite the assertions.

The Weak Tests (what Copilot produced)​

// calculateDiscount.weak.test.ts — passes today but proves almost nothing

it('SAVE10 produces a discount', () => {
const r = calculateDiscount({ subtotal: 100, code: 'SAVE10' });
expect(r.discountAmount).toBeTruthy(); // ← passes even if discount is $100
});

it('FLAT5 processes the order', () => {
const r = calculateDiscount({ subtotal: 15, code: 'FLAT5' });
expect(r).toBeDefined(); // ← passes even when discount should be 0
});

it('handles a discount code', () => {
const r = calculateDiscount({ subtotal: 3, code: 'FLAT5' });
expect(r.finalTotal).toBeDefined(); // ← passes even if finalTotal is negative
});
HintWhat each weak test misses
  • Bug 1 — toBeTruthy() passes for any non-zero value. If the implementation deducts 100% instead of 10%, this test still passes. Assert the exact amount: toBe(10).
  • Bug 2 — subtotal: 15 is below the $20 minimum; FLAT5 should not apply. toBeDefined() passes regardless. Assert discountAmount is 0.
  • Bug 3 — subtotal: 3 is below the $5 flat deduction; without the clamp,finalTotal could be -2. toBeDefined() misses this entirely. Assert finalTotal toBeGreaterThanOrEqual(0).
SolutionStrong Rewrites
// Bug 1 fixed — assert exact amount, not just truthy
it('SAVE10 deducts exactly 10%, not 100%', () => {
const r = calculateDiscount({ subtotal: 100, code: 'SAVE10' });
expect(r.discountAmount).toBe(10);
expect(r.finalTotal).toBe(90);
});

// Bug 2 fixed — assert the minimum-order guard works
it('FLAT5 is skipped when subtotal is below the $20 minimum', () => {
const r = calculateDiscount({ subtotal: 15, code: 'FLAT5' });
expect(r.discountAmount).toBe(0);
expect(r.finalTotal).toBe(15);
});

// Bug 3 fixed — assert the non-negative finalTotal invariant
it('finalTotal is never negative when discount exceeds subtotal', () => {
const r = calculateDiscount({ subtotal: 3, code: 'FLAT5' });
expect(r.finalTotal).toBeGreaterThanOrEqual(0);
expect(r.discountAmount).toBeLessThanOrEqual(3);
});
BonusPrompt Copilot to improve weak tests

Select the three weak tests in Copilot Chat and paste this prompt:

Review these tests. Replace any toBeTruthy/toBeDefined assertions with
specific value matchers. Add the missing boundary and minimum-order cases.
Each test name should describe the behavior it proves, not the code path.
🏋️Bonus: Prompt Engineering Challenge
⏱10 min

Write a prompt that produces all six strong tests from the generated output above in a single Copilot Chat turn. Then identify what Copilot still won't get right even with your best prompt.

HintWhat a strong prompt must include
  • Attach #file:calculateDiscount.ts and #file:.copilot/context/domain-rules.md
  • List every case explicitly: golden path, boundary (exactly at $20), below minimum, unknown code, case-insensitivity, negative clamp
  • Specify the assertion style: "use toBe() for exact values, not toBeTruthy or toBeDefined"
  • Specify test names: "name each test for the behavior it proves"
BonusWhat Copilot still won't get right
  • The EXPIRED code — Copilot won't know it exists unless you add it to the prompt
  • Exactly-at-minimum boundary — subtotal: 20 with FLAT5 should apply; Copilot may skip this unless explicitly asked
  • Domain intent — whether a 10% discount on a $3 order is a business error or a valid case requires your judgment, not Copilot's
🏋️Bonus: More Services to Practice On
⏱20 min

calculateDiscount is one pure function. The rest of the checkout pipeline has its own service classes, each already stubbed with it.todo placeholders in tests/unit/ — not timed, not required, just extra reps if you finish early or want more practice with the prompt template from .copilot/skills/unit-testing.md on real classes instead of one function.

FileWhat to practice
tests/unit/cartService.test.tsAdd/remove items, merge duplicate products, subtotal calculation, ITEM_NOT_FOUND
tests/unit/discountService.test.tsSame discount rules as Exercise A, but testing the class + repository instead of a pure function — compare how the prompt changes
tests/unit/fraudService.test.tsRisk-scoring thresholds and country blocks — clear numeric rules make great Copilot prompts
tests/unit/paymentService.test.tsThe pending → captured → refunded state machine and its invalid-transition errors
HintReuse the same prompt template

Swap the target file and function/class name into the template from .copilot/skills/unit-testing.md:

Context files:
- #file:src/services/<serviceName>.ts
- #file:.copilot/context/domain-rules.md

Write a complete Jest test suite for <ServiceClass> that:
1. Tests the golden path with exact expected values
2. Tests every boundary condition from the domain rules
3. Tests every error path (thrown ApiError codes)
4. Uses AAA pattern, one assertion group per test
5. Uses .toBe()/.toEqual() — never toBeTruthy()/toBeDefined()

🔧 Troubleshooting​

HintTests pass but bugs are present

Run the weak test file against a broken implementation: npx jest calculateDiscount.weak.test.ts. All three tests should still pass despite the bugs — that is the point of the exercise. The strong tests should detect all three failures.

HintTypeScript cannot find calculateDiscount

Check your import path is relative to the test file location: import { calculateDiscount } from '../../src/services/calculateDiscount'. Run npx jest --verbose to see the full import error.

Key Takeaways​

  • Specific prompts with attached context files produce better, targeted test suggestions
  • Always assert exact values — toBeTruthy() and toBeDefined() hide bugs
  • Boundary cases (exactly at minimum, zero, negative) are where real bugs live
  • Run tests against a broken implementation to verify they actually catch the bug
  • Copilot drafts the cases; you validate the intent and the assertions
Done with this step? Mark it to track your progress.