Test-Driven Development (TDD) is a powerful methodology for writing clean, bug-free code. In this article, we’ll explore how to implement TDD using Bun’s built-in test runner, Bun Test, which is known for its speed and simplicity.
Test-driven development (TDD) is a software development practice in which tests are written before the code. The TDD practice guides the implementation and ensures functionality through iterative writing, testing, and refactoring cycles.
TDD is a development process that follows these steps:
This iterative process is designed to produce robust and well-tested code.
If you haven’t installed Bun, install it by following the instructions at the Bun JavaScript documentation.
Then, initialize a new project:
bun init
➜ example bun init bun init helps you get started with a minimal project and tries to guess sensible defaults. Press ^C anytime to quit package name (example): entry point (index.ts): Done! A package.json file was saved in the current directory.
Create a test file in the tests directory (e.g., tests/example.test.js). Bun automatically detects files ending with .test.ts or .test.js for testing.
mkdir tests touch tests/example.test.js
Let’s start with a simple example.
We’ll create a calculator file to implement some mathematical functions.
We’ll first focus on a simple function, like sum(), even though JavaScript already has a native addition operator. This allows us to concentrate on structuring the tests rather than the complexity of the logic.
Here’s the plan:
In the tests/calculator.test.js file, you can implement your tests:
import { describe, expect, it } from "bun:test"; import { sum } from "../calculator"; describe("sum function", () => { it("should return the sum of two numbers (both are positive)", () => { expect(sum(2, 3)).toBe(5); }); it("should return the sum of two numbers (one is negative)", () => { expect(sum(-1, 2)).toBe(1); }); });
These tests verify the behavior of the sum() function, defined in the calculator module. The tests are written using Bun's testing library and organized within a describe block named "sum function". The describe() block helps to group "similar" tests. Each it() block specifies a particular scenario to test. Here's what each test does:
These tests ensure that the sum function behaves as expected for basic addition scenarios, covering both positive numbers and mixed (positive and negative) inputs.
Now, you can create your calculator module that will export the sum() function.
In the calculator.ts file:
bun init
The first version of the function returns a hardcoded value, so I expect the tests to fail.
Running the tests:
➜ example bun init bun init helps you get started with a minimal project and tries to guess sensible defaults. Press ^C anytime to quit package name (example): entry point (index.ts): Done! A package.json file was saved in the current directory.
Now we can adjust the logic of the sum() function in the calculator.ts adjust the logic of the sum() function:
mkdir tests touch tests/example.test.js
Now, if you run the tests, you will have a "green" ✅ status.
If you want to run the same tests with different scenarios (input values), you can use the each() method.
import { describe, expect, it } from "bun:test"; import { sum } from "../calculator"; describe("sum function", () => { it("should return the sum of two numbers (both are positive)", () => { expect(sum(2, 3)).toBe(5); }); it("should return the sum of two numbers (one is negative)", () => { expect(sum(-1, 2)).toBe(1); }); });
Using a dataset-driven approach, this code tests the sum function from the calculator module. The it.each() method is employed to simplify repetitive test cases by iterating over a dataset of inputs and expected outputs. Here's a breakdown of how it works:
First, you can define a dataset
export function sum(a: number, b: number) { // Function yet to be implemented return 0; }
The dataset is an array of arrays. Each inner array represents a test case, where the elements correspond to:
The describe function groups all tests related to the sum function under a single block for better organization.
In the describe() block, it.each(dataset) iterates over each row in the dataset array.
"Sum of %d and %d should be %d" is a description template for the test, where %d is replaced with the actual numbers from the dataset during each iteration.
For example, the first iteration generates the description: "Sum of 2 and 3 should be 5".
In the callback function (a, b, expected), the elements of each row in the dataset are destructured into variables: a, b, and expected. Then, inside the test, the sum function is called with a and b, and the result is checked using expect() to ensure it matches the expected.
To show an additional example for TDD, let’s implement a mean function in the calculator module that calculates the mean (average) of an array of numbers. Following the TDD approach, we’ll start by writing the tests.
In the already existent calculator.test.js add these tests specific for mean() function:
bun init
Now in the calculator.ts file, add the mean() function:
➜ example bun init bun init helps you get started with a minimal project and tries to guess sensible defaults. Press ^C anytime to quit package name (example): entry point (index.ts): Done! A package.json file was saved in the current directory.
So now you can execute again the tests
mkdir tests touch tests/example.test.js
All the tests should pass.
In this case, the implementation is already tested, so no further refactoring is needed. However, always take the time to review your code for improvements.
Test coverage is a metric that measures the percentage of your codebase executed during automated tests. It provides insights into how well your tests validate your code.
The Bun test coverage helps to identify the "line coverage".
The line coverage checks whether each line of code is executed during the test suite.
Running the test coverage:
import { describe, expect, it } from "bun:test"; import { sum } from "../calculator"; describe("sum function", () => { it("should return the sum of two numbers (both are positive)", () => { expect(sum(2, 3)).toBe(5); }); it("should return the sum of two numbers (one is negative)", () => { expect(sum(-1, 2)).toBe(1); }); });
While high test coverage is important, it's not the only measure of code quality. Aim for meaningful tests focusing on functionality, edge cases, and critical parts of your application. Achieving 100% coverage is ideal, but not at the cost of writing unnecessary or trivial tests.
Test-Driven Development (TDD) with Bun Test empowers developers to write clean, maintainable, and robust code by focusing on requirements first and ensuring functionality through iterative testing. By leveraging Bun's fast and efficient testing tools, you can streamline your development process and confidently handle edge cases. Adopting TDD not only improves code quality but also fosters a mindset of writing testable, modular code from the start. Start small, iterate often, and let your tests guide your implementation.
The above is the detailed content of Test-Driven Development (TDD) with Bun Test. For more information, please follow other related articles on the PHP Chinese website!