GoConvey
GoConvey
Overview
GoConvey is a BDD (Behavior-Driven Development) style testing framework for Go. While fully integrating with the standard go test, it provides readable test descriptions, real-time Web UI, and rich assertion capabilities. Through nested descriptions using the Convey function, it clearly expresses test intentions and significantly improves the developer experience as a modern testing solution.
Details
Key Features
- BDD Style Descriptions: Highly readable nested tests using
Conveyfunctions - Real-time Web UI: Browser-based visualization of test results and coverage
- go test Integration: Full compatibility with standard testing tools
- Rich Assertions: Expressive assertions using the
Sofunction - Automatic Test Execution: Automatic test re-execution on file changes
- Colorful Console: Readable color-coded console output
- Test Code Generator: Efficient test code generation functionality
- Desktop Notifications: Optional test result notifications
Architecture
GoConvey consists of two main components:
- Testing Framework: BDD description system using
ConveyandSo - Web UI: Real-time test monitoring and reporting functionality
Core Functions
- Convey Function: Test scope declaration and hierarchical organization
- So Function: Assertion execution
- Reset Function: Test cleanup registration
- FailureMode: Control behavior on test failures
Pros and Cons
Pros
- High Readability: Test descriptions close to natural language
- Excellent Developer Experience: Intuitive feedback through Web UI
- Standard Compatibility: Can be used alongside existing Go tests
- Efficient Workflow: Improved development efficiency with automatic test execution
- Comprehensive Reports: Test coverage and performance analysis
- Low Learning Curve: Intuitive API design
Cons
- External Dependencies: Dependencies beyond the standard library
- Performance Overhead: Minor execution cost due to nested structure
- Web UI Dependency: Full functionality requires a web browser
- Verbosity for Complex Tests: May be excessive for simple tests
References
Code Examples
Basic Testing
package main
import (
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func Add(a, b int) int {
return a + b
}
func TestAdd(t *testing.T) {
Convey("Given two integers", t, func() {
a := 2
b := 3
Convey("When they are added together", func() {
result := Add(a, b)
Convey("The result should be their sum", func() {
So(result, ShouldEqual, 5)
})
})
})
}
Nested Test Structure
func TestCalculator(t *testing.T) {
Convey("Calculator functionality", t, func() {
Convey("Addition operations", func() {
Convey("Adding positive numbers", func() {
So(Add(2, 3), ShouldEqual, 5)
So(Add(10, 15), ShouldEqual, 25)
})
Convey("Adding negative numbers", func() {
So(Add(-2, -3), ShouldEqual, -5)
So(Add(-10, 5), ShouldEqual, -5)
})
Convey("Adding zero", func() {
So(Add(0, 5), ShouldEqual, 5)
So(Add(5, 0), ShouldEqual, 5)
})
})
Convey("Multiplication operations", func() {
Convey("Multiplying positive numbers", func() {
So(Multiply(2, 3), ShouldEqual, 6)
So(Multiply(4, 5), ShouldEqual, 20)
})
Convey("Multiplying by zero", func() {
So(Multiply(5, 0), ShouldEqual, 0)
So(Multiply(0, 10), ShouldEqual, 0)
})
})
})
}
Rich Assertions
func TestAssertions(t *testing.T) {
Convey("Various assertion types", t, func() {
Convey("Equality assertions", func() {
So(42, ShouldEqual, 42)
So(3.14, ShouldAlmostEqual, 3.14159, 0.01)
So("hello", ShouldEqual, "hello")
})
Convey("Boolean assertions", func() {
So(true, ShouldBeTrue)
So(false, ShouldBeFalse)
So(1 > 0, ShouldBeTrue)
})
Convey("Nil assertions", func() {
var ptr *int
So(ptr, ShouldBeNil)
value := 42
ptr = &value
So(ptr, ShouldNotBeNil)
})
Convey("Collection assertions", func() {
slice := []int{1, 2, 3, 4, 5}
So(slice, ShouldContain, 3)
So(slice, ShouldHaveLength, 5)
So(slice, ShouldNotBeEmpty)
})
Convey("Numeric assertions", func() {
So(10, ShouldBeGreaterThan, 5)
So(3, ShouldBeLessThan, 10)
So(5, ShouldBeBetween, 1, 10)
})
Convey("String assertions", func() {
So("hello world", ShouldContainSubstring, "world")
So("hello", ShouldStartWith, "hel")
So("world", ShouldEndWith, "rld")
So("[email protected]", ShouldMatchRegex, `\w+@\w+\.\w+`)
})
Convey("Type assertions", func() {
So(42, ShouldHaveSameTypeAs, 0)
So("string", ShouldHaveSameTypeAs, "")
})
})
}
Setup and Cleanup
func TestWithSetup(t *testing.T) {
Convey("Database operations", t, func() {
// Setup
db := setupTestDatabase()
user := createTestUser()
// Register cleanup function
Reset(func() {
cleanupTestUser(user)
cleanupTestDatabase(db)
})
Convey("Creating a user", func() {
err := db.CreateUser(user)
So(err, ShouldBeNil)
Convey("Should find the user by ID", func() {
foundUser, err := db.FindUserByID(user.ID)
So(err, ShouldBeNil)
So(foundUser.Name, ShouldEqual, user.Name)
})
Convey("Should find the user by email", func() {
foundUser, err := db.FindUserByEmail(user.Email)
So(err, ShouldBeNil)
So(foundUser.ID, ShouldEqual, user.ID)
})
})
Convey("Updating a user", func() {
// Create existing user
db.CreateUser(user)
Convey("Should update user name", func() {
newName := "Updated Name"
err := db.UpdateUserName(user.ID, newName)
So(err, ShouldBeNil)
updatedUser, _ := db.FindUserByID(user.ID)
So(updatedUser.Name, ShouldEqual, newName)
})
})
})
}
Custom Assertions
// Creating custom assertions
func ShouldBeValidEmail(actual interface{}, expected ...interface{}) string {
email, ok := actual.(string)
if !ok {
return "Expected string type for email validation"
}
emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
if !emailRegex.MatchString(email) {
return fmt.Sprintf("Expected '%s' to be a valid email address", email)
}
return "" // Success
}
func TestCustomAssertion(t *testing.T) {
Convey("Email validation", t, func() {
Convey("Valid emails should pass", func() {
So("[email protected]", ShouldBeValidEmail)
So("[email protected]", ShouldBeValidEmail)
})
Convey("Invalid emails should fail", func() {
So("invalid-email", ShouldNotSatisfy, ShouldBeValidEmail)
So("@domain.com", ShouldNotSatisfy, ShouldBeValidEmail)
})
})
}
Error Handling
func TestErrorHandling(t *testing.T) {
Convey("Error handling scenarios", t, func() {
Convey("Function that should return an error", func() {
result, err := functionThatShouldFail()
So(err, ShouldNotBeNil)
So(result, ShouldBeNil)
So(err.Error(), ShouldContainSubstring, "expected error message")
})
Convey("Function that should not return an error", func() {
result, err := functionThatShouldSucceed()
So(err, ShouldBeNil)
So(result, ShouldNotBeNil)
})
Convey("Specific error types", func() {
_, err := functionWithSpecificError()
So(err, ShouldHaveSameTypeAs, &CustomError{})
So(err, ShouldImplement, (*error)(nil))
})
})
}
Advanced Testing Techniques
func TestAdvancedFeatures(t *testing.T) {
Convey("Advanced GoConvey features", t, func() {
// Skip tests
SkipConvey("This test is skipped", func() {
So(1, ShouldEqual, 2) // Not executed
})
// Focus tests (skip other tests)
FocusConvey("Only this test runs", func() {
So(true, ShouldBeTrue)
})
Convey("Failure modes", func() {
// Control behavior on failure
Convey("Continue on failure", func(c C) {
c.Convey("First assertion", FailureContinues, func() {
So(1, ShouldEqual, 2) // Fails but continues
})
c.Convey("Second assertion", func() {
So(true, ShouldBeTrue) // Executed
})
})
})
Convey("Using context", func() {
ctx := context.WithValue(context.Background(), "key", "value")
So(ctx.Value("key"), ShouldEqual, "value")
So(ctx.Value("missing"), ShouldBeNil)
})
})
}