
Testkit Mastery, Part 2: Designing a Developer-Friendly Testkit
This article is part of a series on building testkits for complex libraries that behave differently in non-production environments. In our case, we needed a testkit for Vulcan, a global state management library that provides a caching layer and centralizes data across microfrontends. Vulcan relies on a specific production setup to function correctly, which makes it challenging to test in environments like Vitest and Storybook.
This Series on Testkit Mastery:
- Overcoming Testing Challenges for Complex Libraries
- Designing a Developer-Friendly Testkit
- Building the Core Structure of a Flexible and Reliable Testkit
- Design for Easy Integration into Testing Environments
- Key Takeaways and Lessons Learned for Building Better Testkits
In the previous part, we examined these challenges and defined our approach for mocking Vulcan’s functionality. With this foundation in place, our next step was to prototype an API that would make the testkit intuitive and flexible for developers. In this article, we’ll walk through the process of prototyping the testkit’s API, exploring the key requirements that emerged to guide its design.
Prototyping the Testkit with Real Tests
To design an API that felt intuitive, we began by writing tests as if the testkit already existed. This approach allowed us to simulate how developers would interact with the testkit in practice and identify the essential features it needed to support. By mimicking real usage patterns, we could ensure that the API provided a smooth and efficient testing experience.
Here’s an example of a test written to simulate how the testkit might be used:
import { mockedVulcan } from 'mondaycom/vulcan/testkit';
import { render } from '@testing-library/react';
import UserName from './UserName';
describe('UserName component', () => {
it("should render a loading message when still fetching the user's data", () => {
const userId = 123;
mockedVulcan.user.getById.withLoadingTime(10000); // Simulating a long loading time
const { getByText } = render(<UserName userId={userId} />);
expect(getByText(`Loading user data...`)).toBeInTheDocument();
});
it("should render the user's name when the user's data is fetched", () => {
const userId = 123;
mockedVulcan.user.getById.withResolvedData({ id: userId, name: 'John Doe' });
const { getByText } = render(<UserName userId={userId} />);
expect(getByText('User name: John Doe')).toBeInTheDocument();
});
});
Writing tests like this gave us valuable insight into what developers would need to customize Vulcan’s behavior in different scenarios. For example, we realized that developers would need to control the loading time, set resolved data, and handle error states easily. These tests shaped the core features we designed into the testkit, ensuring it would be both flexible and aligned with real-world usage.
Defining the Requirements
By writing tests as if the testkit already existed, we were able to clearly define the core requirements for an effective solution. These requirements would guide our implementation, ensuring that developers could easily mock Vulcan’s behavior while keeping their tests simple and maintainable. We also found that these needs are likely universal to testkits built for any complex library.
The key requirements we identified were:
- Reasonable Default Behavior: Each query should have sensible defaults, even when mocked, so developers don’t have to define behavior for every single query in each test. For example, if no specific behavior is set, the query could resolve with empty data or enter a default loading state, allowing tests to proceed smoothly without excessive setup.
- Flexible Adjustment of Behaviors: While default behavior is important, developers needed an easy way to adjust behaviors for specific test cases. This included the ability to control the resolved data, simulate errors, and set custom loading times.
- TypeScript Integration: Since we wanted the testkit to integrate seamlessly with TypeScript, we needed to ensure that the utility methods were accessible through type-safe APIs. This meant defining new types in the testkit that extended Vulcan’s regular API, allowing developers to access these utility functions without sacrificing TypeScript’s type checking.
With these requirements in place, we had a clear vision of the testkit’s essential features. Providing both flexible customization and sensible defaults would allow developers to write tests that remained consistent with Vulcan’s production behavior while allowing full control over query states.
Summary
Through prototyping, we identified the core requirements for the testkit: sensible default behaviors, flexible control over query states, and TypeScript integration. With these elements defined, we were ready to move forward and implement the core structure that would bring this design to life. In the next part, we’ll dive into how we built flexible, type-safe components to control the library’s behavior in tests.