Test-Driven Development with Vitest: How to

A collection of katas with JavaScript.

Build Status Coverage Status

Test-Driven Development with Vitest: How to

A code kata is an exercise in programming which helps a programmer hone their skills through practice and repetition.

The term was probably first coined by Dave Thomas, co-author of the book The Pragmatic Programmer, in a bow to the Japanese concept of kata in the martial arts.

Katas

Setup

All project dependencies are installed and managed via npm, the Node.js package manager.

npm install
npm test

Continuous Integration with Github Actions workflows

A GitHub Actions workflow is essentially a customizable automated process that can perform various tasks. You set it up using a YAML file in your repository, and it kicks in based on events in your repository, manual triggers, or on a schedule you define.

These workflows live in the .github/workflows directory of your repository, and you can have multiple workflows, each handling different tasks.

This is just an example:

name: Node CI

on: [push]

jobs:
  test:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node: [18.x, 20.x, 22.x]

    steps:
    - uses: actions/checkout@v4

    - uses: actions/setup-node@v4
      with:
        node-version: ${{ matrix.node }}

    - name: Project setup `npm ci`
      run: npm ci

    - name: Run `test`
      run: npm test

Test-driven development

Test-driven development (TDD) is a software development process that relies on the repetition of a very short development cycle:

Toolkit

Vitest is a next-generation unit testing framework built on Vite, offering lightning-fast test execution, seamless migration from Jest due to its compatible features, intelligent watch mode for efficient development, and out-of-the-box support for modern JavaScript like ES modules, TypeScript, and JSX.

Istanbul is a JavaScript code coverage tool that helps you understand how well your tests exercise your codebase. It works by instrumenting your code to track which parts are executed during testing, providing insights into areas that might need more attention.

Stryker is a tool for mutation testing, specifically designed for JavaScript and similar languages. It injects tiny faults (mutations) into your code, then re-runs your tests. If a mutation breaks a test, it indicates your tests are effective at catching errors. This helps improve the quality and reliability of your codebase.

Example

Following an example of Test-Driven Development using Vitest for the most famous application: Hello World!

Setup is easy, just run an npm command and change few lines into your package.json.

npm install -D vitest
"scripts": {
  "test": "vitest"
},

Now you are able to run unit tests with npm test.

First of all we should create a new file HelloWorld.spec.js.

Now we can start writing our first test.

//- HelloWorldSpec.js

import { describe, it, expect } from 'vitest';
import HelloWorld from from './HelloWorld';

describe('HelloWorld', () => {
  it('should exist.', () => {
    // given
    new HelloWorld();
  });
});

❗ RED - Try running test and it will fail.


Create a new file HelloWorld.js.

The next step is writing some code that would cause the test to pass.

//- HelloWorld.js

function HelloWorld() {
}

export default HelloWorld;

πŸ’š GREEN - Try running test and it will pass.


❔ Need for refactoring?


We have a green bar! Now we can write a new test.

//- HelloWorld.spec.js

...

  it('should greet() correctly.', () => {
    // given
    const helloWorld = new HelloWorld();

    // then
    expect(helloWorld.greet()).to.equal('Hello world');
  });

...

❗ RED - Try running test and it will fail.


Now we can write some code that would cause the test to pass.

//- HelloWorld.js

...

HelloWorld.prototype.greet = function () {
  return 'Hello world';
};

πŸ’š GREEN - Try running test and it will pass.


❔ Need for refactoring?


πŸŽ‰ Done


SPEC - HelloWorld.spec.js

//- HelloWorld.spec.js

import { describe, it, expect } from 'vitest';
import HelloWorld from from './HelloWorld';

describe('HelloWorld', () => {

  it('should exist.', () => {
    // given
    new HelloWorld();
  });

  it('should greet() correctly.', () => {
    // given
    const helloWorld = new HelloWorld();

    // then
    expect(helloWorld.greet()).to.equal('Hello world');
  });

});

SRC - HelloWorld.js

//- HelloWorld.js

function HelloWorld() {
}

HelloWorld.prototype.greet = function () {
  return 'Hello world';
};

export default HelloWorld;

Now if we decide to refactor the application moving from prototype to class, we can do it without fear.

So, let’s do this 😎

//- HelloWorld.js

export default class {
  greet() {
    return 'Hello world'
  }
}

πŸ’š GREEN - Try running test and it will pass.

Further readings