Readable Go tests without magic. Inspired by RSpec/Pest
zentests provides fluent, chainable HTTP testing for Fiber applications with RSpec-style syntax.
- No magic: Standard Go testing, no reflection tricks
- Type safe: Strict assertions catch bugs early
- Fiber-native: Built for Fiber, not adapted from net/http
- Chainable: Readable, expressive test code
- Familiar: RSpec/Pest-inspired syntax
- Logger testing:
Loggerinterface +MockLoggerfor injection-friendly log assertions - Route assertions:
HasRoute/NotHasRouteverify registrations without HTTP round-trips - App helpers:
NewAppwith auto-cleanup and optionalfiber.Config - GORM helpers: one-line in-memory DB setup, reset, and generic record creation
- Well-tested: 98.2% code coverage
Simple. I'm using Fiber for my projects and created these tests to make working on my projects faster.
You are welcome to create a version with support for your preferred routing package.
go get github.com/kematzy/zentestsRequirements:
- Go 1.25 or higher
- Fiber v3.x
Fiber v2 Users: If you're using Fiber v2, use the
fiber-v2branch:go get github.com/kematzy/zentests@fiber-v2See Branches for more information.
| Branch | Fiber Version | Go Version | Status |
|---|---|---|---|
master |
v3.x | 1.25+ | Active development |
fiber-v2 |
v2.x | 1.21+ | Maintenance only |
The fiber-v2 branch receives bug fixes and security updates only. New features are developed on master (Fiber v3).
If you're upgrading from Fiber v2 to v3, see the upgrade guide.
package myapp_test
import (
"testing"
"github.com/gofiber/fiber/v3"
"github.com/kematzy/zentests"
)
func TestAPI(t *testing.T) {
zt := zentests.New(t)
app := zentests.NewApp(t) // auto-shutdown via t.Cleanup
// Your routes here...
// Note: Fiber v3 uses fiber.Ctx (interface), not *fiber.Ctx (pointer)
app.Get("/api/health", func(c fiber.Ctx) error {
return c.JSON(fiber.Map{"status": "ok"})
})
// Verify routes are registered (no HTTP request needed)
zt.HasRoute("GET", "/api/health", app)
// Fluent, chainable assertions
zt.Get(app, "/api/health").
OK().
IsJSON().
Has("status", "ok")
}Coverage: 98.2% of statements
All functions are fully documented with comprehensive examples. See our test coverage report for details.
The examples/ directory contains practical examples demonstrating different testing styles:
flat_style.go - Simple Testing
Demonstrates simple, flat-style testing without suites. Perfect for quick API tests.
// Simple, no suite overhead
zt.Get(app, "/api/health").OK().IsJSON().Has("status", "ok")
zt.PostJSON(app, "/api/users", userData).
Created().
Has("id", float64(1)).
Has("name", "John Doe")routes.go - JSON Testing
Shows comprehensive JSON assertions with dot notation for nested objects and arrays.
zt.Get(app, "/api/users").
OK().
IsJSON().
Has("success", true).
Has("data.count", 2).
ArrayLength("data.users", 2).
Has("data.users.0.name", "John") // dot notationsuite.go - BDD Style Testing
Demonstrates BDD-style testing with Describe, It, and lifecycle hooks (BeforeEach, AfterEach).
zt.Describe("User API", func(ctx *zentests.SuiteContext) {
ctx.
BeforeEach(func(zt *zentests.T) {
// Fresh app for each test
app = fiber.New()
SetupRoutes(app)
}).
AfterEach(func(zt *zentests.T) {
// Cleanup
app.Shutdown()
})
ctx.It("returns user list", func(zt *zentests.T) {
zt.Get(app, "/api/users").
OK().
IsJSON().
ArrayLength("data.users", 2)
})
})All methods accept *fiber.App and path, return *Response for chaining:
| Method | Description |
|---|---|
Get(app, path) |
GET request |
Post(app, path, body) |
POST with raw body |
PostJSON(app, path, data) |
POST with JSON body |
PostForm(app, path, data) |
POST with form data |
Put(app, path, body) |
PUT request |
PutJSON(app, path, data) |
PUT with JSON |
Patch(app, path, body) |
PATCH request |
PatchJSON(app, path, data) |
PATCH with JSON |
Delete(app, path) |
DELETE request |
DeleteJSON(app, path, data) |
DELETE with JSON body |
For custom timeout and behavior control, use the *WithConfig variants:
| Method | Description |
|---|---|
GetWithConfig(app, path, cfg) |
GET with custom TestConfig |
PostWithConfig(app, path, body, cfg) |
POST with custom TestConfig |
PostJSONWithConfig(app, path, data, cfg) |
POST JSON with custom TestConfig |
PostFormWithConfig(app, path, data, cfg) |
POST form with custom TestConfig |
PutWithConfig(app, path, body, cfg) |
PUT with custom TestConfig |
PutJSONWithConfig(app, path, data, cfg) |
PUT JSON with custom TestConfig |
PatchWithConfig(app, path, body, cfg) |
PATCH with custom TestConfig |
PatchJSONWithConfig(app, path, data, cfg) |
PATCH JSON with custom TestConfig |
DeleteWithConfig(app, path, cfg) |
DELETE with custom TestConfig |
DeleteJSONWithConfig(app, path, data, cfg) |
DELETE JSON with custom TestConfig |
import (
"time"
"github.com/gofiber/fiber/v3"
"github.com/kematzy/zentests"
)
func TestSlowEndpoint(t *testing.T) {
zt := zentests.New(t)
app := fiber.New()
// Custom timeout for slow endpoints
resp := zt.GetWithConfig(app, "/slow-endpoint", fiber.TestConfig{
Timeout: 10 * time.Second,
})
resp.OK().IsJSON()
// Custom timeout with FailOnTimeout behavior
resp2 := zt.PostJSONWithConfig(app, "/api/users", userData, fiber.TestConfig{
Timeout: 5 * time.Second,
FailOnTimeout: true, // Returns error instead of partial response
})
resp2.Created()
}| Option | Type | Default | Description |
|---|---|---|---|
Timeout |
time.Duration |
1s |
Request timeout duration |
FailOnTimeout |
bool |
true |
Return error on timeout (true) or partial response (false) |
resp.OK() // 200
resp.Created() // 201
resp.Accepted() // 202
resp.NoContent() // 204
resp.BadRequest() // 400
resp.Unauthorized() // 401
resp.Forbidden() // 403
resp.NotFound() // 404
resp.Unprocessable() // 422
resp.ServerError() // 500
resp.Status(code) // Custom statusresp.HasHeader("X-Custom", "value") // Exact match
resp.HeaderContains("X-Rate", "limit") // Substring match
resp.HeaderPresent("X-Auth-Token") // Header exists
resp.HeaderNotPresent("X-Auth-Token") // Header does not exists
resp.HeaderHasValues("X-Features", []string{"audio", "video"}) // Header has multiple values
resp.CookieHasValues(map[string]string{"lang": "en", "theme": "dark"}) // Set-Cookie header test
// Content-Type Helpers
resp.HasContentType("text/html; charset=utf-8") // Checks Content-Type value
// Content-Type Shortcuts
resp.IsJSON() // Content-Type: application/json
resp.IsHTML() // Content-Type: text/html
resp.IsPlainText() // Content-Type: text/plain
resp.IsCSS() // Content-Type: text/css
resp.IsJS() // Content-Type: (application|text)/javascript
resp.IsXML() // Content-Type: (application|text)/xml
resp.IsImage() // Content-Type: image/*
resp.IsPNG() // Content-Type: image/png
resp.IsJPEG() // Content-Type: image/jpeg
resp.IsGIF() // Content-Type: image/gif
resp.IsSVG() // Content-Type: image/svg+xml
resp.IsWebP() // Content-Type: image/webp
// AJAX Detection:
resp.IsXHR() // checks for "XMLHttpRequest" in X-Requested-With headerresp.Contains("substring") // Body contains text
resp.NotContains("substring") // Body excludes text
resp.BodyMatches(`regex\d+`) // Regex match
resp.Equals("exact body") // Exact match
resp.IsEmpty() // Body is emptyDot notation for nested keys: user.profile.name
resp.JSON() // Parse & cache JSON
resp.HasKey("user.name") // Key exists
resp.Has("user.name", "John") // Strict type match
resp.HasString("user.name", "John") // String value
resp.HasInt("user.age", 30) // Integer value (handles JSON float64)
resp.HasFloat("user.score", 9.5) // Float value
resp.HasBool("user.active", true) // Boolean value
resp.MatchesRegex("user.email", `[\w\.]+@`) // Regex match
resp.IsNull("user.deleted_at") // Null check
resp.IsNotNull("user.id") // Not null check
resp.ArrayLength("items", 3) // Array length
// Bulk assertion
resp.Matches(map[string]any{
"success": true,
"data.count": 5,
"data.user.name": "John",
})Verify that routes are (or are not) registered without making an HTTP request.
Both methods are chainable and return *T.
zt := zentests.New(t)
// assert routes are registered
zt.HasRoute("GET", "/users", app).
HasRoute("POST", "/users", app).
HasRoute("DELETE", "/users/:id", app, "delete route must exist")
// assert routes are absent
zt.NotHasRoute("DELETE", "/users", app, "bulk delete must not be exposed").
NotHasRoute("PATCH", "/admin", app)Method matching is case-insensitive ("get", "Get", and "GET" all work).
Path matching is exact against the registered route path — /users/:id matches
the registration, not a concrete request path like /users/42.
Creates a fresh Fiber app with automatic shutdown registered via t.Cleanup.
The app is always stopped when the test finishes, whether it passes or fails —
no defer app.Shutdown() required.
// zero-config — plain Fiber defaults
func (s *MySuite) SetupTest() {
s.app = zentests.NewApp(s.T())
s.app.Get("/ping", handler)
s.zt = zentests.New(s.T())
}
// with views engine — same automatic cleanup, full Fiber config
func (s *MySuite) SetupTest() {
engine := html.New("./views", ".html")
// register any custom view helpers here
s.app = zentests.NewApp(s.T(), fiber.Config{
Views: engine,
AppName: "MyApp",
PassLocalsToViews: true,
})
}The variadic config parameter means the zero-config form NewApp(t) is
unchanged — the optional fiber.Config is only provided when needed.
zentests provides a set of opinionated GORM helpers that reduce database test
setup to single lines. They use an in-memory SQLite database — no files, no
external services, no state leaking between tests.
Requirements: CGO must be enabled (required by the SQLite driver).
// Open an empty in-memory DB — auto-closed via t.Cleanup
db := zentests.SetupTestDB(t)
// Open + AutoMigrate in one call (most common)
db := zentests.SetupTestDBWithModels(t, &User{}, &Post{})
// Migrate additional models onto an existing connection
zentests.DBMigrate(t, db, &Comment{})// Delete all rows from every table; preserve schema and reset sequences.
// Use in SetupTest() when a suite shares one DB across tests.
func (s *MySuite) SetupTest() {
zentests.DBReset(s.T(), s.db)
}// Insert one record — returns the same pointer with ID and timestamps filled.
user := zentests.DBCreate(t, db, &User{Name: "Alice", Email: "alice@example.com"})
s.NotZero(user.ID)
// Insert N records via a 1-based factory function.
// Index i goes 1, 2, … count — no i+1 offset needed.
users := zentests.DBCreateN(t, db, 5, func(i int) User {
return User{Email: fmt.Sprintf("user%d@example.com", i)}
})
// → user1@example.com … user5@example.com
s.Len(users, 5)// t.Cleanup closes the connection automatically after every test.
// Use CloseTestDB only when you need to close mid-test.
zentests.CloseTestDB(t, db)type UserSuite struct {
suite.Suite
db *gorm.DB
zt *zentests.T
}
func (s *UserSuite) SetupSuite() {
// Migrate once for all tests in the suite.
s.db = zentests.SetupTestDBWithModels(s.T(), &User{}, &Post{})
s.zt = zentests.New(s.T())
}
func (s *UserSuite) SetupTest() {
// Clear data before each test; schema stays intact.
zentests.DBReset(s.T(), s.db)
}
func (s *UserSuite) Test_ListUsers() {
zentests.DBCreateN(s.T(), s.db, 3, func(i int) User {
return User{Name: fmt.Sprintf("User %d", i)}
})
// wire the DB into a Fiber handler, then test via HTTP...
app := zentests.NewApp(s.T())
app.Get("/users", func(c fiber.Ctx) error {
var users []User
s.db.Find(&users)
return c.JSON(users)
})
s.zt.Get(app, "/users").OK().IsJSON().ArrayLength("", 3)
}| Function | Signature | Purpose |
|---|---|---|
SetupTestDB |
(t) *gorm.DB |
In-memory SQLite, silent logger, auto-close |
SetupTestDBWithModels |
(t, ...any) *gorm.DB |
Above + AutoMigrate in one call |
DBMigrate |
(t, db, ...any) |
AutoMigrate on an existing connection |
DBReset |
(t, db) |
DELETE all rows, reset sequences, keep schema |
CloseTestDB |
(t, db) |
Explicit close for mid-test control |
DBCreate[T] |
(t, db, *T) *T |
Insert one record, return with ID populated |
DBCreateN[T] |
(t, db, int, func(int) T) []*T |
Insert N records via 1-based factory |
DBFind[T] |
(t, db, *T, id) *T |
Find record by ID |
DBFindBy[T] |
(t, db, *T, where, args...) *T |
Find record by field condition |
DBFirst[T] |
(t, db, *T) *T |
Get first record by PK |
DBLast[T] |
(t, db, *T) *T |
Get last record by PK |
DBCount[T] |
(t, db, *T, conditions...) int64 |
Count records matching conditions |
DBExists[T] |
(t, db, *T, conditions...) bool |
Assert at least one record exists |
DBNotExists[T] |
(t, db, *T, conditions...) bool |
Assert no records exist |
DBUpdate[T] |
(t, db, *T, id, updates) |
Update record by ID |
DBUpdateBy[T] |
(t, db, *T, where, args, updates) |
Update by where clause |
DBDelete[T] |
(t, db, *T) |
Delete record by instance |
DBDeleteBy[T] |
(t, db, *T, where, args...) int64 |
Delete by where clause |
DBTx |
(t, db, fn) error |
Execute function within transaction |
DBSeed[T] |
(t, db, []*T) |
Load test data from slice of pointers |
DBSeedSlice[T] |
(t, db, []T) |
Load test data from slice of values |
// Find by primary key
user := zentests.DBFind(t, db, &User{}, 1)
s.Equal("Alice", user.Name)
// Find by specific field
user := zentests.DBFindBy(t, db, &User{}, "email = ?", "alice@example.com")
// Get first/last
user := zentests.DBFirst(t, db, &User{})
user := zentests.DBLast(t, db, &User{})
// Count records
count := zentests.DBCount(t, db, &User{})
activeCount := zentests.DBCount(t, db, &User{}, "status = ?", "active")
// Assert existence
zentests.DBExists(t, db, &User{}, "email = ?", "alice@example.com")
zentests.DBNotExists(t, db, &User{}, "status = ?", "deleted")// Update by ID
zentests.DBUpdate(t, db, &User{}, 1, map[string]any{"status": "active"})
// Update by condition
zentests.DBUpdateBy(t, db, &User{}, "status = ?", []any{"pending"}, map[string]any{"processed": true})
// Delete by instance
zentests.DBDelete(t, db, user)
// Delete by condition
deleted := zentests.DBDeleteBy(t, db, &User{}, "status = ?", "deleted")err := zentests.DBTx(t, db, func(tx *gorm.DB) error {
if err := tx.Create(&order).Error; err != nil {
return err
}
return tx.Create(&payment).Error
})
s.NoError(err)// From slice of pointers
users := []*User{
{Name: "Alice", Email: "alice@example.com"},
{Name: "Bob", Email: "bob@example.com"},
}
zentests.DBSeed(t, db, users)
// From slice of values
names := []User{
{Name: "Alice"}, {Name: "Bob"},
}
zentests.DBSeedSlice(t, db, names)func TestUserAPI(t *testing.T) {
zt := zentests.New(t)
zt.Describe("User API", func(ctx *zentests.SuiteContext) {
var app *fiber.App
ctx.BeforeEach(func(zt *zentests.T) {
app = fiber.New()
SetupRoutes(app)
}).AfterEach(func(zt *zentests.T) {
app.Shutdown()
})
ctx.It("lists users", func(zt *zentests.T) {
zt.Get(app, "/users").
OK().
IsJSON().
ArrayLength("data", 2)
})
ctx.It("creates user", func(zt *zentests.T) {
zt.PostJSON(app, "/users", map[string]any{
"name": "John",
}).
Created().
Has("data.name", "John")
})
})
}zentests enforces strict type checking:
// JSON numbers are float64 by default
resp.Has("count", 42) // FAIL: 42 is int, JSON has float64
resp.HasInt("count", 42) // PASS: handles conversion
resp.Has("count", float64(42)) // PASS: exact type match
resp.Has("count", 42.0) // PASS: exact type matchzentests integrates seamlessly with testify/suite for more complex test scenarios requiring suite-level setup/teardown.
package myapp_test
import (
"testing"
"github.com/gofiber/fiber/v3"
"github.com/kematzy/zentests"
"github.com/stretchr/testify/suite"
)
// Define your suite struct
type UserAPISuite struct {
suite.Suite
app *fiber.App
zt *zentests.T
}
// SetupSuite runs ONCE before all tests in the suite
func (s *UserAPISuite) SetupSuite() {
s.app = fiber.New()
s.zt = zentests.New(s.T()) // Initialize zentests.T
// Setup routes
s.app.Get("/users", func(c fiber.Ctx) error {
return c.JSON(fiber.Map{"users": []string{"John", "Jane"}})
})
s.app.Post("/users", func(c fiber.Ctx) error {
return c.Status(201).JSON(fiber.Map{"id": 1, "name": "John"})
})
}
// TearDownSuite runs ONCE after all tests in the suite
func (s *UserAPISuite) TearDownSuite() {
// Cleanup resources (close database, etc.)
}
// SetupTest runs BEFORE EACH test
func (s *UserAPISuite) SetupTest() {
// Reset state before each test
}
// TearDownTest runs AFTER EACH test
func (s *UserAPISuite) TearDownTest() {
// Cleanup after each test
}
// Tests use the zentests.T instance
func (s *UserAPISuite) TestListUsers() {
s.zt.Get(s.app, "/users").
OK().
IsJSON().
ArrayLength("users", 2)
}
func (s *UserAPISuite) TestCreateUser() {
s.zt.PostJSON(s.app, "/users", map[string]any{
"name": "John",
}).
Created().
Has("id", float64(1))
}
// Run the suite
func TestUserAPISuite(t *testing.T) {
suite.Run(t, new(UserAPISuite))
}SetupSuite() ← Runs once before all tests
├── TestA()
│ ├── SetupTest()
│ ├── [test code]
│ └── TearDownTest()
│
└── TestB()
├── SetupTest()
├── [test code]
└── TearDownTest()
TearDownSuite() ← Runs once after all tests
Using the GORM helpers, database suites need no manual teardown:
type DatabaseSuite struct {
suite.Suite
app *fiber.App
zt *zentests.T
db *gorm.DB
}
func (s *DatabaseSuite) SetupSuite() {
// One DB for the whole suite — auto-closed via t.Cleanup.
s.db = zentests.SetupTestDBWithModels(s.T(), &User{})
s.zt = zentests.New(s.T())
}
func (s *DatabaseSuite) SetupTest() {
// Wipe all rows before each test; keep the schema.
zentests.DBReset(s.T(), s.db)
// Fresh app wired to the shared DB — auto-shutdown via t.Cleanup.
s.app = zentests.NewApp(s.T())
s.app.Get("/users", func(c fiber.Ctx) error {
var users []User
s.db.Find(&users)
return c.JSON(users)
})
}
// No TearDownSuite needed — t.Cleanup handles DB close and app shutdown.
func (s *DatabaseSuite) Test_ListUsers_Empty() {
s.zt.Get(s.app, "/users").OK().IsJSON()
}
func (s *DatabaseSuite) Test_ListUsers_WithRecords() {
zentests.DBCreateN(s.T(), s.db, 3, func(i int) User {
return User{Name: fmt.Sprintf("User %d", i)}
})
s.zt.Get(s.app, "/users").OK().IsJSON().ArrayLength("", 3)
}// Base suite with common setup
type BaseSuite struct {
suite.Suite
App *fiber.App
ZT *zentests.T
}
func (s *BaseSuite) SetupSuite() {
s.App = fiber.New()
s.ZT = zentests.New(s.T())
}
// UserAPISuite inherits BaseSuite
type UserAPISuite struct {
BaseSuite
}
func (s *UserAPISuite) SetupSuite() {
s.BaseSuite.SetupSuite()
// Add user-specific routes
s.App.Get("/users", s.listUsers)
}
func TestUserAPISuite(t *testing.T) {
suite.Run(t, new(UserAPISuite))
}func (s *SlowAPISuite) TestSlowEndpoint() {
// Use WithConfig for slow endpoints
s.zt.GetWithConfig(s.app, "/slow-endpoint", fiber.TestConfig{
Timeout: 10 * time.Second,
}).OK()
}zentests exports a Logger interface with three methods:
type Logger interface {
Debugf(format string, v ...any)
Warnf(format string, v ...any)
Fatalf(format string, v ...any)
}Declare this type in your own code wherever you need injectable logging:
type MyService struct {
log zentests.Logger
}A production-ready implementation that writes to stderr with level prefixes
(DEBUG:, WARN:, FATAL:). Fatalf calls os.Exit(1) after logging.
var logger zentests.Logger = zentests.DefaultLogger{}
logger.Warnf("retrying in %d seconds", 5)A test double that captures every call without producing any output and without
calling os.Exit. Inject *MockLogger into the code under test, then inspect
its exported fields:
func (s *MySuite) Test_Warns_On_Missing_Config() {
ml := &zentests.MockLogger{}
svc := NewMyService(ml)
svc.Start() // triggers Warnf internally
s.True(ml.WarnCalled)
s.Equal("config not found", ml.WarnMsg)
}| Field | Type | Set by |
|---|---|---|
DebugCalled |
bool |
Debugf |
WarnCalled |
bool |
Warnf |
FatalCalled |
bool |
Fatalf |
DebugMsg |
string |
Debugf — last formatted message |
WarnMsg |
string |
Warnf — last formatted message |
FatalMsg |
string |
Fatalf — last formatted message |
Fatalf on MockLogger records the call and returns — it does not call
os.Exit, making fatal-path tests safe.
| Feature | zentests.Describe |
testify/suite |
|---|---|---|
| Setup | BeforeEach |
SetupTest |
| Teardown | AfterEach |
TearDownTest |
| Suite setup | Manual | SetupSuite |
| Suite teardown | Manual | TearDownSuite |
| Parallel tests | Limited | Supported |
| Subtests | It() |
s.Run() |
| Assertions | zentests.T |
s.T() + s.zt |
| Best for | Quick BDD tests | Complex test setups |
If you're upgrading from Fiber v2 to v3, here are the key changes affecting zentests:
// Fiber v2
import "github.com/gofiber/fiber/v2"
// Fiber v3
import "github.com/gofiber/fiber/v3"Fiber v3 uses fiber.Ctx as an interface (not a pointer):
// Fiber v2
app.Get("/route", func(c *fiber.Ctx) error {
return c.SendString("hello")
})
// Fiber v3
app.Get("/route", func(c fiber.Ctx) error {
return c.SendString("hello")
})The BodyParser method has been replaced with the new Bind() API:
// Fiber v2
app.Post("/users", func(c *fiber.Ctx) error {
var data map[string]any
c.BodyParser(&data)
return c.JSON(data)
})
// Fiber v3
app.Post("/users", func(c fiber.Ctx) error {
var data map[string]any
if err := c.Bind().Body(&data); err != nil {
return err
}
return c.JSON(data)
})Fiber v3's app.Test() now accepts TestConfig instead of timeout duration:
// Fiber v2
resp, err := app.Test(req, 5*time.Second)
// Fiber v3
resp, err := app.Test(req) // Uses default 1s timeout
resp, err := app.Test(req, fiber.TestConfig{
Timeout: 5 * time.Second,
})zentests provides *WithConfig methods to expose this functionality:
// Fiber v2 style (still works)
zt.Get(app, "/route")
// Fiber v3 with custom timeout
zt.GetWithConfig(app, "/slow-route", fiber.TestConfig{
Timeout: 10 * time.Second,
})make help # Show this help message
# Testing
make test # Run all tests
make test-verbose # Run all tests with verbose colored output
make test-coverage # Run tests and generate coverage report
# Code Quality
make fmt # Format source code
make lint # Run linter
make tidy # Run tidy on Go modules
make vet # Run `go vet`
make modernize # Run tool to identify & replace old Go code with newer standards.
make modernize-check # Dry-run with diffs to identify old Go code with newer standards.
make check # Run all checks (fmt, vet, lint, check-test-coverage)
make check-test-coverage # Run tests and enforce coverage threshold (default: 90%)
# Release
make release # Release a new tagged version (e.g. make release VERSION=v1.2.3)
make release VERSION=v1.2.3 # Create specific version
# Development
make versions # Show current and all versions
make git-push # Push code to both repositories
# Documentation
make changelog # Generate changelog using git-cliff
make docs # Open documentation in browser
make docs-md # Create Markdown API doc with `gomarkdoc` in `docs/API.md`
make clean # Clean build artifactsContributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Clone your fork:
git clone https://github.com/[yourusername]/zentests.git - Install dependencies:
go mod download - Run tests:
make test,make test-verbose - Check coverage:
make test-coverage
- Code Quality: Maintain the existing code style and documentation standards
- Tests: All new features must include comprehensive tests (aim for 100% coverage)
- Documentation: Update the
README.mdwith any new features or changes - Commits: Use clear, descriptive commit messages
- Compatibility: Ensure changes work with the latest stable Go and Fiber versions
Please use GitHub Issues to report bugs or request features. When reporting bugs, include:
- Go version
- Fiber version
- Minimal code example that reproduces the issue
- Expected vs actual behavior
Authors:
- Kematzy
- Kimi K2.5 model via opencode Zen
- GLM 5 model via opencode Go
Inspiration:
- RSpec - Ruby testing framework that inspired the BDD syntax
- Pest - PHP testing framework with elegant assertion syntax
- testify - Go assertion library used internally
Built with:
Special thanks to the Go and Fiber communities for their excellent tools and documentation.
This code is released under the MIT License.
Copyright (c) 2025 Kematzy
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.