A new step in BDD - Crius

2019-12-23

BDD (behavior-driven development) extends TDD (test-driven development) by describing application behavior before implementation begins. DSLs are often used to express behaviors and expected results in a format close to natural language. For complex business requirements, BDD can be an effective way to keep product, QA, and engineering teams aligned.

Motivation

BDD

A typical BDD process begins with epics, which are then broken down into user stories. Each user story defines acceptance criteria (AC) that describe the expected behavior and test conditions for a feature. Because acceptance criteria are concrete and behavior-oriented, they are a strong foundation for automated tests.

Cucumber, first released in 2008, popularized BDD in software development and influenced many BDD tools that followed. Its core value is a DSL that expresses behavior in natural-language-like clauses such as Given, When, and Then. However, the actual test code is usually kept in step files. Test drivers rely heavily on string pattern matching to connect feature definitions to step implementations, which can create performance issues and make steps harder to manage. As an application grows, defining unique step names also becomes increasingly difficult because many steps share similar wording.

Through years of BDD practice, we found several Cucumber limitations that we wanted to address:

  1. String Pattern Matching
  2. Implicit links between features and steps
  3. Limited support for reusing scenarios or steps

These shortcomings eventually led to the advent of Crius.

What is Crius

Crius is our answer to these Cucumber limitations. Let’s look at an example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
@autorun(test)
@title('User add ${todo} item in todo list')
class AddTodoItem extends Step {

// cucumber-like example table, with JavaScript literals
@examples`
| todo | object |
| 'learning' | { obj: 'literal' } |
| 'crius' | { obj: 'hello' } |
`

// which is equivalent to:
// @examples([{
// todo: 'learning',
// object: { obj: 'literal' }
// }, {
// todo: 'crius',
// object: { obj: 'hello' }
// }])

run() {

// JSX-like declarative feature description
return (
// explicit reference to actions
<Scenario desc='User logs in to the website' action={Login}>
<Given desc='User navigates to the todo list' action={Navigate} />

{ /* Allow Step class AddTodo to be used as an action to promote re-use */ }
<When desc='User types ${todo} in the input field and clicks the "add" button' action={AddTodo} />
<Then desc='User should see the ${todo} item in the todo list' action={CheckTodo} />

{ /* action is optional */ }
<Then desc='Just a description' />
</Scenario>
)
}
}

// Functional Step definition similar to Functional Components in React
const Login = async () => {
// actual code to perform login
};

const Navigate = async () => {
// navigate to todo list
}

// Step is similar to React Component class
class AddTodo extends Step {
async run() {
return (
<>
<TypeTodoText text={this.context.example.todo} />
<SubmitTodo />
</>
);
};
}

const TypeTodoText = async (props) => {
// `props.text` is the todo text.
}

const SubmitTodo = async () => {
// click the "add" button
};

const CheckTodo = async (_, { example }) => {
// check todo item
};

Features of Crius

  • Declarative and expressive DSL - Combines DSL characteristics from Cucumber and React
  • Reusable step definitions - Provides better Step and Scenario composition
  • Step lifecycle - Provides lifecycle hooks for more control over tests
  • Plugin support - Allows custom features to be added easily
  • Test runner agnostic - Compatible with Jest, Mocha, and Jasmine out of the box
  • JavaScript literals for examples - Makes complex example objects easy to define
  • Lightweight - Core source code is less than 17 KB

Compared to Cucumber, Crius offers the following benefits:

  • No string pattern matching - Feature definitions and test steps are connected by explicit references
  • Syntax highlighting in IDEs and diff tools - Feature definitions are written in TSX or JSX
  • Extensibility through lifecycle hooks and plugins - Test workflows can be customized for project needs
  • Static type checking when using TypeScript - IDE support can catch typos and type errors earlier

Conclusion

Cucumber brought expressive DSLs to BDD and helped popularize the practice. Crius aims to improve BDD workflows by providing better composition, explicit relationships between feature definitions and actions, stronger integration with modern development tools, and more extensibility for project-specific test workflows. We believe Crius can improve maintainability, application quality, and collaboration among developers, QA, and product managers. We will continue iterating on Crius with more analysis and collaboration tools.

Crius Repository:

https://github.com/unadlib/crius