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 myapp
import "github.com/denizgursoy/cacik/pkg/cacik"
// IHaveApples handles the step "I have X apples"
// @cacik `^I have (\d+) apples$`
func IHaveApples(ctx *cacik.Context, appleCount int) {
ctx.Logger().Info("I have apples", "count", appleCount)
}- 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 |
{bool} |
bool |
Boolean value | true, false, 1, 0 |
{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 |
{uuid} |
string |
UUID (v1-v5) | 550e8400-e29b-41d4-a716-446655440000 |
{ip} |
net.IP |
IPv4 or IPv6 address | 192.168.1.1, ::1 |
{hex} |
int64 |
Hex integer (0x prefix) | 0xFF, 0x1A2B |
{path} |
string |
File/directory path | ./config.yaml, ~/docs/file.txt |
{semver} |
string |
Semantic version | 1.0.0, 2.1.3-beta+build.123 |
{base64} |
[]byte |
Base64-encoded data | SGVsbG8=, dGVzdA== |
{csv} |
[]string |
Comma-separated values | a,b,c, 1,2,3 |
{json} |
string |
JSON object or array | {"key":"value"}, [1,2,3] |
{phone} |
string |
Phone number | +1-555-123-4567, 555-123-4567 |
{percent} |
float64 |
Percentage (divided by 100) | 50%, 99.9%, -10% |
{bigint} |
*big.Int |
Arbitrary-precision integer | 12345678901234567890 |
{regex} |
*regexp.Regexp |
Regular expression | /^\d+$/, /[a-z]+/ |
Example:
// @cacik `^I have {int} apples$`
func IHaveApples(ctx *cacik.Context, count int) {
ctx.Logger().Info("I have apples", "count", count)
}
// @cacik `^the price is {float}$`
func PriceIs(ctx *cacik.Context, price float64) {
ctx.Logger().Info("price set", "price", price)
}
// @cacik `^my name is {word}$`
func NameIs(ctx *cacik.Context, name string) {
ctx.Logger().Info("name set", "name", name)
}
// @cacik `^I say {string}$`
func Say(ctx *cacik.Context, message string) {
ctx.Logger().Info("saying message", "message", message)
}
// @cacik `^the meeting is at {time}$`
func MeetingAt(ctx *cacik.Context, t time.Time) {
ctx.Logger().Info("meeting scheduled", "time", t.Format("15:04"))
}
// @cacik `^the event is on {date}$`
func EventOn(ctx *cacik.Context, d time.Time) {
ctx.Logger().Info("event scheduled", "date", d.Format("2006-01-02"))
}
// @cacik `^the appointment is at {datetime}$`
func AppointmentAt(ctx *cacik.Context, dt time.Time) {
ctx.Logger().Info("appointment scheduled", "datetime", dt.Format(time.RFC3339))
}
// @cacik `^convert to {timezone}$`
func ConvertTo(ctx *cacik.Context, loc *time.Location) {
ctx.Logger().Info("converting timezone", "timezone", loc.String())
}
// @cacik `^user {email} logged in$`
func UserLoggedIn(ctx *cacik.Context, email string) {
ctx.Logger().Info("user logged in", "email", email)
}
// @cacik `^wait for {duration}$`
func WaitFor(ctx *cacik.Context, d time.Duration) {
ctx.Logger().Info("waiting", "duration", d)
}
// @cacik `^navigate to {url}$`
func NavigateTo(ctx *cacik.Context, u *url.URL) {
ctx.Logger().Info("navigating", "url", u.String())
}
// @cacik `^the feature is {bool}$`
func FeatureEnabled(ctx *cacik.Context, enabled bool) {
ctx.Logger().Info("feature state", "enabled", enabled)
}
// @cacik `^resource {uuid}$`
func Resource(ctx *cacik.Context, id string) {
ctx.Logger().Info("resource", "uuid", id)
}
// @cacik `^connect to {ip}$`
func ConnectTo(ctx *cacik.Context, ip net.IP) {
ctx.Logger().Info("connecting", "ip", ip.String())
}
// @cacik `^color code is {hex}$`
func ColorCode(ctx *cacik.Context, val int64) {
ctx.Logger().Info("color", "hex", val)
}
// @cacik `^file at {path}$`
func FileAt(ctx *cacik.Context, p string) {
ctx.Logger().Info("file", "path", p)
}
// @cacik `^version {semver}$`
func Version(ctx *cacik.Context, ver string) {
ctx.Logger().Info("version", "semver", ver)
}
// @cacik `^payload {base64}$`
func Payload(ctx *cacik.Context, data []byte) {
ctx.Logger().Info("payload", "size", len(data))
}
// @cacik `^tags {csv}$`
func Tags(ctx *cacik.Context, tags []string) {
ctx.Logger().Info("tags", "values", tags)
}
// @cacik `^data {json}$`
func Data(ctx *cacik.Context, j string) {
ctx.Logger().Info("json data", "raw", j)
}
// @cacik `^call {phone}$`
func Call(ctx *cacik.Context, phone string) {
ctx.Logger().Info("calling", "phone", phone)
}
// @cacik `^discount {percent}$`
func Discount(ctx *cacik.Context, rate float64) {
ctx.Logger().Info("discount", "rate", rate) // 50% → 0.5
}
// @cacik `^balance {bigint}$`
func Balance(ctx *cacik.Context, bi *big.Int) {
ctx.Logger().Info("balance", "value", bi.String())
}
// @cacik `^match pattern {regex}$`
func MatchPattern(ctx *cacik.Context, re *regexp.Regexp) {
ctx.Logger().Info("pattern", "regex", re.String())
}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/api
And the feature is true
And resource 550e8400-e29b-41d4-a716-446655440000
And connect to 192.168.1.1
And color code is 0xFF
And file at ./config.yaml
And version 2.1.3-beta
And payload SGVsbG8=
And tags smoke,fast,critical
And data {"key":"value"}
And call +1-555-123-4567
And discount 50%
And balance 12345678901234567890
And match pattern /^\d+$/All 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 values (supports hex0xprefix)uint,uint8,uint16,uint32,uint64- unsigned integers (supports hex0xprefix)float32,float64- floating point values (also handles%suffix for percent)bool- boolean values (see below)time.Time- for{time},{date},{datetime}types*time.Location- for{timezone}typenet.IP- for{ip}type[]byte- for{base64}type (base64-decoded)[]string- for{csv}type (comma-split)*big.Int- for{bigint}type (arbitrary-precision integer)*regexp.Regexp- for{regex}type (compiled regular expression)*cacik.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 *cacik.Context, count int) {
// Step implementation
}Use {bool} placeholder for boolean parameters. Accepts human-readable values (case-insensitive):
| Truthy | Falsy |
|---|---|
true |
false |
yes |
no |
on |
off |
enabled |
disabled |
1 |
0 |
t |
f |
Example:
// FeatureToggle handles feature state
// @cacik `^the feature is {bool}$`
func FeatureToggle(ctx *cacik.Context, enabled bool) {
ctx.Logger().Info("feature toggled", "enabled", enabled)
}Feature: Feature toggles
Scenario: Enable feature
Given the feature is enabled
Scenario: Disable feature
Given the feature is disabled
Scenario: Turn on
Given the feature is on
Scenario: Using yes/no
Given the feature is yesCacik supports custom enum-like types. Define a type based on a primitive and use constants to define allowed values:
package steps
import (
"fmt"
"github.com/denizgursoy/cacik/pkg/cacik"
)
// 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 *cacik.Context, c Color) {
ctx.Logger().Info("color selected", "color", c)
}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 *cacik.Context, p Priority) {
ctx.Logger().Info("priority set", "priority", p)
}# 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 do not return anything. Use ctx.Assert() for assertions or ctx.TestingT() for direct test control:
// Simple function with no arguments
func MyStep() {}
// Function with context
func MyStep(ctx *cacik.Context) {}
// Function with captured arguments
func MyStep(ctx *cacik.Context, arg1 int, arg2 string) {}
// Function without context but with arguments
func MyStep(count int, name string) {}
// Function with custom type
func MyStep(ctx *cacik.Context, color Color) {}When a Gherkin step has an attached DataTable, cacik converts it to a cacik.Table and auto-injects it into your step function, just like *cacik.Context.
Feature: User management
Scenario: Create users
Given the following users:
| name | age |
| Alice | 30 |
| Bob | 25 |// @cacik `^the following users:$`
func TheFollowingUsers(ctx *cacik.Context, table cacik.Table) {
for _, row := range table.SkipHeader() {
name := row.Get("name")
age := row.Get("age")
ctx.Logger().Info("user", "name", name, "age", age)
}
}The cacik.Table parameter can appear anywhere in the function signature alongside *cacik.Context and regex capture arguments:
Feature: Inventory
Scenario: Add items with details
Given I have 3 items:
| item | price |
| apple | 1.50 |
| banana | 0.75 |
| cherry | 2.00 |// @cacik `^I have (\d+) items:$`
func IHaveItems(ctx *cacik.Context, count int, table cacik.Table) {
ctx.Logger().Info("items", "count", count)
for _, row := range table.SkipHeader() {
ctx.Logger().Info("item", "name", row.Get("item"), "price", row.Get("price"))
}
}If a step function declares a cacik.Table parameter but the step has no DataTable attached, execution fails with an error.
Table provides two iterators using Go 1.24's range-over-func:
All()- iterates over all rows including the header rowSkipHeader()- iterates over data rows only (skips the first row)
Both return iter.Seq2[int, Row] where the int is a 0-based index.
// Iterate all rows (including header)
for i, row := range table.All() {
fmt.Println(i, row.Cell(0))
}
// Iterate data rows only (skip header)
for i, row := range table.SkipHeader() {
name := row.Get("name")
fmt.Println(i, name)
}| Method | Description |
|---|---|
row.Get(col) |
Lookup by column header name (case-insensitive) |
row.Cell(index) |
Lookup by column index (0-based) |
row.Values() |
Returns all cell values as []string |
row.Len() |
Number of cells in the row |
| Method | Description |
|---|---|
table.Headers() |
Returns column headers (first row values) |
table.Len() |
Total number of rows (including header) |
table.All() |
Iterator over all rows |
table.SkipHeader() |
Iterator over data rows only |
For tables without a meaningful header row, use Cell(index) for positional access:
Feature: Geometry
Scenario: Plot coordinates
Given the coordinates are:
| 10 | 20 |
| 30 | 40 |
| 50 | 60 |// @cacik `^the coordinates are:$`
func Coordinates(table cacik.Table) {
for _, row := range table.All() {
x := row.Cell(0)
y := row.Cell(1)
fmt.Println(x, y)
}
}The *cacik.Context provides logging, assertions, and state management for BDD tests.
func MyStep(ctx *cacik.Context) {
ctx.Logger().Debug("debugging info", "key", "value")
ctx.Logger().Info("informational message")
ctx.Logger().Warn("warning message")
ctx.Logger().Error("error message")
}Store and retrieve values across steps within a scenario via ctx.Data(). All Data methods are safe for concurrent use by multiple goroutines, so you can call Set, Get, and MustGet from goroutines spawned within a step without additional synchronization:
// @cacik `^I have {int} apples$`
func IHaveApples(ctx *cacik.Context, count int) {
ctx.Data().Set("apples", count)
}
// @cacik `^I eat {int} apples$`
func IEatApples(ctx *cacik.Context, eaten int) {
current := ctx.Data().MustGet("apples").(int)
ctx.Data().Set("apples", current - eaten)
}
// @cacik `^I should have {int} apples$`
func IShouldHaveApples(ctx *cacik.Context, expected int) {
actual := ctx.Data().MustGet("apples").(int)
ctx.Assert().Equal(expected, actual, "apple count mismatch")
}All assertions fail immediately (fail-fast behavior). Access assertions via ctx.Assert():
func MyStep(ctx *cacik.Context, value int) {
// Equality
ctx.Assert().Equal(expected, actual, "optional message")
ctx.Assert().NotEqual(a, b)
// Nil checks
ctx.Assert().Nil(value)
ctx.Assert().NotNil(value)
// Boolean
ctx.Assert().True(condition, "message")
ctx.Assert().False(condition)
// Errors
ctx.Assert().NoError(err)
ctx.Assert().Error(err)
ctx.Assert().ErrorContains(err, "substring")
// Collections
ctx.Assert().Contains(slice, element)
ctx.Assert().NotContains(slice, element)
ctx.Assert().Len(collection, expectedLen)
ctx.Assert().Empty(collection)
ctx.Assert().NotEmpty(collection)
// Comparisons
ctx.Assert().Greater(5, 3)
ctx.Assert().GreaterOrEqual(5, 5)
ctx.Assert().Less(3, 5)
ctx.Assert().LessOrEqual(5, 5)
// Zero values
ctx.Assert().Zero(value)
ctx.Assert().NotZero(value)
// Fail immediately
ctx.Assert().Fail("reason")
}For compatibility with Go libraries that expect context.Context:
func MyStep(ctx *cacik.Context) {
// Get the underlying context.Context
stdCtx := ctx.Context()
// Use with libraries
result, err := someLibrary.DoSomething(stdCtx)
ctx.Assert().NoError(err)
// Update the context (for timeouts, cancellation, etc.)
ctx.WithContext(context.WithTimeout(stdCtx, 5*time.Second))
}Each scenario execution gets a unique UUID (v4) via ctx.ID(). This is useful for correlating logs, creating unique test resources, or tagging external systems per scenario:
// @cacik `^I create a test user$`
func CreateTestUser(ctx *cacik.Context) {
username := fmt.Sprintf("test-user-%s", ctx.ID())
ctx.Logger().Info("creating user", "id", ctx.ID(), "username", username)
ctx.Data().Set("username", username)
}When running in parallel, each scenario has its own context with a distinct ID, so there is no risk of collision.
go install github.com/denizgursoy/cacik/cmd/cacik@latestcacikBy default, cacik generates cacik_test.go with func TestCacik(t *testing.T).
Use the --output flag to set a custom file name prefix:
cacik --output billingThis produces billing_test.go with func TestBilling(t *testing.T). The prefix is used to derive both the file name (<prefix>_test.go) and the test function name (Test<CamelCasedPrefix>):
--output value |
File name | Test function |
|---|---|---|
| (default) | cacik_test.go |
TestCacik |
billing |
billing_test.go |
TestBilling |
my_feature |
my_feature_test.go |
TestMyFeature |
user_auth |
user_auth_test.go |
TestUserAuth |
Cacik will detect your package name and create a Go test file:
├── apple.feature
├── cacik_test.go
└── steps.go
cacik_test.go
package myapp
import (
runner "github.com/denizgursoy/cacik/pkg/runner"
"testing"
)
func TestCacik(t *testing.T) {
err := runner.NewCucumberRunner(t).
RegisterStep("^I have (\\d+) apples$", IHaveApples).
Run()
if err != nil {
t.Fatal(err)
}
}Since the step functions are in the same package, they are called directly without an import qualifier. If steps are in a different package, cacik will add the appropriate import and qualifier automatically.
To execute scenarios in the feature file, run:
go test -vEach scenario runs as a Go subtest via t.Run(), so you get standard go test output with per-scenario pass/fail reporting. Assertion failures use t.Fatalf() instead of panicking.
All scenarios run as parallel subtests via t.Parallel(). Concurrency is controlled by Go's built-in -parallel flag (defaults to GOMAXPROCS).
# Default: all scenarios run in parallel (limited by GOMAXPROCS)
go test -v ./...
# Limit to 4 concurrent scenarios
go test -v -parallel 4 ./...
# Run sequentially (one scenario at a time)
go test -v -parallel 1 ./...
# Combine with tags
go test -v -parallel 4 -- --tags "@smoke"Note: -parallel is a native go test flag — no -- separator needed for it. Use -- only to separate cacik-specific flags like --tags.
- Each scenario runs as a
t.Run()subtest that callst.Parallel() - Go's test runner controls how many parallel subtests execute concurrently
- Each scenario runs in complete isolation with its own
*cacik.Context - Background steps are re-executed for each scenario
Each scenario gets its own isolated context:
// @cacik `^I set value to {int}$`
func SetValue(ctx *cacik.Context, val int) {
ctx.Data().Set("value", val) // This is isolated per scenario
}
// @cacik `^the value should be {int}$`
func CheckValue(ctx *cacik.Context, expected int) {
actual := ctx.Data().MustGet("value").(int)
ctx.Assert().Equal(expected, actual)
}Each scenario has its own Data() store, so there's no risk of data leakage between scenarios. The Data store is also thread-safe — if a step spawns goroutines that read or write shared state, concurrent Set, Get, and MustGet calls are protected by an internal sync.RWMutex.
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 test -v
# Run only @smoke scenarios
go test -v -- --tags "@smoke"
# Run scenarios with both @smoke AND @fast
go test -v -- --tags "@smoke and @fast"
# Run scenarios with @gui OR @database
go test -v -- --tags "@gui or @database"
# Run scenarios that are NOT @slow
go test -v -- --tags "not @slow"
# Complex expression
go test -v -- --tags "(@smoke or @ui) and not @slow"
# Alternative syntax with equals sign
go test -v -- --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 test -v -- --tags "@billing"
# Matches "Quick payment" (has @smoke)
go test -v -- --tags "@smoke"
# Matches "Monthly billing" (has @subscription)
go test -v -- --tags "@subscription"
# Matches both scenarios (both have @billing from feature)
go test -v -- --tags "@billing"
# Matches "Quick payment" only (needs both @billing AND @smoke)
go test -v -- --tags "@billing and @smoke"Cacik automatically discovers functions returning *cacik.Config for runtime settings. CLI flags always override config values.
package mysteps
import "github.com/denizgursoy/cacik/pkg/cacik"
// MyConfig returns runtime configuration
func MyConfig() *cacik.Config {
return &cacik.Config{
FailFast: true, // Stop on first failure
NoColor: false, // Colored output (default: true)
DisableLog: false, // Logger (ctx.Logger()) enabled (default: false)
DisableReporter: false, // Reporter output enabled (default: false)
Logger: customLogger, // Custom logger (default: slog)
ReportFile: "report", // Generate HTML report (produces report.html)
AfterRun: myAfterRunHandler, // Callback after all scenarios finish
}
}| Field | Type | Description | CLI Override |
|---|---|---|---|
FailFast |
bool |
Stop execution on first failure | --fail-fast |
NoColor |
bool |
Disable colored output | --no-color |
DisableLog |
bool |
Disable the structured logger (ctx.Logger()) |
--disable-log |
DisableReporter |
bool |
Disable reporter output (feature/scenario/step lines) | --disable-reporter |
Logger |
cacik.Logger |
Custom logger (default: slog to stdout) | - |
ReportFile |
string |
File name (without extension) for the HTML test report | --report-file |
AfterRun |
func(RunResult) |
Callback after all scenarios complete | - |
Multiple config functions are merged (last wins for conflicts).
Cacik can generate a self-contained HTML report after all scenarios complete. The report includes a summary dashboard, per-scenario results with collapsible step details, and step-level timing.
go test -v -- --report-file reportThe --report-file flag accepts a file name without extension; .html is appended automatically. Both --report-file report and --report-file=report forms are supported.
func MyConfig() *cacik.Config {
return &cacik.Config{
ReportFile: "test-results/report",
}
}The CLI flag always overrides the config value. The report is generated after all scenarios finish and before AfterRun is called.
Use Config.AfterRun to run custom logic after all scenarios complete. The callback receives a RunResult containing the complete run results — useful for custom reporting, sending notifications, or uploading results to external systems.
func MyConfig() *cacik.Config {
return &cacik.Config{
AfterRun: func(result cacik.RunResult) {
fmt.Printf("Ran %d scenarios in %s\n", len(result.Scenarios), result.Duration)
for _, s := range result.Scenarios {
if !s.Passed {
fmt.Printf(" FAILED: %s — %s\n", s.Name, s.Error)
}
}
},
}
}The callback runs after the HTML report is generated (if configured) and before Run() returns.
After all scenarios execute, cacik builds a RunResult containing structured data about every scenario and step. This is passed to AfterRun and used by the HTML report generator.
| Field | Type | Description |
|---|---|---|
Scenarios |
[]ScenarioResult |
Result of every executed scenario |
Summary |
ReporterSummary |
Aggregate pass/fail/skip counters |
Duration |
time.Duration |
Total wall-clock time for the entire run |
StartedAt |
time.Time |
When the run started |
| Field | Type | Description |
|---|---|---|
FeatureName |
string |
Name of the parent feature |
RuleName |
string |
Name of the parent rule (empty if not inside a rule) |
Name |
string |
Scenario name from the .feature file |
Tags |
[]string |
Tags including inherited tags from Feature and Rule |
Passed |
bool |
True when all steps passed |
Error |
string |
Error message on failure (empty if passed) |
Duration |
time.Duration |
Wall-clock execution time |
StartedAt |
time.Time |
When the scenario started executing |
Steps |
[]StepResult |
Results of all steps in order (background steps first) |
| Field | Type | Description |
|---|---|---|
Keyword |
string |
Gherkin keyword with trailing space (e.g. "Given ", "When ") |
Text |
string |
Step text after the keyword |
Status |
StepStatus |
Execution outcome: StepPassed, StepFailed, or StepSkipped |
Error |
string |
Error message on failure (empty for passed/skipped) |
Duration |
time.Duration |
Wall-clock execution time (zero for skipped steps) |
StartedAt |
time.Time |
When the step started (zero for skipped steps) |
| Constant | Value | String |
|---|---|---|
StepPassed |
0 |
"passed" |
StepFailed |
1 |
"failed" |
StepSkipped |
2 |
"skipped" |
Cacik automatically discovers functions returning *cacik.Hooks for lifecycle hooks. ALL discovered hooks are executed, sorted by their Order field.
package database
import (
"fmt"
"github.com/denizgursoy/cacik/pkg/cacik"
)
// DatabaseHooks sets up database connection
func DatabaseHooks() *cacik.Hooks {
return &cacik.Hooks{
Order: 10, // Lower = runs first (default: 0)
BeforeAll: func() {
// Setup database connection (runs once before all scenarios)
},
AfterAll: func() {
// Close database connection (runs once after all scenarios)
},
BeforeScenario: func(s cacik.Scenario) {
// Runs before each scenario
fmt.Println("Starting scenario:", s.Name)
},
AfterScenario: func(s cacik.Scenario, err error) {
// Runs after each scenario (always runs, even on failure)
// err is nil on success, non-nil on failure
if err != nil {
fmt.Println("Scenario failed:", s.Name, err)
}
},
BeforeStep: func(s cacik.Step) {
// Runs before each step
fmt.Println("Running step:", s.Keyword+s.Text)
},
AfterStep: func(s cacik.Step, err error) {
// Runs after each step
// err is nil on success, non-nil on failure
if err != nil {
fmt.Println("Step failed:", s.Text, err)
}
},
}
}package api
import "github.com/denizgursoy/cacik/pkg/cacik"
// APIHooks sets up mock API server
func APIHooks() *cacik.Hooks {
return &cacik.Hooks{
Order: 20, // Runs after DatabaseHooks (Order: 10)
BeforeAll: func() {
// Start mock API server (needs database)
},
AfterAll: func() {
// Stop mock API server
},
}
}Scenario and step hooks receive metadata about the currently executing scenario or step:
// cacik.Scenario — passed to BeforeScenario/AfterScenario
type Scenario struct {
Name string // Scenario name (e.g. "User login")
Tags []string // Tags including inherited (e.g. "@smoke", "@auth")
Description string // Optional description text
Keyword string // "Scenario" or "Scenario Outline"
Line int64 // Source file line number
}
// cacik.Step — passed to BeforeStep/AfterStep
type Step struct {
Keyword string // Gherkin keyword with trailing space (e.g. "Given ", "When ")
Text string // Step text after keyword (e.g. "the user is logged in")
Line int64 // Source file line number
}AfterScenario is guaranteed to run even if background steps or scenario steps fail. This makes it safe for cleanup logic (closing connections, resetting state, etc.):
BeforeScenario: func(s cacik.Scenario) {
db.Begin() // start transaction
},
AfterScenario: func(s cacik.Scenario, err error) {
db.Rollback() // always rolls back, even on failure
},- BeforeAll: All hooks execute in
Orderascending (0, 10, 20, ...) - BeforeScenario: All hooks execute in
Orderascending (before each scenario) - BeforeStep: All hooks execute in
Orderascending (before each step) - Step executes
- AfterStep: All hooks execute in
Orderascending (after each step, receives step error) - AfterScenario: All hooks execute in
Orderascending (after each scenario, receives scenario error) - AfterAll: All hooks execute in
Orderascending (after all scenarios)
| Field | Type | Description |
|---|---|---|
Order |
int |
Execution order (lower = first, default: 0) |
BeforeAll |
func() |
Runs once before all scenarios |
AfterAll |
func() |
Runs once after all scenarios |
BeforeScenario |
func(Scenario) |
Runs before each scenario |
AfterScenario |
func(Scenario, error) |
Runs after each scenario (always runs; error is nil on success) |
BeforeStep |
func(Step) |
Runs before each step |
AfterStep |
func(Step, error) |
Runs after each step (error is nil on success) |