codex-lv3-may-2025

Level 1: Quick Intro to Vitest

Vitest is a testing framework from the folks that brought you Vite. Learn how to set up and use Vitest to test your functions.


Level 2: Why Testing and Vitest?

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.

What You’ll Learn

In this guide, you’ll learn how to:


Level 3: Set up the project

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"
  }
}
Show Me: What the full package.json might look like When you run `npm init -y`, it creates a basic `package.json` (see **[npm](/codex-lv3-may-2025/VOCABULARY_LIST.html#npm)** for more on npm commands). After adding the `type` and `scripts` fields, your complete `package.json` might look like this: ```json { "name": "function-practice", "version": "1.0.0", "description": "", "main": "index.js", "type": "module", "scripts": { "test": "vitest" }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { "vitest": "^1.0.0" } } ``` **Key points:** - The `"type": "module"` field enables ES6 module syntax (import/export) - The `"scripts"` section includes your test command - `vitest` appears in `devDependencies` after installation - Other fields (name, version, etc.) are created by `npm init -y`

Now run the test command to verify Vitest is set up correctly (it will show no tests found, which is expected):

npm run test

🔍 Diving Deeper

Press q to quit the test runner when you’re done checking.

Key Terms


Level 4: Create function files

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();
}

Level 5: Create test files

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');
  });
});

🔍 Diving Deeper

Key Terms


Level 6: Run your tests

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

🔍 Diving Deeper

Key Terms


Level 7: Summary and Next Steps

Complete File Structure

function-practice/
├── utils.js
├── utils.test.js
├── package.json
└── node_modules/

Next Steps

Resources


Level 8: Adding More Tests

Once 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.


Level 9: Why Add More Tests?


Level 10: Testing with Zero

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!


Level 11: Testing with Very Large Numbers

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?


Level 12: Testing with Decimal Numbers

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!


Level 13: Testing with Positive and Negative Numbers

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?


Level 14: Testing When Result is Zero

What if the result itself is zero? Let’s test that:

Plan: ensure opposite values cancel out by verifying add(5, -5) returns 0.

Show Me ```javascript it('should add when result is zero', () => { const result = add(5, -5); expect(result).toBe(0); }); ```

Try it: Add this test and verify it works!


Level 15: Testing Fractions (Floating Point Precision)

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.

Show Me ```javascript it('should add fractions', () => { const result = add(0.1, 0.2); expect(result).toBeCloseTo(0.3); // Use toBeCloseTo for floating point! }); ```

🔍 Diving Deeper

Try it: Add this test. Notice we use toBeCloseTo() instead of toBe()!

Key Terms


Level 16: Testing Empty Strings

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.

Show Me ```javascript it('should handle empty string', () => { const result = toSnakeCase(''); expect(result).toBe(''); }); ```

Try it: Add this test to your toSnakeCase tests!


Level 17: Testing Single Words

What about a single word with no spaces?

Plan: prove toSnakeCase lowercases solo words by asserting toSnakeCase('Hello') yields hello.

Show Me ```javascript it('should handle single word', () => { const result = toSnakeCase('Hello'); expect(result).toBe('hello'); }); ```

Try it: Add this test and run it!


Level 18: Testing Multiple Spaces

What if there are multiple spaces between words?

Plan: see how repeated whitespace converts by testing toSnakeCase('Hello World') and observing the underscore trio.

Show Me ```javascript it('should handle multiple spaces', () => { const result = toSnakeCase('Hello World'); expect(result).toBe('hello___world'); }); ```

Try it: Add this test. Notice how multiple spaces become multiple underscores!


Level 19: Testing Very Long Text

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.

Show Me ```javascript it('should handle very long text', () => { const result = toSnakeCase('This Is A Very Long Sentence With Many Words'); expect(result).toBe('this_is_a_very_long_sentence_with_many_words'); }); ```

Try it: Add this test and see if it handles long text correctly!


Level 20: Testing Special Characters

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.

Show Me ```javascript it('should handle text with special characters', () => { const result = toSnakeCase('Hello World!'); expect(result).toBe('hello_world!'); }); ```

Try it: Add this test. Do special characters get preserved?


Level 21: Testing Numbers in Text

What if the text contains numbers?

Plan: confirm digits survive conversion by checking toSnakeCase('Hello 123 World') preserves 123 between underscores.

Show Me ```javascript it('should handle numbers in text', () => { const result = toSnakeCase('Hello 123 World'); expect(result).toBe('hello_123_world'); }); ```

Try it: Add this test and see how numbers are handled!


Level 22: Practice, Key Takeaways, and Next Steps

Practice: Your Turn!

Now it’s your turn to add some tests! Try adding tests for these scenarios:

For the add function:

  1. Test with two negative numbers - What happens when you add -1 and -2?
  2. Test with very small decimal numbers - Try adding 0.001 and 0.002
  3. Test with one number being zero - What about add(0, 10)?

For the toSnakeCase function:

  1. Test with text that has no spaces - What happens with 'HELLOWORLD'?
  2. Test with text that is only spaces - Try ' ' (three spaces)
  3. Test with mixed case - What about 'HeLLo WoRLd'?

