A test is a short statement about the code's behavior, written so a machine can check it. In Vitest a minimal test consists of two things: the test function, which takes a name and a callback, and inside it the expect(value).matcher(expected) expression. The name explains what is checked; the callback runs the action under test; the matcher compares the result with the expectation and decides whether the test is green or red. There is no magic here, and that is exactly why everything else rests on it.
Related tests are grouped in describe - usually by entity or scenario. It is not required, but it gives a nested name in the report and a place for shared setup. describe('calculateDelivery', ...) reads as a section heading, and the nested test cases as its items. While there are few tests a flat list is fine; describe appears when a group has shared context.
Everything runs with the vitest command: with no arguments it is watch in the interactive terminal, which reruns the affected tests and offers filters right in the console, while vitest run is a single pass for CI. On the first run Vitest finds files by the glob, runs them and prints a summary: how many passed, how many failed and how long it took. Watch is handy while developing, run is for the pipeline. You filter individual tests by name with -t, or by path by just passing it to the command.
A newcomer's main skill is reading a failure rather than fearing it. Vitest shows what it Expected and what it Received, and points at the specific line with the matcher. If calculateDelivery returned 250 instead of 300, the report shows both and the place where the comparison did not hold. A red test is not a broken process but a precise message about exactly what diverged from the expectation.
You pick a matcher for the value's type. toBe compares primitives and references via Object.is and suits numbers, strings and booleans; objects and arrays need toEqual, which compares by value recursively. Precise matchers are a separate chapter, but the rule is simple already: the more specific the matcher, the clearer the error message.
The professional habit is the red-green loop. First you write a failing test for the desired behavior and make sure it is actually red and fails for the right reason. Then you add the minimal code until it turns green. The order matters: a test you never saw red may pass because of a typo in the matcher or an extra condition and actually check nothing.
It is worth writing a test's name as a statement about the condition and the observable outcome: 'charges 300 for standard delivery', not 'works' or 'test1'. Then the list of tests reads as a specification of behavior, and a red name in CI says at once what exactly broke - you do not have to dig into the code to understand what the test was about.
The typical newcomer failure is a test with no assertion: the callback calls a function but compares nothing. Such a test is always green and checks nothing beyond the fact that the call did not throw. Next to it is a second trap - an assertion that never failed: if you remove the code itself and the test is still green, it does not protect the behavior but only creates an illusion of coverage.
FAIL src/delivery.test.ts > charges 300 for standard delivery
expect(received).toBe(expected) // Object.is equality
Expected: 300
Received: 250
6| expect(calculateDelivery(order)).toBe(300)
| ^import { expect, test } from 'vitest'
import { calculateDelivery } from './delivery.js'
test('charges 300 for standard delivery', () => {
const order = { total: 1_000, premium: false }
expect(calculateDelivery(order)).toBe(300)
})