A Guide to Testing in TypeScript Projects: Overcoming Challenges with Jest and Bun
In this blog post, I’ll walk you through my experience setting up and using Jest for testing in a TypeScript project. Along the way, I’ll share the hurdles I faced, particularly when trying to use Jest with Bun (my preferred package manager), and how I handled mocking LLM (Large Language Model) responses.
Again I will be demoing this on my project, DialectMorph
Using Jest
Framework
When it comes to testing, Jest is one of the most popular frameworks for JavaScript and TypeScript projects. I chose Jest because:
- Ease of Setup: Jest works out of the box with minimal configuration for most projects.
- Built-in Mocks and Spies: Jest has built-in support for mocking and spying, which makes it easy to isolate parts of your code, especially when working with external dependencies.
- Snapshot Testing: Jest provides the ability to perform snapshot testing, useful when dealing with complex objects.
- Active Ecosystem and Community: With a large community and lots of plugins, Jest offers solid support and continuous improvements.
For more information, you can visit Jest’s official documentation: Jest.
Bun: The Package Manager
I use Bun as my package manager for managing dependencies in my TypeScript projects. It is known for its speed and simplicity. However, I encountered a significant issue with Bun and Jest compatibility that I’ll elaborate on later. You can learn more about Bun at Bun.
Setting Up the Project
Initial Setup
To get started, I first installed Jest, TypeScript, and related dependencies:
bun add -d jest ts-jest @types/jest
Adding Bun to the Mix
Since I use Bun as my package manager, I ran into issues while trying to use Bun with Jest, as Bun doesn't natively support Jest out-of-the-box. This led to several frustrating days of trying to resolve compatibility problems. After many attempts at using bun test
for Jest, I eventually had to work around it by running Jest directly using npm
(even though Bun was the default package manager).
I used Bun for dependency management and npm for testing. This workaround helped resolve the Jest compatibility issue but resulted in additional setup time and confusion.
bun install
npm test
Mocking LLM Responses
One of the key challenges in testing is handling external dependencies, especially when dealing with large language models (LLMs) or API calls. In my project, I needed mock responses from LLMs interacting with the file system.
To achieve this, I used Jest’s powerful mocking abilities. I mocked several modules (fs
, os
, path
, and toml
) to isolate the functions under test. For example:
jest.mock("fs");
jest.mock("os");
jest.mock("path");
jest.mock("toml");
This allows me to simulate different scenarios without actually interacting with the file system or external dependencies.
Writing Test Cases
File Utility Functions
I had a set of utility functions to test, including file operations like creating directories and files. Here's how I structured the test cases for one of the functions, makeDir
:
describe("makeDir Function", () => {
it("Creates a Directory and returns its path", () => {
const result = makeDir("testDir");
expect(fs.mkdirSync).toHaveBeenCalledWith("/root/testDir", { recursive: true });
expect(result).toBe("/root/testDir");
});
it("Doesn't create directory if it already exists", () => {
(fs.existsSync as jest.Mock).mockReturnValue(true);
const result = makeDir("testDir");
expect(result).toBe("/root/testDir");
expect(fs.mkdirSync).not.toHaveBeenCalled();
});
});
Each of the utility functions (makeDir
, createFile
, extractCodeBlock
, loadTomlConfigFile
) had corresponding test cases that mocked the necessary parts of the code (like file reading and writing) to avoid side effects.
What I Learned from Writing Test Cases
Writing test cases helped me discover several interesting things:
-
Mocking complexity: Mocking certain modules, especially system-level ones like
fs
,os
, andpath
, proved tricky. However, Jest’sjest.mock
function made this easier once I figured out how to properly mock these external dependencies. -
Mocking non-trivial dependencies: One of the most challenging parts of mocking was the
toml
parsing, as it involved simulating both file reading and parsing logic. But with proper mocks, I could easily simulate various file contents. -
Test organization: I found that grouping related tests together (as seen in the nested
describe
blocks) kept my tests organized and made them easier to read and maintain.
Issues
- Jest and Bun compatibility issues: The biggest obstacle I encountered was integrating Jest with Bun. This issue caused a lot of wasted time, and I had to use npm instead of Bun for running tests. This was a frustrating experience and a learning moment about the importance of checking compatibility early in the process.
- Mocking non-obvious behaviors: Another moment of realization was learning how to mock file system behaviors, especially with Jest's ability to mock implementation details like file reads and writes. This was crucial to avoid side effects in tests.
Uncovered Bugs and Edge Cases
While writing tests, I discovered some edge cases in the file utility functions. For instance:
- Handling non-existent files: There were scenarios where the code didn't handle the absence of files correctly. By testing this behavior, I was able to ensure that the code didn’t break and returned expected results.
-
Unexpected behaviors with path resolution: By mocking the
path
module, I ensured that the directory and file paths were correctly resolved across different environments.
Conclusion
This process has been an eye-opener in terms of testing and mocking in JavaScript/TypeScript. Prior to this project, I hadn’t done much testing, and it was rewarding to see how structured testing could catch bugs and ensure code correctness.
I believe personally, I would rather avoid writing test cases at least after most of the code is written as it gets too complicated, for me If I know I have to write tests, I would rather begin with a TDD approach
Top comments (0)