What you'll learn
Tests are executable evidence about behavior. A balanced strategy keeps most logic fast and isolated while protecting integration boundaries and a few critical user journeys.
By the end of this lesson, you'll be able to:
- Choose unit, integration, and end-to-end scopes
- Use test doubles without testing implementation details
- Test asynchronous and DOM behavior
Core mental model
Use this decision table as a compact reference. Focus on what each tool means and when it earns its place in production code.
| Concept | What it means | Decision rule |
|---|---|---|
| Unit test | Fast check of one focused behavior | Use for domain rules and pure transformations |
| Integration test | Several real boundaries work together | Use for database, HTTP, DOM, and module collaboration |
| End-to-end test | Critical journey through the deployed shape | Keep few, stable, and user-centered |
Professional workflow
Build the behavior in small, observable steps. Each step should leave something you can inspect or test.
- Describe the test strategy boundary: inputs, outputs, state, timing, and expected failures.
- Implement the smallest correct path with names that expose intent.
- Add edge cases and failure handling before introducing abstractions.
- Verify behavior with realistic data and one deliberately adversarial example.
- Refactor only after the observable behavior is protected.
Make behavior observable
Guided code lab
Test behavior at a public boundary
The test states a domain example instead of mirroring internal lines.
import { describe, expect, it } from "vitest";
import { total } from "./pricing.js";
describe("total", () => {
it("applies quantity and tax", () => {
const lines = [{ price: 10, quantity: 2 }];
expect(total(lines, 0.1)).toBe(22);
});
it("returns zero for an empty cart", () => {
expect(total([], 0.1)).toBe(0);
});
});Control an async dependency
Dependency injection keeps the test deterministic without replacing the function under test.
it("returns a normalized course", async () => {
const request = async () => ({ id: 7, display_name: "JavaScript" });
const service = createCourseService({ request });
await expect(service.get(7)).resolves.toEqual({ id: 7, name: "JavaScript" });
});Production practice
Contract
Write down the test strategy quality target, ownership boundary, evidence, and acceptable tradeoffs.
Verification
Use repeatable automated checks plus a realistic end-to-end scenario; include failure and recovery behavior.
Operations
Track a small set of user-centered signals and revisit the decision when evidence changes.
Common failure mode
Independent workshop
Add a layered test suite to the full-stack course application.
Your finished workshop must include:
- Unit tests for domain rules
- Integration tests for API and persistence
- One critical accessible browser journey
Definition of done
Recap & quick check
Key takeaways
- Tests protect behavior
- Scope follows risk and feedback speed
- Doubles replace boundaries selectively
- Critical journeys deserve end-to-end evidence
Quick check
1. Which tests should usually be most numerous?
2. What should a test double replace?
3. What makes async tests reliable?
Keep the workshop. Later modules deliberately build on these decisions, so each exercise can become part of your final portfolio architecture.