Vitest is a testing framework from the folks that brought you Vite. Learn how to set up and use Vitest to test your functions.
Writing tests helps you verify that your code works correctly. When you write a function, tests let you check that it produces the expected output for different inputs. This helps catch bugs early and gives you confidence when making changes to your code.
Vitest is fast, easy to set up, and has a simple API that’s similar to other popular testing frameworks. It works great with modern JavaScript (ES6 modules) and provides helpful error messages when tests fail.
Note: Vitest will work with Vite projects, but you don’t need to use npm create vite to use Vitest. Vitest is a standalone testing framework that works in any Node.js project. You can use it with any project setup, not just Vite projects.
In this guide, you’ll learn how to:
First, initialize your npm project and install Vitest:
npm init -y
npm install --save-dev vitest
Then, update your package.json to add the test script and set the module type:
{
"type": "module",
"scripts": {
"test": "vitest"
}
}
Now run the test command to verify Vitest is set up correctly (it will show no tests found, which is expected):
npm run test
package.json so tooling like Vitest is only installed for development, keeping production bundles lean."type": "module" unlocks ES module syntax. See the type: “module” entry for a refresher on why modern bundlers like Vite expect ESM by default.npm run test executes the script you defined. Review npm scripts to see how custom commands plug into your workflow.Press q to quit the test runner when you’re done checking.
Create utils.js with your functions:
// utils.js
export function add(a, b) {
return a + b;
}
export function toSnakeCase(text) {
// Convert text to snake_case by replacing spaces with underscores and lowercasing
return text.replaceAll(' ', '_').toLowerCase();
}
We add .test. to the filename (like utils.test.js) to indicate this is a test file. Vitest automatically finds and runs files that match the pattern *.test.js or *.spec.js.
Create utils.test.js:
// utils.test.js
import { describe, it, expect } from 'vitest';
import { add, toSnakeCase } from './utils.js';
describe('add function', () => {
it('should add two positive numbers', () => {
const result = add(2, 3);
expect(result).toBe(5);
});
it('should add negative numbers', () => {
const result = add(-1, -2);
expect(result).toBe(-3);
});
});
describe('toSnakeCase function', () => {
it('should convert text with spaces to snake_case', () => {
const result = toSnakeCase('Hello World');
expect(result).toBe('hello_world');
});
it('should convert to lowercase', () => {
const result = toSnakeCase('HELLO WORLD');
expect(result).toBe('hello_world');
});
});
it should focus on one behavior.Now that you have functions and tests, run your tests:
npm run test
Or run in watch mode (automatically reruns on file changes):
npm run test -- --watch
vitest --watch narrows reruns to changed files.console.log or breakpoints to tighten your feedback loop. When you’re done, remember you can press q to exit.function-practice/
├── utils.js
├── utils.test.js
├── package.json
└── node_modules/
--watch flag to see tests run automatically when you save filesOnce you have basic tests working, it’s time to add more test cases! Testing different scenarios helps ensure your functions work correctly in all situations.
Thoughtful tests confirm behavior and also serve as living documentation for teammates who depend on these functions.
In the next levels we’ll lean into this mindset by layering on robust tests that stress your functions from every angle.
Let’s start by testing what happens when we use zero. Add this test to your add function:
Plan: confirm the add helper treats zero as neutral by writing a test that checks add(5, 0) still returns 5.
it('should add zero', () => {
const result = add(5, 0);
expect(result).toBe(5);
});
Try it: Run your tests to make sure this passes!
Now let’s test with very large numbers to see if our function handles them correctly:
Plan: stress-test add with a huge value and ensure it increments without overflow by asserting add(999999999, 1) equals 1000000000.
it('should add very large numbers', () => {
const result = add(999999999, 1);
expect(result).toBe(1000000000);
});
Try it: Add this test and run it. Does it pass?
What about decimal numbers? Let’s test that:
Plan: verify add handles decimals cleanly by checking add(3.14, 2.86) produces the precise total you expect.
it('should add decimal numbers', () => {
const result = add(3.14, 2.86);
expect(result).toBe(6);
});
Try it: Add this test and see what happens!
Let’s test what happens when we add a positive number to a negative number:
Plan: confirm add balances signs correctly by asserting add(10, -5) results in 5.
it('should add positive and negative numbers', () => {
const result = add(10, -5);
expect(result).toBe(5);
});
Try it: Add this test. What do you expect the result to be?
What if the result itself is zero? Let’s test that:
Plan: ensure opposite values cancel out by verifying add(5, -5) returns 0.
Try it: Add this test and verify it works!
When working with decimal numbers like 0.1 + 0.2, JavaScript’s floating point arithmetic can cause tiny rounding errors. We need to use toBeCloseTo() instead of toBe():
Plan: demonstrate floating point quirks by expecting add(0.1, 0.2) to be close to 0.3 rather than exactly equal.
0.1 + 0.2 becomes 0.30000000000000004). toBeCloseTo compares with a tolerance so tiny rounding errors don’t break your tests.toBeCloseTo(0.3, 5), to assert five decimal places.Try it: Add this test. Notice we use toBeCloseTo() instead of toBe()!
Now let’s move to testing toSnakeCase. What happens with an empty string?
Plan: check that toSnakeCase('') returns an empty string so blank inputs remain unchanged.
Try it: Add this test to your toSnakeCase tests!
What about a single word with no spaces?
Plan: prove toSnakeCase lowercases solo words by asserting toSnakeCase('Hello') yields hello.
Try it: Add this test and run it!
What if there are multiple spaces between words?
Plan: see how repeated whitespace converts by testing toSnakeCase('Hello World') and observing the underscore trio.
Try it: Add this test. Notice how multiple spaces become multiple underscores!
Let’s test with a very long sentence:
Plan: validate long strings stay consistent by mapping "This Is A Very Long Sentence With Many Words" to the expected snake case version.
Try it: Add this test and see if it handles long text correctly!
What about text with special characters like exclamation marks?
Plan: capture how punctuation is treated by asserting toSnakeCase('Hello World!') keeps the exclamation mark intact at the end.
Try it: Add this test. Do special characters get preserved?
What if the text contains numbers?
Plan: confirm digits survive conversion by checking toSnakeCase('Hello 123 World') preserves 123 between underscores.
Try it: Add this test and see how numbers are handled!
Now it’s your turn to add some tests! Try adding tests for these scenarios:
For the add function:
-1 and -2?0.001 and 0.002add(0, 10)?For the toSnakeCase function:
'HELLOWORLD'?' ' (three spaces)'HeLLo WoRLd'?Challenge: Write each test, run it, and see if it passes. If it fails, think about why!
toBeCloseTo() for decimal/fraction testingundefined or null - what happens?The Red-Green-Refactor cycle is a core testing practice:
This approach helps you:
Let’s say we want our toSnakeCase function to also replace exclamation marks with underscores. Currently, our function only handles spaces:
// Current function
export function toSnakeCase(text) {
return text.replaceAll(' ', '_').toLowerCase();
}
Let’s write a test that we know will fail because the function doesn’t handle exclamation marks yet:
it('should replace exclamation marks with underscores', () => {
const result = toSnakeCase('Hello World!');
expect(result).toBe('hello_world_');
});
Try it now: Add this test to your toSnakeCase tests and run it. What happens?
You should see a red (failing) test! The test expects 'hello_world_' but gets 'hello_world!' because the function only replaces spaces, not exclamation marks.
Now let’s fix it by adding a replaceAll() call for exclamation marks:
export function toSnakeCase(text) {
return text.replaceAll(' ', '_').replaceAll('!', '_').toLowerCase();
}
What does this do?
replaceAll(' ', '_') - replaces all spaces with underscoresreplaceAll('!', '_') - replaces all exclamation marks with underscorestoLowerCase() - converts everything to lowercaseOops! We broke our old test!
When we add this feature to convert ! to _, we’re changing how our function works. Remember that test we wrote back in a previous level? It’s going to fail now!
In a previous level, we had a test that expected:
it('should handle text with special characters', () => {
const result = toSnakeCase('Hello World!');
expect(result).toBe('hello_world!'); // This expects the ! to be preserved
});
But now our function converts ! to _, so the result will be 'hello_world_' instead of 'hello_world!'.
Sometimes we need to change our tests! When we intentionally change how a function works, we need to update our tests to match the new behavior. Go back to that previous test and update it to expect 'hello_world_' instead of 'hello_world!'.
Try it now:
toSnakeCase function with the new feature'hello_world_' instead of 'hello_world!' or just remove it.Now let’s add support for question marks. Write a test that will fail:
it('should replace question marks with underscores', () => {
const result = toSnakeCase('Hello World?');
expect(result).toBe('hello_world_');
});
Try it now: Add this test and run it. It should fail (red) because we haven’t added support for question marks yet!
Now let’s fix it! You need to add support for question marks, similar to how we added support for exclamation marks.
Try it now: Update your function to handle question marks and run your tests. Both tests should pass (green)!
Now let’s add support for commas. Write a test that will fail:
it('should replace commas with underscores', () => {
const result = toSnakeCase('Hello, World');
expect(result).toBe('hello__world');
});
Try it now: Add this test and run it. It should fail (red)!
Now let’s fix it! You need to add support for commas, following the same pattern as before.
Try it now: Update your function to handle commas and run your tests. All three tests should pass (green)!
Now we have a lot of replaceAll() calls! We can simplify this using a regular expression (regex) to match multiple characters at once.
A regex lets us match a pattern of characters. We can use square brackets [] to match any of the characters inside:
export function toSnakeCase(text) {
return text.replaceAll(' ', '_').replaceAll(/[!?,]/g, '_').toLowerCase();
}
What does this regex do?
/[!?,]/g is a regular expression pattern with the global flag[] mean “match any of these characters”!?, means match exclamation marks, question marks, or commas/ mark the start and end of the regex patterng flag makes the regex global so replaceAll() can replace every matchTry it now: Update your function with this regex and run your tests. They should all still pass (green)!
Let’s break down the regex pattern /[!?,]/g:
/[!?,]/g
││││││││
│││││││└─ g = global flag (replace every match)
││││││└── / = end of regex pattern
│││││└─── ] = end of character class (match any of these)
││││└──── , = comma character
││└───── ? = question mark character
││└────── ! = exclamation mark character
│└─────── [ = start of character class (match any of these)
└──────── / = start of regex pattern
Why use regex?
replaceAll() calls, we have one!g flag ensures the regex applies globally so replaceAll() updates every matchTesting Regex Patterns: Want to test and experiment with regex patterns? Check out regex101.com - it’s a great tool for testing regex patterns and seeing what they match. You can paste your regex pattern and test it against sample text to see exactly what it matches!
Exercise: expand your regex to cover more punctuation.
., ;, and : with underscores.toSnakeCase handles the extra marks.Hint: revisit Level 31’s breakdown if you need to recall how character classes work.
If we want to replace ALL punctuation (not just specific ones), we can use \W which matches any non-word character:
export function toSnakeCase(text) {
return text.replaceAll(' ', '_').replaceAll(/\W/g, '_').toLowerCase();
}
What does \W do?
\W matches any non-word character (punctuation, symbols, etc.)\w stands for word characters; uppercase \W flips the meaning to non-word characters.g flag makes the regex global so every non-word character gets replacedTry it: Update your function and run your tests. They should still pass!
Write a test that checks if multiple punctuation marks work together:
it('should replace multiple punctuation marks', () => {
const result = toSnakeCase('Hello!!! World???');
expect(result).toBe('hello___world___');
});
Try it: Add this test. Does it pass with your current function?
Exercise: design a test that mixes commas, exclamation marks, and question marks in the same string, then update toSnakeCase if needed so the assertion passes.
[] to match any of several charactersHere are some useful patterns:
// Replace spaces (simple string replacement)
text.replaceAll(' ', '_')
// Replace specific punctuation
text.replaceAll(/[!?,]/g, '_')
// Replace all punctuation (non-word characters)
text.replaceAll(/\W/g, '_')
// Replace everything except letters, numbers, and underscores
text.replaceAll(/[^a-zA-Z0-9_]/g, '_')
Here are some common regex patterns you might use:
| Pattern | Matches | Example |
|---|---|---|
[abc] |
Any of these characters (a, b, or c) | /[abc]/g matches “a”, “b”, or “c” |
[!?,] |
Any of these punctuation marks | /[!?,]/g matches “!”, “?”, or “,” |
\W |
Any non-word character (punctuation, symbols) | /\W/g matches “!”, “?”, “,”, etc. |
\w |
Any word character (letters, numbers, underscore) | /\w/g matches “a”, “1”, “_” |
\s |
Any whitespace (spaces, tabs) | /\s/g matches “ “ (space) |
\d |
Any digit (0-9) | /\d/g matches “0” through “9” |
[^abc] |
NOT any of these characters | /[^abc]/g matches anything except a, b, or c |
+ |
One or more of the previous | /\W+/g matches one or more punctuation marks |
* |
Zero or more of the previous | /\d*/g matches zero or more digits |
? |
Zero or one of the previous | /\d?/g matches zero or one digit |
Note: In JavaScript, regex patterns are written between forward slashes: /pattern/
Now that you’ve learned the basics of Vitest, let’s practice by building real functions and composing them together!
Check out function-ideas.md for a list of function ideas organized by category. We’ll work through some of these together, then you’ll choose your own to practice with.
makeGreetingLet’s start with the first greeting function. Create a function that takes a name and an occasion, and returns a greeting message.
Function signature:
makeGreeting(name, occasion) → string
Examples:
makeGreeting('Alex', 'Birthday') → "Happy Birthday, Alex!"makeGreeting('Sam', 'New Year') → "Happy New Year, Sam!"Your task:
makeGreeting first (red)Try it: Create greeting.test.js and greeting.js, then write your tests and function!
addSignatureNow let’s add a function that adds a signature to a message.
Function signature:
addSignature(message, from) → string
Examples:
addSignature('Happy Birthday, Alex!', 'Sam') → "Happy Birthday, Alex! — from Sam"addSignature('Happy New Year, Sam!', 'Alex') → "Happy New Year, Sam! — from Alex"Your task:
addSignature firstTry it: Add tests and the function to your greeting.test.js and greeting.js files!
decorateMessageNow let’s create a function that decorates a message with emojis.
Function signature:
decorateMessage(message) → string
Examples:
decorateMessage('Happy Birthday, Alex!') → "🌸🌸 Happy Birthday, Alex! 🌸🌸"decorateMessage('Hello World') → "🌸🌸 Hello World 🌸🌸"Your task:
decorateMessage firstTry it: Add this function to your greeting files!
Now that we have all three functions, let’s compose them together! Create a function that uses all three to create a decorated, signed greeting.
Function signature:
createFullGreeting(name, occasion, from) → string
Examples:
createFullGreeting('Alex', 'Birthday', 'Sam') → "🌸🌸 Happy Birthday, Alex! — from Sam 🌸🌸"Your task:
createFullGreetingHint: You can call functions inside other functions:
function createFullGreeting(name, occasion, from) {
const greeting = makeGreeting(name, occasion);
const signed = addSignature(greeting, from);
return decorateMessage(signed);
}
Try it: Compose your functions together!
Keeping an eye on test coverage helps you understand which parts of your code are exercised by your suite. Let’s install the coverage peer dependency and add a script that makes running coverage painless.
Vitest’s coverage command relies on the V8/Istanbul integration shipped in @vitest/coverage-v8. Install it as a dev dependency using npm:
npm install -D @vitest/coverage-v8
Update your package.json scripts section so it includes a test:coverage command:
"scripts": {
"test": "vitest",
"test:coverage": "vitest run --coverage"
}
Tip: If you already have other scripts (like
"dev"or"lint"), just add this line alongside them—keep the trailing commas consistent with the existing JSON.
npm run test:coverage
Vitest will execute the full suite once and print a table showing statements, branches, functions, and lines covered. The report also lands in the coverage/ directory if you want to inspect the HTML output.
Example console output:
% Coverage report from v8
----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 50 | 100 | 50 | 50 |
utils.js | 50 | 100 | 50 | 50 | 2
----------|---------|----------|---------|---------|-------------------
npm run test:coverage after writing new tests to confirm the numbers move in the right directionTry it: Install the reporter, add the script, run coverage, and note any functions that still need tests before moving on.
Let’s create more composed functions that use our building blocks in different ways.
Function signature:
createDecoratedGreeting(name, occasion) → string
Example:
createDecoratedGreeting('Alex', 'Birthday') → "🌸🌸 Happy Birthday, Alex! 🌸🌸"Function signature:
createSignedGreeting(name, occasion, from) → string
Example:
createSignedGreeting('Alex', 'Birthday', 'Sam') → "Happy Birthday, Alex! — from Sam"Your task:
Try it: Create these composed functions!
Now it’s your turn! Choose one of the function clusters from function-ideas.md and build them out.
Available clusters:
convertToCups, calculateTip, applyDiscountdouble, addTax, distancemixColors, lighten, convertArrayToRGBtoSnakeCase, toKebabCase, toCamelCasesimplifyPokemonObject, tempToday, conditionsTodayYour task:
Example workflow:
// Step 1: Build individual functions
function calculateTip(total, percent) { /* ... */ }
function applyDiscount(price, discount) { /* ... */ }
// Step 2: Compose them
function calculateFinalPrice(price, discount, tipPercent) {
const discounted = applyDiscount(price, discount);
return calculateTip(discounted, tipPercent);
}
Try it: Pick a cluster and build it out! Write tests, implement functions, and compose them together.