Skip to content

How to Write Tests

Test file naming

  • Test files must end with _test.go
  • Place test files in the same package as the code being tested
  • Example: hash.gohash_test.go

Test function naming

// Pattern: Test<FunctionName>_<Scenario>
func TestGenerateHashPassword_ValidInput(t *testing.T) { }
func TestLogin_InvalidCredentials(t *testing.T) { }
func TestUser_HasRole_MultipleRoles(t *testing.T) { }

Using test helpers

Database setup

func TestMyFunction(t *testing.T) {
    // Setup in-memory SQLite database
    db := testhelpers.SetupTestDBWithGlobalDB(t)

    // Database is automatically cleaned up after test
    // model.DB is set for code that uses the global DB
}

Creating test users

// Create user with role
user := testhelpers.CreateTestUser(t, db, "test@example.com", "Parent")

// Create user with password
user := testhelpers.CreateTestUserWithPassword(t, db, "test@example.com", "password123", "Admin")

Generating JWT tokens

secret := testhelpers.GetTestSecret()
os.Setenv("APP_SECRET", secret)
defer os.Unsetenv("APP_SECRET")

// Generate valid token
token := testhelpers.GenerateTestToken(t, secret, userID, email)

// Generate expired token
expiredToken := testhelpers.GenerateExpiredToken(t, secret, userID, email)

Using testify

import (
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
)

func TestExample(t *testing.T) {
    // assert - test continues on failure
    assert.Equal(t, expected, actual, "optional message")
    assert.NotNil(t, obj)
    assert.True(t, condition)
    assert.Contains(t, haystack, needle)

    // require - test stops on failure (use for prerequisites)
    require.NoError(t, err, "this is critical")
    require.NotNil(t, user, "user must exist")
}

Table-driven tests

func TestHashPassword_Variants(t *testing.T) {
    tests := []struct {
        name     string
        password string
        wantErr  bool
    }{
        {
            name:     "valid password",
            password: "SecurePass123!",
            wantErr:  false,
        },
        {
            name:     "empty password",
            password: "",
            wantErr:  false,
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            hash, err := GenerateHashPassword(tt.password)

            if tt.wantErr {
                require.Error(t, err)
                return
            }

            require.NoError(t, err)
            assert.NotEmpty(t, hash)
        })
    }
}

Testing Gin handlers

func TestLoginHandler(t *testing.T) {
    // Setup
    gin.SetMode(gin.TestMode)
    w := httptest.NewRecorder()
    c, _ := gin.CreateTestContext(w)

    // Create request
    form := url.Values{}
    form.Add("Email", "test@example.com")
    form.Add("Password", "password")

    req, _ := http.NewRequest(http.MethodPost, "/login",
        strings.NewReader(form.Encode()))
    req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
    c.Request = req

    // Execute
    Login(c)

    // Assert
    assert.Equal(t, http.StatusOK, w.Code)
}

Conventions

Test independence

  • Each test should be independent and self-contained
  • Use t.Cleanup() or defer for teardown
  • Don't rely on test execution order
  • Avoid global state when possible

Arrange-Act-Assert pattern

func TestExample(t *testing.T) {
    // Arrange - setup test data
    user := CreateTestUser(...)

    // Act - execute the code under test
    result := user.HasRole("Admin")

    // Assert - verify the outcome
    assert.True(t, result)
}

Test edge cases

Always test:

  • Happy path (valid input)
  • Invalid input
  • Empty/nil values
  • Boundary conditions
  • Error scenarios
  • Domain-specific edge cases

Use require for prerequisites

func TestExample(t *testing.T) {
    user, err := CreateUser(...)
    require.NoError(t, err)  // Stop if creation fails
    require.NotNil(t, user)  // Stop if user is nil

    // Now safe to continue
    assert.Equal(t, "test@example.com", user.Email)
}

Writing benchmarks

func BenchmarkGenerateHashPassword(b *testing.B) {
    password := "BenchmarkPassword123!"

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        _, _ = GenerateHashPassword(password)
    }
}

Best practices

  1. Write tests first (TDD) or immediately after implementing features
  2. Keep tests simple — one logical assertion per test when possible
  3. Use table-driven tests for multiple similar scenarios
  4. Mock external dependencies (databases, APIs)
  5. Test error handling as thoroughly as success paths
  6. Maintain test coverage above 70% for critical code
  7. Run tests before committing code changes
  8. Keep tests fast — use in-memory databases, avoid sleeps
  9. Document complex test scenarios with comments
  10. Review test coverage reports regularly