Challenge: Write each test, run it, and see if it passes. If it fails, think about why!

Key Takeaways

Next Steps

Resources


Level 23: Red-Green-Refactor: Making Tests Fail, Then Pass

The Red-Green-Refactor cycle is a core testing practice:

  1. Red: Write a test that fails (because the function doesn’t do what you want yet)
  2. Green: Write the code to make the test pass
  3. Refactor: Improve the code while keeping tests green

This approach helps you:


Level 24: Write a Failing Test for Exclamation Marks (Red)

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.


Level 25: Make the Test Pass (Green) - 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?

Oops! 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:

  1. Update your toSnakeCase function with the new feature
  2. Update that previous test to expect 'hello_world_' instead of 'hello_world!' or just remove it.
  3. Run your tests again. Both the new test and the updated test from the previous level should pass (green)!

Level 26: Write a Failing Test for Question Marks (Red)

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!


Level 27: Make the Test Pass (Green) - Question Marks

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)!


Level 28: Write a Failing Test for Commas (Red)

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)!


Level 29: Make the Test Pass (Green) - Commas

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)!


Level 30: Refactor - Using Regex

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?

Try it now: Update your function with this regex and run your tests. They should all still pass (green)!


Level 31: Understanding the Regex Pattern

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?

Testing 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!

Key Terms


Level 32: Adding More Punctuation

Exercise: expand your regex to cover more punctuation.

Hint: revisit Level 31’s breakdown if you need to recall how character classes work.


Level 33: Using \W for All Non-Word Characters

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?

Try it: Update your function and run your tests. They should still pass!

Key Terms


Level 34: Practice: Your Turn!

Challenge 1: Test Multiple Punctuation Together

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?

Challenge 2: Test Mixed Punctuation

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.


Level 35: Key Takeaways, Next Steps, and Resources

Key Takeaways

  1. Red: Write tests first that describe what you want
  2. Green: Write the minimum code to make tests pass
  3. Refactor: Improve code while keeping tests green
  4. Regex: Use regular expressions to match patterns of characters
  5. Character classes: Use [] to match any of several characters
  6. \W: Matches any non-word character (punctuation, symbols)

Common Regex Patterns

Here 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, '_')

Quick Regex Reference for Beginners

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/

Next Steps

Resources


Level 36: Vitest Challenges: Building and Composing Functions

Now that you’ve learned the basics of Vitest, let’s practice by building real functions and composing them together!


Level 37: Function Ideas Reference

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.


Level 38 (Challenge): Build makeGreeting

Let’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:

Your task:

  1. Write tests for makeGreeting first (red)
  2. Implement the function to make tests pass (green)
  3. Test with different names and occasions

Try it: Create greeting.test.js and greeting.js, then write your tests and function!


Level 39 (Challenge): Build addSignature

Now let’s add a function that adds a signature to a message.

Function signature:

addSignature(message, from)  string

Examples:

Your task:

  1. Write tests for addSignature first
  2. Implement the function
  3. Test with different messages and signatures

Try it: Add tests and the function to your greeting.test.js and greeting.js files!


Level 40 (Challenge): Build decorateMessage

Now let’s create a function that decorates a message with emojis.

Function signature:

decorateMessage(message)  string

Examples:

Your task:

  1. Write tests for decorateMessage first
  2. Implement the function
  3. Test with different messages

Try it: Add this function to your greeting files!


Level 41 (Challenge): Compose Functions - Create a Full Greeting

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:

Your task:

  1. Write tests for createFullGreeting
  2. Implement it by calling your other three functions
  3. Think about the order: greeting → signature → decoration

Hint: 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!


Level 42: Add Test Coverage

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.

Step 1: Install the coverage reporter

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

Step 2: Add the coverage script

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.

Step 3: Run the coverage report

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
----------|---------|----------|---------|---------|-------------------

Step 4: Review your results

Try it: Install the reporter, add the script, run coverage, and note any functions that still need tests before moving on.

Key Terms


Level 43 (Challenge): Compose Functions - Create Multiple Variations

Let’s create more composed functions that use our building blocks in different ways.

5a. Create a greeting without signature

Function signature:

createDecoratedGreeting(name, occasion)  string

Example:

5b. Create a signed greeting without decoration

Function signature:

createSignedGreeting(name, occasion, from)  string

Example:

Your task:

  1. Write tests for both functions
  2. Implement them by composing your existing functions
  3. Notice how you can reuse the same building blocks in different ways!

Try it: Create these composed functions!


Level 44 (Challenge): Choose Your Own Function Cluster

Now it’s your turn! Choose one of the function clusters from function-ideas.md and build them out.

Available clusters:

Your task:

  1. Choose a cluster that interests you
  2. Build each function in the cluster:
    • Write tests first (red)
    • Implement the function (green)
    • Test edge cases
  3. Compose them into at least one larger function that uses multiple functions from your cluster
  4. Document your functions with clear input/output examples

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.


Level 45: Key Takeaways


Level 46: Next Steps


Level 47: Resources