Cacik executes cucumber scenarios with Go functions. Cacik parses Go function comments starting with @cacik to find step definitions.
Create your feature file and steps in a directory.
├── apple.feature
└── steps.go
apple.feature
Feature: My first feature
Scenario: My first scenario
When I have 3 applessteps.go
package main
import (
"context"
"fmt"
)
// IHaveApples handles the step "I have X apples"
// @cacik `^I have (\d+) apples$`
func IHaveApples(ctx context.Context, appleCount int) (context.Context, error) {
fmt.Printf("I have %d apples\n", appleCount)
return ctx, nil
}- Use
// @cacikfollowed by a backtick-enclosed pattern - Use
{type}placeholders for built-in types or custom types - Arguments are automatically converted to the function parameter types
Cacik supports Cucumber-style parameter placeholders:
| Placeholder | Go Type | Description | Example Match |
|---|---|---|---|
{int} |
int |
Integer (positive/negative) | 42, -5 |
{float} |
float64 |
Floating point number | 3.14, -0.5 |
{word} |
string |
Single word (no spaces) | hello, test123 |
{string} |
string |
Double-quoted string | "hello world" |
{any} or {} |
string |
Matches anything | anything here |
{time} |
time.Time |
Time values (zero date) | 14:30, 2:30pm, 14:30 Europe/London |
{date} |
time.Time |
Date values (midnight) | 15/01/2024, 2024-01-15, 15 Jan 2024 |
{datetime} |
time.Time |
Date and time | 2024-01-15 14:30, 2024-01-15T14:30:00Z |
{timezone} |
*time.Location |
Timezone | UTC, Europe/London, +05:30 |
{email} |
string |
Email address | user@example.com, name+tag@domain.org |
{duration} |
time.Duration |
Go duration | 5s, 1h30m, 500ms |
{url} |
*url.URL |
HTTP/HTTPS URL | https://example.com/path?q=1 |
Example:
// @cacik `^I have {int} apples$`
func IHaveApples(ctx context.Context, count int) (context.Context, error) {
fmt.Printf("I have %d apples\n", count)
return ctx, nil
}
// @cacik `^the price is {float}$`
func PriceIs(ctx context.Context, price float64) (context.Context, error) {
fmt.Printf("Price: %.2f\n", price)
return ctx, nil
}
// @cacik `^my name is {word}$`
func NameIs(ctx context.Context, name string) (context.Context, error) {
fmt.Printf("Name: %s\n", name)
return ctx, nil
}
// @cacik `^I say {string}$`
func Say(ctx context.Context, message string) (context.Context, error) {
fmt.Printf("Message: %s\n", message)
return ctx, nil
}
// @cacik `^the meeting is at {time}$`
func MeetingAt(ctx context.Context, t time.Time) (context.Context, error) {
fmt.Printf("Meeting at: %s\n", t.Format("15:04"))
return ctx, nil
}
// @cacik `^the event is on {date}$`
func EventOn(ctx context.Context, d time.Time) (context.Context, error) {
fmt.Printf("Event on: %s\n", d.Format("2006-01-02"))
return ctx, nil
}
// @cacik `^the appointment is at {datetime}$`
func AppointmentAt(ctx context.Context, dt time.Time) (context.Context, error) {
fmt.Printf("Appointment at: %s\n", dt.Format(time.RFC3339))
return ctx, nil
}
// @cacik `^convert to {timezone}$`
func ConvertTo(ctx context.Context, loc *time.Location) (context.Context, error) {
fmt.Printf("Timezone: %s\n", loc.String())
return ctx, nil
}
// @cacik `^user {email} logged in$`
func UserLoggedIn(ctx context.Context, email string) (context.Context, error) {
fmt.Printf("User logged in: %s\n", email)
return ctx, nil
}
// @cacik `^wait for {duration}$`
func WaitFor(ctx context.Context, d time.Duration) (context.Context, error) {
fmt.Printf("Waiting for: %s\n", d)
return ctx, nil
}
// @cacik `^navigate to {url}$`
func NavigateTo(ctx context.Context, u *url.URL) (context.Context, error) {
fmt.Printf("Navigating to: %s\n", u.String())
return ctx, nil
}Feature file:
Feature: Built-in types
Scenario: Using built-in types
Given I have 5 apples
And the price is 19.99
And my name is John
And I say "Hello World"
And the meeting is at 2:30pm
And the event is on 15/01/2024
And the appointment is at 2024-01-15 14:30
And convert to Europe/London
And user john@example.com logged in
And wait for 5s
And navigate to https://example.com/apiAll time-related types parse to Go's time.Time or *time.Location types.
Parses to time.Time with zero date (0001-01-01). Supports optional timezone.
| Format | Examples |
|---|---|
| 24-hour | 14:30, 09:15, 00:00, 23:59 |
| With seconds | 14:30:45, 09:15:00 |
| With milliseconds | 14:30:45.123, 09:15:00.500 |
| 12-hour AM/PM | 2:30pm, 9:15am, 2:30 PM, 12:00am |
| With timezone Z | 14:30Z, 14:30:00Z |
| With timezone offset | 14:30+05:30, 14:30-08:00, 14:30+0530 |
| With IANA timezone | 14:30 Europe/London, 2:30pm America/New_York |
Parses to time.Time at midnight (00:00:00) in local timezone. EU format (DD/MM/YYYY) is the default.
| Format | Examples |
|---|---|
| EU (DD/MM/YYYY) - default | 15/01/2024, 31/12/2024 |
| EU with dashes | 15-01-2024, 31-12-2024 |
| EU with dots | 15.01.2024, 31.12.2024 |
| ISO (YYYY-MM-DD) | 2024-01-15, 2024-12-31 |
| ISO with slashes | 2024/01/15, 2024/12/31 |
| Written (Day Month Year) | 15 Jan 2024, 31 December 2024 |
| Written (Month Day, Year) | Jan 15, 2024, January 15, 2024 |
Combines date and time. Supports optional timezone.
| Format | Examples |
|---|---|
| ISO with space | 2024-01-15 14:30, 2024-01-15 14:30:45 |
| ISO with T | 2024-01-15T14:30, 2024-01-15T14:30:45 |
| With milliseconds | 2024-01-15 14:30:45.123 |
| With AM/PM | 2024-01-15 2:30pm, 15/01/2024 9:00am |
| With timezone Z | 2024-01-15T14:30:00Z |
| With timezone offset | 2024-01-15T14:30:00+05:30, 2024-01-15 14:30-08:00 |
| With IANA timezone | 2024-01-15 14:30 Europe/London, 15/01/2024 2:30pm America/New_York |
Parses to Go's *time.Location.
| Format | Examples |
|---|---|
| UTC | UTC, Z |
| Offset with colon | +05:30, -08:00, +00:00 |
| Offset without colon | +0530, -0800 |
| IANA timezone names | Europe/London, America/New_York, Asia/Tokyo |
string- text valuesint,int8,int16,int32,int64- integer valuesuint,uint8,uint16,uint32,uint64- unsigned integersfloat32,float64- floating point valuesbool- boolean values (see below)time.Time- for{time},{date},{datetime}types*time.Location- for{timezone}typecontext.Context- automatically passed (should be first parameter)
You can also use raw regex patterns with capture groups:
// Using regex capture group instead of {int}
// @cacik `^I have (\d+) apples$`
func IHaveApples(ctx context.Context, count int) (context.Context, error) {
return ctx, nil
}Boolean parameters support human-readable values (case-insensitive):
| Truthy | Falsy |
|---|---|
true |
false |
yes |
no |
on |
off |
enabled |
disabled |
1 |
0 |
Example:
// FeatureToggle handles feature state
// @cacik `^the feature is (enabled|disabled)$`
func FeatureToggle(ctx context.Context, enabled bool) (context.Context, error) {
if enabled {
fmt.Println("Feature is ON")
} else {
fmt.Println("Feature is OFF")
}
return ctx, nil
}Feature: Feature toggles
Scenario: Enable feature
Given the feature is enabled
Scenario: Disable feature
Given the feature is disabledCacik supports custom enum-like types. Define a type based on a primitive and use constants to define allowed values:
package steps
import (
"context"
"fmt"
)
// Define a custom type based on string
type Color string
const (
Red Color = "red"
Blue Color = "blue"
Green Color = "green"
)
// Use {typename} syntax in step definition
// @cacik `^I select {color}$`
func SelectColor(ctx context.Context, c Color) (context.Context, error) {
fmt.Printf("Selected: %s\n", c)
return ctx, nil
}Feature file:
Feature: Color selection
Scenario: Select red
When I select red
Scenario: Select blue
When I select blueThe {color} placeholder is automatically replaced with a regex pattern matching all defined constants. Invalid values are rejected at runtime.
Custom types can be based on any primitive type:
string- e.g.,type Color stringint,int8,int16,int32,int64- e.g.,type Priority intuint,uint8,uint16,uint32,uint64float32,float64bool
For integer types, you can use either the constant name or value:
type Priority int
const (
Low Priority = 1
Medium Priority = 2
High Priority = 3
)
// @cacik `^priority is {priority}$`
func SetPriority(ctx context.Context, p Priority) (context.Context, error) {
fmt.Printf("Priority: %d\n", p)
return ctx, nil
}# Both work:
Given priority is high # matches High constant, p = 3
Given priority is 3 # direct value, p = 3Custom type matching is case-insensitive:
# All these match the Red constant:
When I select red
When I select RED
When I select RedStep functions can have the following signatures:
// Simple function with no arguments
func MyStep() {}
// Function with context
func MyStep(ctx context.Context) (context.Context, error) {}
// Function with captured arguments
func MyStep(ctx context.Context, arg1 int, arg2 string) (context.Context, error) {}
// Function without context but with arguments
func MyStep(count int, name string) error {}
// Function with custom type
func MyStep(ctx context.Context, color Color) (context.Context, error) {}go install github.com/denizgursoy/cacik/cmd/cacik@latestcacikCacik will create the main file:
├── apple.feature
├── main.go
└── steps.go
main.go
package main
import (
runner "github.com/denizgursoy/cacik/pkg/runner"
"log"
)
func main() {
err := runner.NewCucumberRunner().
RegisterStep("^I have (\\d+) apples$", IHaveApples).
Run()
if err != nil {
log.Fatal(err)
}
}To execute scenarios in the feature file, run:
go run .It will print I have 3 apples
Cacik supports Cucumber tag expressions for filtering scenarios. Tags are passed via the --tags command-line flag.
Tag expressions support and, or, not operators and parentheses for complex filtering:
| Expression | Description |
|---|---|
@smoke |
Scenarios tagged with @smoke |
@smoke and @fast |
Scenarios with both @smoke AND @fast |
@gui or @database |
Scenarios with either @gui OR @database |
not @slow |
Scenarios NOT tagged with @slow |
@wip and not @slow |
Scenarios with @wip but NOT @slow |
(@smoke or @ui) and not @slow |
Complex expression with parentheses |
# Run all scenarios
go run .
# Run only @smoke scenarios
go run . --tags "@smoke"
# Run scenarios with both @smoke AND @fast
go run . --tags "@smoke and @fast"
# Run scenarios with @gui OR @database
go run . --tags "@gui or @database"
# Run scenarios that are NOT @slow
go run . --tags "not @slow"
# Complex expression
go run . --tags "(@smoke or @ui) and not @slow"
# Alternative syntax with equals sign
go run . --tags="@smoke and @fast"Tags are inherited following the Gherkin specification:
- Scenario inherits tags from its parent Feature
- Scenario inside a Rule inherits tags from both Feature and Rule
@billing
Feature: Billing
@smoke
Scenario: Quick payment
# This scenario has both @billing and @smoke tags
When I make a payment
Rule: Subscriptions
@subscription
Scenario: Monthly billing
# This scenario has @billing, @subscription tags
When I check my subscription# Matches "Quick payment" (has @billing)
go run . --tags "@billing"
# Matches "Quick payment" (has @smoke)
go run . --tags "@smoke"
# Matches "Monthly billing" (has @subscription)
go run . --tags "@subscription"
# Matches both scenarios (both have @billing from feature)
go run . --tags "@billing"
# Matches "Quick payment" only (needs both @billing AND @smoke)
go run . --tags "@billing and @smoke"You can configure hooks by creating a config function:
package mysteps
import "github.com/denizgursoy/cacik/pkg/models"
func GetConfig() *models.Config {
return &models.Config{
BeforeAll: func() { /* setup */ },
AfterAll: func() { /* teardown */ },
BeforeStep: func() { /* before each step */ },
AfterStep: func() { /* after each step */ },
}
}