GoConvey

GoTesting FrameworkBDDWeb UIAssertionsBehavior Driven Development

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 Convey functions
  • 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 So function
  • 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:

  1. Testing Framework: BDD description system using Convey and So
  2. 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)
        })
    })
}