Use tools like k6, JMeter, or custom Go benchmarks to test system performance under load.
Example k6 script:
import http from 'k6/http';
import { check, sleep } from 'k6';
export let options = {
vus: 100,
duration: '30s',
};
export default function() {
let res = http.get('http://localhost:8080/api/v1/users');
check(res, {
'status is 200': (r) => r.status === 200,
'response time < 200ms': (r) => r.timings.duration < 200,
});
sleep(1);
}Use Go's built-in benchmarking capability for performance-critical code:
func BenchmarkUserRepository_GetUser(b *testing.B) {
// Setup
db := setupTestDB()
repo := NewUserRepository(db)
// Run the benchmark
for i := 0; i < b.N; i++ {
user, err := repo.GetUser(context.Background(), 1)
if err != nil {
b.Fatal(err)
}
}
}Run benchmarks with:
go test -bench=. ./path/to/packageTests are automatically run in our CI pipeline to ensure code quality.
-
Lint Check: Verify code style and formatting
make lint
-
Unit Tests: Run all unit tests
make test -
Integration Tests: Run integration tests
go test -tags=integration ./test/integration -
Coverage Report: Generate and enforce coverage thresholds
go test -coverprofile=coverage.out ./... go tool cover -func=coverage.out
We recommend setting up pre-commit hooks to run tests before committing:
# Add to .git/hooks/pre-commit
#!/bin/sh
go test ./...
if [ $? -ne 0 ]; then
echo "Tests failed, commit aborted"
exit 1
fi- Arrange-Act-Assert: Structure tests with clear setup, action, and verification
- One Assertion Per Test: Keep tests focused on a single behavior
- Test Edge Cases: Include tests for boundary conditions and error scenarios
- Use Test Helpers: Create helper functions for common test operations
- Descriptive Names: Use clear test names that describe what is being tested
- Flaky Tests: Tests that sometimes pass and sometimes fail
- Test Interdependence: Tests that depend on the state from other tests
- Testing Implementation Details: Focus on testing behavior, not implementation
- Slow Tests: Keep tests fast to encourage frequent running
Use the -v flag for detailed test output:
go test -v ./...Use t.Logf() to add debug information to tests:
func TestSomething(t *testing.T) {
result := functionUnderTest()
t.Logf("Got result: %+v", result)
assert.Equal(t, expected, result)
}Skip tests that aren't ready or are environment-specific:
func TestFeatureInProgress(t *testing.T) {
if !isFeatureEnabled() {
t.Skip("Feature not enabled, skipping test")
}
// Test code...
}Store test data in testdata directories or embedded in test files as constants.
Create test data factories for complex objects:
func createTestUser(t *testing.T) *domain.User {
return &domain.User{
ID: 1,
Name: "Test User",
Email: "test@example.com",
// ...
}
}For integration tests, seed the database with test data:
func seedTestData(t *testing.T, db *sql.DB) {
// Execute SQL to insert test data
_, err := db.Exec(`INSERT INTO users (name, email) VALUES ('Test User', 'test@example.com')`)
require.NoError(t, err)
}Thorough testing is essential to maintain high quality in the DefiFundr codebase. By following the guidelines in this document, we can ensure that our tests are effective, maintainable, and provide confidence in our code.
Remember:
- Write tests for new features and bug fixes
- Run the test suite regularly
- Keep tests fast and reliable
- Use mocks appropriately to isolate components
- Maintain high test coverage for critical components