Parallel analysis of tech stack, architecture, structure, conventions, testing patterns, integrations, and concerns. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
9.9 KiB
Testing Patterns
Analysis Date: 2026-03-23
Test Framework
Runner:
- Go standard
testingpackage - No third-party test frameworks (no testify, gomega, etc.)
- Config: none beyond standard Go tooling
Assertion Style:
- Manual assertions using
t.Errorfandt.Fatalf(no assertion library) t.Fatalffor fatal precondition failures that should stop the testt.Errorffor non-fatal check failures
Run Commands:
go test -v -coverprofile=coverage.out -coverpkg=./... ./... # All tests with coverage
go test -v -run TestWebhookHandler ./pkg/diunwebhook/ # Single test
go tool cover -func=coverage.out # View coverage by function
go tool cover -html=coverage.out # View coverage in browser
Test File Organization
Location:
- Co-located with source code in
pkg/diunwebhook/
Files:
pkg/diunwebhook/diunwebhook_test.go- All tests (external test packagepackage diunwebhook_test)pkg/diunwebhook/export_test.go- Test-only exports (internal packagepackage diunwebhook)
Naming:
- Test functions:
Test<Function>_<Scenario>(e.g.,TestWebhookHandler_BadRequest,TestDismissHandler_NotFound) - Helper functions: lowercase descriptive names (e.g.,
postTag,postTagAndGetID)
Structure:
pkg/diunwebhook/
├── diunwebhook.go # All production code
├── diunwebhook_test.go # All tests (external package)
└── export_test.go # Test-only exports
Test Structure
External Test Package:
Tests use package diunwebhook_test (not package diunwebhook), which forces testing through the public API only. The production package is imported with an alias:
package diunwebhook_test
import (
diun "awesomeProject/pkg/diunwebhook"
)
Test Initialization:
TestMain resets the database to an in-memory SQLite instance before all tests:
func TestMain(m *testing.M) {
diun.UpdatesReset()
os.Exit(m.Run())
}
Individual Test Pattern: Each test resets state at the start, then performs arrange-act-assert:
func TestDismissHandler_Success(t *testing.T) {
diun.UpdatesReset() // Reset DB
err := diun.UpdateEvent(diun.DiunEvent{Image: "nginx:latest"}) // Arrange
if err != nil {
return
}
req := httptest.NewRequest(http.MethodPatch, "/api/updates/nginx:latest", nil) // Act
rec := httptest.NewRecorder()
diun.DismissHandler(rec, req)
if rec.Code != http.StatusNoContent { // Assert
t.Errorf("expected 204, got %d", rec.Code)
}
m := diun.GetUpdatesMap()
if !m["nginx:latest"].Acknowledged {
t.Errorf("expected entry to be acknowledged")
}
}
Helper Functions:
Test helpers use t.Helper() for proper error line reporting:
func postTag(t *testing.T, name string) (int, int) {
t.Helper()
body, _ := json.Marshal(map[string]string{"name": name})
req := httptest.NewRequest(http.MethodPost, "/api/tags", bytes.NewReader(body))
rec := httptest.NewRecorder()
diun.TagsHandler(rec, req)
return rec.Code, rec.Body.Len()
}
Mocking
Framework: No mocking framework used
Patterns:
- In-memory SQLite database via
InitDB(":memory:")replaces the real database httptest.NewRequestandhttptest.NewRecorderfor HTTP handler testinghttptest.NewServerfor integration-level tests- Custom
failWriterstruct to simulate brokenhttp.ResponseWriter:
type failWriter struct{ http.ResponseWriter }
func (f failWriter) Header() http.Header { return http.Header{} }
func (f failWriter) Write([]byte) (int, error) { return 0, errors.New("forced error") }
func (f failWriter) WriteHeader(_ int) {}
What to Mock:
- Database: use in-memory SQLite (
:memory:) - HTTP layer: use
httptestpackage - ResponseWriter errors: use custom struct implementing
http.ResponseWriter
What NOT to Mock:
- Handler logic (test through the HTTP interface)
- JSON encoding/decoding (test with real payloads)
Fixtures and Factories
Test Data: Events are constructed inline with struct literals:
event := diun.DiunEvent{
DiunVersion: "1.0",
Hostname: "host",
Status: "new",
Provider: "docker",
Image: "nginx:latest",
HubLink: "https://hub.docker.com/nginx",
MimeType: "application/json",
Digest: "sha256:abc",
Created: time.Now(),
Platform: "linux/amd64",
}
Minimal events are also used when only the image field matters:
event := diun.DiunEvent{Image: "nginx:latest"}
Location:
- No separate fixtures directory; all test data is inline in
pkg/diunwebhook/diunwebhook_test.go
Test-Only Exports
File: pkg/diunwebhook/export_test.go
These functions are only accessible to test packages (files ending in _test.go):
func GetUpdatesMap() map[string]UpdateEntry // Convenience wrapper around GetUpdates()
func UpdatesReset() // Re-initializes DB with in-memory SQLite
func ResetTags() // Clears tag_assignments and tags tables
func ResetWebhookSecret() // Sets webhookSecret to ""
Coverage
Requirements: CI warns (does not fail) when coverage drops below 80%
CI Coverage Check:
go test -v -coverprofile=coverage.out -coverpkg=./... ./...
go tool cover -func=coverage.out | tee coverage.txt
cov=$(go tool cover -func=coverage.out | grep total: | awk '{print substr($3, 1, length($3)-1)}')
cov=${cov%.*}
if [ "$cov" -lt 80 ]; then
echo "::warning::Test coverage is below 80% ($cov%)"
fi
View Coverage:
go test -coverprofile=coverage.out -coverpkg=./... ./...
go tool cover -func=coverage.out # Text summary
go tool cover -html=coverage.out # Browser view
CI Pipeline
Platform: Gitea Actions (Forgejo-compatible)
CI Workflow: .gitea/workflows/ci.yml
- Triggers: push to
develop, PRs targetingdevelop - Container: custom Docker image with Go and Node.js
- Steps:
gofmt -l .- Formatting check (fails build if unformatted)go vet ./...- Static analysisgo test -v -coverprofile=coverage.out -coverpkg=./... ./...- Tests with coverage- Coverage threshold check (80%, warning only)
go build ./...- Build verification
Release Workflow: .gitea/workflows/release.yml
- Triggers: manual dispatch with version bump type (patch/minor/major)
- Runs the same build-test job, then creates a Docker image and Gitea release
Missing from CI:
- No frontend build or type-check step
- No frontend test step (no frontend tests exist)
- No linting beyond
gofmtandgo vet
Test Types
Unit Tests:
- Handler tests using
httptest.NewRequest/httptest.NewRecorder - Direct function tests:
TestUpdateEventAndGetUpdates - All tests in
pkg/diunwebhook/diunwebhook_test.go
Concurrency Tests:
TestConcurrentUpdateEvent- 100 concurrent goroutines writing to the database viasync.WaitGroup
Integration Tests:
TestMainHandlerIntegration- Full HTTP server viahttptest.NewServer, tests webhook POST followed by updates GET
Error Path Tests:
TestWebhookHandler_BadRequest- invalid JSON bodyTestWebhookHandler_EmptyImage- missing required fieldTestWebhookHandler_MethodNotAllowed- wrong HTTP methodsTestWebhookHandler_Unauthorized/TestWebhookHandler_WrongToken- auth failuresTestDismissHandler_NotFound- dismiss nonexistent entryTestDismissHandler_EmptyImage- empty path parameterTestUpdatesHandler_EncodeError- broken ResponseWriterTestCreateTagHandler_DuplicateName- UNIQUE constraintTestCreateTagHandler_EmptyName- validation
Behavioral Tests:
TestDismissHandler_ReappearsAfterNewWebhook- acknowledged state resets on new webhookTestDeleteTagHandler_CascadesAssignment- tag deletion cascades to assignmentsTestTagAssignmentHandler_Reassign- reassigning image to different tagTestDismissHandler_SlashInImageName- image names with slashes in URL path
E2E Tests:
- Not implemented
- No frontend tests of any kind (no test runner configured, no test files)
Test Coverage Gaps
Frontend (no tests at all):
frontend/src/App.tsx- main application componentfrontend/src/hooks/useUpdates.ts- polling, acknowledge, tag assignment logicfrontend/src/hooks/useTags.ts- tag CRUD logicfrontend/src/components/ServiceCard.tsx- image name parsing, registry detectionfrontend/src/lib/time.ts- time formatting utilitiesfrontend/src/lib/serviceIcons.ts- icon lookup logic- Priority: Medium (pure utility functions like
getShortName,getRegistry,timeAgowould benefit from unit tests)
Backend gaps:
cmd/diunwebhook/main.go- server startup, graceful shutdown, env var reading (not tested)TagsHandlerandTagByIDHandlermethod-not-allowed paths for unsupported HTTP methodsTagAssignmentHandlerbad request paths (missing image, invalid tag_id)- Priority: Low (main.go is thin; handler edge cases are minor)
Common Patterns
HTTP Handler Testing:
func TestSomeHandler(t *testing.T) {
diun.UpdatesReset()
// arrange: create test data
body, _ := json.Marshal(payload)
req := httptest.NewRequest(http.MethodPost, "/path", bytes.NewReader(body))
rec := httptest.NewRecorder()
// act
diun.SomeHandler(rec, req)
// assert status code
if rec.Code != http.StatusOK {
t.Errorf("expected 200, got %d", rec.Code)
}
// assert response body
var got SomeType
json.NewDecoder(rec.Body).Decode(&got)
}
State Reset Pattern:
Every test calls diun.UpdatesReset() at the start, which re-initializes the in-memory SQLite database. This ensures test isolation without needing parallel-safe fixtures.
Auth Testing Pattern:
diun.SetWebhookSecret("my-secret")
defer diun.ResetWebhookSecret()
// ... test with/without Authorization header
Testing analysis: 2026-03-23