In this post, TikTok engineer Shiguang Ai introduces "jest-bdd-generator," a new Jest plugin designed to integrate Behavior-driven development (BDD) with existing testing practices. This tool enhances test readability by allowing developers to incorporate Gherkin-style specifications into their Jest tests. Read on to discover inspiration behind its creation and how it can transform your testing approach.
If you work in frontend, you've probably heard of Jest. Maybe you've also heard of Behavior-driven development (BDD), a different philosophy of testing. In this post, I'll demo a new Jest plugin developed at TikTok that integrates these two paradigms, plus some insights that make jest-bdd-generator different from previous attempts at combining them.
Why use BDD?
Although Jest incorporates some declarative expressions such as describe() and expect(), fundamentally, Jest tests are computer programs that perform operations and check their results. Engineers often write tests in a hurry, distracted by other problems like feature debugging or mocking complex systems. As a result, test code often gets messy. This hinders collaboration with colleagues, managers, or clients who rely on tests to express requirements and assess correctness.
Behavior-driven development doesn't fundamentally change how tests are run; instead, it adds a domain-specific language (DSL) that enables requirements to be described clearly using plain English. The most popular BDD framework is Cucumber, and its syntax is called Gherkin. This "executable specification" ensures that requirements are communicated effectively. Unlike mere documentation, executing the specification guarantees that it is always accurate and up-to-date. It also enables the generation of automated reports and summaries.
Some challenges with adoption
Cucumber provides official support for the JavaScript language, but many frontend codebases have nontrivial existing investments in Jest, a different test framework. Jest has gained widespread adoption through its support for React and ReactNative, easy onboarding from legacy JavaScript test frameworks, and extensive integration with toolchains and code coverage dashboards.
If switching to Cucumber would be too disruptive for your team, an alternative is to extend Jest to run Gherkin tests alongside Jest tests. We experimented with several NPM packages that accomplish this. However, while you get a unified test runner, managing the new format alongside the old can double the complexity of the developer experience. Our teams were reluctant to adopt it.
A new idea: roundtrip syntax transformations
This led us to consider a different approach: What if we implemented the Gherkin paradigm directly in ordinary Jest tests? By doing so, we can superimpose the benefits of BDD without changes to existing test APIs. But how would this work? Since Gherkin specifications are documentation-like, it makes sense to store them as code comments. We can use TypeScript syntax analysis to reintroduce DSL validation and reporting benefits.
As a simple example, suppose you have existing Jest tests for Math functions like ceiling, floor, and round, but are unfamiliar with the Gherkin representation. You can simply add comments like //@Given
, //@When
, //@Then
to indicate these specification roles:
my-project/src/tests/math.test.ts
import { describe, test, expect } from '@jest/globals';
describe('Rounding methods of Math', () => {
test.each([
{ num: 1234.1, method: 'ceil', result: 1235 },
{ num: 1234.9, method: 'ceil', result: 1235 },
{ num: -1234.1, method: 'ceil', result: -1234 },
{ num: -1234.9, method: 'ceil', result: -1234 },
{ num: 1234.1, method: 'floor', result: 1234 },
{ num: 1234.9, method: 'floor', result: 1234 },
{ num: -1234.1, method: 'floor', result: -1235 },
{ num: -1234.9, method: 'floor', result: -1235 },
{ num: 1234.5, method: 'round', result: 1235 },
{ num: 1234.4, method: 'round', result: 1234 },
{ num: -1234.5, method: 'round', result: -1234 },
{ num: -1234.6, method: 'round', result: -1235 }
])('Integer pattern', async ({ num, method, result }) => {
//@Given input number is <num>
expect(typeof num).toBe('number');
//@When rounding method is <method>
expect(Math).toHaveProperty(method);
//@Then rounded number is <result>
expect(Math[method](num)).toEqual(result);
});
});
Generating comment stubs: gen-comments
To help with this, the jest-bdd-generator SDK provides a gen-comments
command that automatically injects stubs for these comments. The shell command is shown below. (Bash's \
operator is used to split the input lines for readability.)
gen-comments pathTestsInput=./src/tests/math.test.ts \
pathOutput=./src/tests/math-commented.test.ts
Jest to Gherkin: gen-doc
From the file shown above, we can use the gen doc
command to generate a Gherkin specification based on our Jest test cases. It is invoked like this:
gen-doc pathTestsInput=./src/tests/math.test.ts \
pathOutput=./docs/features/math.feature
The generated output file is shown below:
my-project/docs/features/math.feature
@format-feature
Feature: Rounding methods of Math
Scenario Outline: Integer pattern
Given input number is <num>
When rounding with <method>
Then rounded number is <result>
Examples:
| num | method | result |
| 1234.1 | "ceil" | 1235 |
| 1234.9 | "ceil" | 1235 |
| -1234.1 | "ceil" | -1234 |
| -1234.9 | "ceil" | -1234 |
| 1234.1 | "floor" | 1234 |
| 1234.9 | "floor" | 1234 |
| -1234.1 | "floor" | -1235 |
| -1234.9 | "floor" | -1235 |
| 1234.5 | "round" | 1235 |
| 1234.4 | "round" | 1234 |
| -1234.5 | "round" | -1234 |
| -1234.6 | "round" | -1235 |
Gherkin to Jest: gen-test
Once engineers gain familiarity with the Gherkin format, they can shift to directly authoring specification files such as math.feature
. Here, jest-bdd-generator can be invoked in the opposite direction, updating Jest files to match the specification. The command line looks like this:
# Save a backup copy of the test file
mv src/tests/math.test.ts src/tests/math-orig.test.ts
# Regenerate the test file
gen-test pathTestsInput=src/tests/math-orig.test.ts \
pathGherkinInput=docs/features/math.feature \
pathOutput=src/tests/math.test.ts
This approach allows engineers to interchangeably work on both math.feature
and math.test.ts
, automatically syncing changes in either direction to ensure that the readable English specification is kept consistent with its program code implementation.
Test reporting
The BDD formalism also provides an opportunity for more readable reporting on what is being tested.
gen-report pathTestsInput=src/tests/math-orig.test.ts \
pathFeatureInput=docs/features/math.feature
The output is a Cucumber report detailing the test results, including call stacks and diagnostic details for any failed tests:
Give behavior driven development a try!
This provides a quick overview of the concept and motivation behind jest-bdd-generator. More detailed usage instructions can be found on the jest-bdd-generator project website, including other interesting features such as the experimental Test Oracle server. If you have any questions, you can create an issue in our GitHub repository: https://github.com/tiktok/jest-bdd-generator