Skip to main content

MEMO-032: Driver Test Consolidation Strategy

Context

Driver-specific tests in pkg/drivers/*/ are currently isolated unit tests that duplicate coverage provided by the unified acceptance testing framework. This creates redundant test execution and maintenance burden.

Current state:

  • 3 driver test files: memstore_test.go, redis_test.go, nats_test.go
  • Combined ~800 lines of test code
  • Mix of functional tests (Set/Get/Delete/Publish/Subscribe) and driver-specific tests (Init/Health/Stop)
  • Acceptance tests already provide comprehensive interface validation across all backends

Problem:

  • Redundant execution: Functional tests run both in isolation (go test ./pkg/drivers/redis) AND via acceptance framework
  • Wasted CI time: Same code paths tested multiple times
  • Coverage gaps: Isolated tests don't capture driver behavior within pattern context
  • Maintenance burden: Changes require updating both isolated tests and acceptance tests

Analysis

Test Coverage Breakdown

MemStore (pkg/drivers/memstore/memstore_test.go - 230 lines)

TestTypeCoverage Status
TestMemStore_SetGetFunctionalREDUNDANT - Covered by tests/acceptance/patterns/keyvalue/basic_test.go::testSetAndGet
TestMemStore_DeleteFunctionalREDUNDANT - Covered by basic_test.go::testDeleteExisting
TestMemStore_TTLFunctionalREDUNDANT - Covered by ttl_test.go::testTTLExpiration
TestMemStore_CapacityLimitDriver-specificUNIQUE - Tests MemStore-specific max_keys config
TestMemStore_HealthDriver-specificUNIQUE - Tests capacity-based health degradation

Verdict: Keep 2 unique tests, remove 3 redundant tests. 60% redundant.


Redis (pkg/drivers/redis/redis_test.go - 341 lines)

TestTypeCoverage Status
TestRedisPattern_SetGetFunctionalREDUNDANT - Covered by basic_test.go::testSetAndGet
TestRedisPattern_SetWithTTLFunctionalREDUNDANT - Covered by ttl_test.go::testSetWithTTL
TestRedisPattern_GetNonExistentFunctionalREDUNDANT - Covered by basic_test.go::testGetNonExistent
TestRedisPattern_DeleteFunctionalREDUNDANT - Covered by basic_test.go::testDeleteExisting
TestRedisPattern_ExistsFunctionalREDUNDANT - Covered by basic_test.go::testExistsTrue/False
TestRedisPattern_NewDriver-specificUNIQUE - Tests name/version metadata
TestRedisPattern_InitializeDriver-specificUNIQUE - Tests initialization with valid/invalid config
TestRedisPattern_HealthDriver-specificUNIQUE - Tests healthy state
TestRedisPattern_HealthUnhealthyDriver-specificUNIQUE - Tests unhealthy state after connection loss
TestRedisPattern_StopDriver-specificUNIQUE - Tests lifecycle cleanup

Verdict: Keep 6 unique tests, remove 5 redundant tests. 45% redundant.


NATS (pkg/drivers/nats/nats_test.go - 571 lines)

TestTypeCoverage Status
TestNATSPattern_PublishSubscribeFunctionalREDUNDANT - Should be in acceptance tests
TestNATSPattern_MultiplePubSubFunctionalREDUNDANT - Basic fire-and-forget behavior
TestNATSPattern_FanoutFunctional⚠️ QUESTIONABLE - Should be in acceptance tests
TestNATSPattern_MessageOrderingFunctional⚠️ QUESTIONABLE - Should be in acceptance tests
TestNATSPattern_UnsubscribeStopsMessagesFunctional⚠️ QUESTIONABLE - Should be in acceptance tests
TestNATSPattern_ConcurrentPublishFunctional⚠️ QUESTIONABLE - Concurrency should be in acceptance
TestNATSPattern_PublishWithMetadataFunctional⚠️ QUESTIONABLE - Metadata handling in acceptance
TestNATSPattern_InitializeDriver-specificUNIQUE - Tests initialization
TestNATSPattern_HealthDriver-specificUNIQUE - Tests healthy state
TestNATSPattern_HealthAfterDisconnectDriver-specificUNIQUE - Tests unhealthy state
TestNATSPattern_InitializeWithDefaultsDriver-specificUNIQUE - Tests default config values
TestNATSPattern_InitializeFailureDriver-specificUNIQUE - Tests error handling
TestNATSPattern_PublishWithoutConnectionDriver-specificUNIQUE - Tests error handling
TestNATSPattern_SubscribeWithoutConnectionDriver-specificUNIQUE - Tests error handling
TestNATSPattern_UnsubscribeNonExistentDriver-specificUNIQUE - Tests error handling
TestNATSPattern_StopWithActiveSubscriptionsDriver-specificUNIQUE - Tests lifecycle cleanup
TestNATSPattern_NameAndVersionDriver-specificUNIQUE - Tests metadata

Verdict: Keep 10 unique tests, migrate 7 questionable tests to acceptance. 41% redundant/questionable.


Overall Statistics

DriverTotal TestsUnique TestsRedundant TestsRedundancy %
MemStore52360%
Redis116545%
NATS1710741%
TOTAL33181545%

Impact: Removing redundant tests eliminates ~400 lines of code and reduces test execution time by ~30-40%.

Migration Strategy

Phase 1: Consolidate Backend-Specific Tests

Create new directory structure:

tests/unit/backends/
├── memstore/
│ ├── memstore_unit_test.go # Capacity, Health, Initialize
├── redis/
│ ├── redis_unit_test.go # Initialize, Health, Stop
└── nats/
├── nats_unit_test.go # Initialize, Health, Stop, Error handling

What goes here:

  • ✅ Initialization/configuration tests
  • ✅ Health check tests (healthy/unhealthy states)
  • ✅ Lifecycle tests (Start/Stop cleanup)
  • ✅ Driver-specific features (MemStore capacity, Redis connection pooling)
  • ✅ Error handling tests (invalid config, connection failures)

What does NOT go here:

  • ❌ Functional interface tests (Set/Get/Delete/Publish/Subscribe)
  • ❌ TTL/expiration tests
  • ❌ Concurrency tests
  • ❌ Any test that validates interface compliance

Rationale: These are true unit tests that validate driver implementation details, not interface conformance.


Phase 2: Remove Redundant Tests from pkg/drivers

Delete redundant tests:

# Remove functional tests from driver packages
git rm pkg/drivers/memstore/memstore_test.go
git rm pkg/drivers/redis/redis_test.go
git rm pkg/drivers/nats/nats_test.go

Coverage strategy:

  • Acceptance tests provide functional coverage
  • Unit tests provide driver-specific coverage
  • CI runs both: make test-unit-backends test-acceptance

Phase 3: Enhance Acceptance Test Coverage

Add missing tests to acceptance suite:

NATS-specific tests to add to tests/acceptance/patterns/consumer/:

  1. Fanout behavior (test_fanout.go):

    func testFanout(t *testing.T, driver interface{}, caps framework.Capabilities) {
    // Multiple subscribers receive same message
    }
  2. Message ordering (test_ordering.go):

    func testMessageOrdering(t *testing.T, driver interface{}, caps framework.Capabilities) {
    // Messages received in publish order
    }
  3. Unsubscribe behavior (test_unsubscribe.go):

    func testUnsubscribeStopsMessages(t *testing.T, driver interface{}, caps framework.Capabilities) {
    // No messages after unsubscribe
    }
  4. Concurrent publish (concurrent_test.go - already exists, verify NATS is included):

    func testConcurrentPublish(t *testing.T, driver interface{}, caps framework.Capabilities) {
    // Concurrent publishers don't interfere
    }
  5. Metadata handling (test_metadata.go):

    func testPublishWithMetadata(t *testing.T, driver interface{}, caps framework.Capabilities) {
    // Metadata preserved (backend-dependent)
    }

Benefit: These tests run against ALL backends (NATS, Kafka, Redis Streams), not just NATS.


Phase 4: Update Build System

Makefile Changes

Before:

test-drivers:
@cd pkg/drivers/memstore && go test -v ./...
@cd pkg/drivers/redis && go test -v ./...
@cd pkg/drivers/nats && go test -v ./...

test-all: test-drivers test-acceptance

After:

# Unit tests for backend-specific behavior
test-unit-backends:
@echo "Running backend unit tests..."
@go test -v ./tests/unit/backends/...

# Acceptance tests run all backends through unified framework
test-acceptance:
@echo "Running acceptance tests..."
@go test -v ./tests/acceptance/patterns/...

# Full test suite
test-all: test-unit-backends test-acceptance

# Coverage with proper coverpkg
test-coverage:
@go test -coverprofile=coverage.out \
-coverpkg=github.com/jrepp/prism-data-layer/pkg/drivers/... \
./tests/unit/backends/... ./tests/acceptance/patterns/...
@go tool cover -func=coverage.out | grep total

CI Workflow Changes

Before (.github/workflows/ci.yml):

- name: Test drivers
run: make test-drivers

- name: Test acceptance
run: make test-acceptance

After:

- name: Unit Tests
run: make test-unit-backends

- name: Acceptance Tests
run: make test-acceptance

- name: Verify Coverage
run: make test-coverage

Coverage Strategy

Coverage Targets

ComponentMinimum CoverageTarget CoverageTested By
Driver Init/Lifecycle90%95%tests/unit/backends/
Interface Methods85%90%tests/acceptance/patterns/
Error Handling80%85%tests/unit/backends/
Concurrent Operations75%80%tests/acceptance/patterns/

Coverage Measurement

Generate coverage including driver code:

go test -coverprofile=coverage.out \
-coverpkg=github.com/jrepp/prism-data-layer/pkg/drivers/... \
./tests/unit/backends/... ./tests/acceptance/patterns/...

go tool cover -html=coverage.out -o coverage.html

Coverage report format:

pkg/drivers/memstore/memstore.go:   92.3% of statements
pkg/drivers/redis/redis.go: 88.7% of statements
pkg/drivers/nats/nats.go: 85.1% of statements
----------------------------------------
TOTAL DRIVER COVERAGE: 88.7%

Enforcement in CI:

COVERAGE=$(go tool cover -func=coverage.out | grep total | awk '{print $3}' | sed 's/%//')
if (( $(echo "$COVERAGE < 85" | bc -l) )); then
echo "❌ Driver coverage ${COVERAGE}% < 85%"
exit 1
fi

Benefits

1. Reduced Test Execution Time

Test SuiteBeforeAfterImprovement
Driver unit tests~15s~5s67% faster
Acceptance tests~45s~45sNo change
Total60s50s17% faster

2. Improved Coverage Quality

Before:

  • Functional tests run in isolation (e.g., Redis tested with miniredis mock)
  • Don't catch integration issues (pattern → driver → backend)
  • Coverage gaps in pattern layer

After:

  • Functional tests run through full stack (pattern → driver → backend)
  • Integration issues caught automatically
  • Complete coverage of driver code paths via acceptance tests

3. Reduced Maintenance Burden

Before:

  • Change to interface requires updating:
    • Driver implementation
    • Isolated driver test
    • Acceptance test
    • (3 locations)

After:

  • Change to interface requires updating:
    • Driver implementation
    • Acceptance test
    • (2 locations)

Example: Adding GetWithMetadata(key string) ([]byte, map[string]string, bool, error):

  • Before: Update 3 driver tests + acceptance test = 4 files
  • After: Update acceptance test only = 1 file

4. Better Test Organization

Before (scattered tests):

pkg/drivers/redis/redis_test.go              # Functional + unit tests mixed
tests/acceptance/patterns/keyvalue/basic_test.go # Functional tests

After (clear separation):

tests/unit/backends/redis/redis_unit_test.go      # Driver-specific unit tests
tests/acceptance/patterns/keyvalue/basic_test.go # Interface compliance tests

Clarity: Developers know exactly where to add tests:

  • Driver bug (initialization, health)? → tests/unit/backends/
  • Interface behavior? → tests/acceptance/patterns/

Migration Checklist

Pre-Migration

  • Document current test coverage
  • Identify redundant vs unique tests
  • Create migration plan
  • Get team buy-in

Migration Execution

  • Create tests/unit/backends/ directory structure
  • Migrate MemStore unique tests
    • Capacity limit test
    • Health degradation test
  • Migrate Redis unique tests
    • Initialize with valid/invalid config
    • Health (healthy/unhealthy states)
    • Stop lifecycle cleanup
  • Migrate NATS unique tests
    • Initialize with defaults/failure
    • Health (healthy/unhealthy/disconnected)
    • Error handling (no connection)
    • Stop with active subscriptions
  • Add missing acceptance tests
    • Fanout behavior
    • Message ordering
    • Unsubscribe behavior
    • Concurrent publish (verify NATS included)
    • Metadata handling
  • Remove redundant driver tests
    • Delete pkg/drivers/memstore/memstore_test.go
    • Delete pkg/drivers/redis/redis_test.go
    • Delete pkg/drivers/nats/nats_test.go
  • Update Makefile
    • Add test-unit-backends target
    • Update test-all to include unit backend tests
    • Update test-coverage to include driver coverpkg
  • Update CI workflows
    • Add unit backend test step
    • Add coverage enforcement
  • Verify coverage metrics
    • Run full test suite
    • Generate coverage report
    • Validate >85% driver coverage

Post-Migration

  • Document new test structure in CLAUDE.md
  • Update BUILDING.md with test commands
  • Announce migration in team channel
  • Monitor CI for issues

Risks and Mitigations

Risk 1: Coverage Regression

Risk: Removing isolated tests might reduce coverage if acceptance tests don't hit all code paths.

Mitigation:

  1. Generate coverage report BEFORE migration: make test-coverage > coverage-before.txt
  2. Generate coverage report AFTER migration: make test-coverage > coverage-after.txt
  3. Compare: diff coverage-before.txt coverage-after.txt
  4. If coverage drops >2%, add targeted acceptance tests

Validation:

# Before migration
make test-drivers test-acceptance
go test -coverprofile=before.out -coverpkg=./pkg/drivers/... ./pkg/drivers/... ./tests/acceptance/...
BEFORE=$(go tool cover -func=before.out | grep total | awk '{print $3}')

# After migration
make test-unit-backends test-acceptance
go test -coverprofile=after.out -coverpkg=./pkg/drivers/... ./tests/unit/backends/... ./tests/acceptance/...
AFTER=$(go tool cover -func=after.out | grep total | awk '{print $3}')

echo "Before: $BEFORE"
echo "After: $AFTER"

Risk 2: CI Build Breakage

Risk: Updated Makefile/CI workflows break existing builds.

Mitigation:

  1. Create feature branch: git checkout -b test-consolidation
  2. Migrate incrementally (one driver at a time)
  3. Verify CI passes on each commit
  4. Merge only when all drivers migrated successfully

Rollback Plan:

# If migration fails, revert
git revert HEAD~5..HEAD # Revert last 5 commits
git push origin main

Risk 3: Missing Functional Tests

Risk: Some driver-specific functional behavior not captured in acceptance tests.

Mitigation:

  1. Run both test suites in parallel during migration
  2. Compare test output for divergences
  3. Add missing tests to acceptance suite BEFORE removing isolated tests
  4. Keep isolated tests for 1 sprint, mark as @deprecated, remove in next sprint

Success Metrics

Quantitative

  • Test execution time: Reduced by >15% (60s → 50s)
  • Driver coverage: Maintained at >85%
  • Test code lines: Reduced by ~400 lines (30% reduction)
  • CI build time: Reduced by >2 minutes

Qualitative

  • Clarity: Developers can easily find where to add tests
  • Maintainability: Interface changes require updates in fewer places
  • Confidence: Acceptance tests provide better integration coverage

References


Appendices

Appendix A: Test Mapping

Complete mapping of current tests to new locations:

MemStore

Current TestNew LocationRationale
TestMemStore_SetGetDELETECovered by tests/acceptance/patterns/keyvalue/basic_test.go::testSetAndGet
TestMemStore_DeleteDELETECovered by basic_test.go::testDeleteExisting
TestMemStore_TTLDELETECovered by ttl_test.go::testTTLExpiration
TestMemStore_CapacityLimittests/unit/backends/memstore/memstore_unit_test.goUnique MemStore feature
TestMemStore_Healthtests/unit/backends/memstore/memstore_unit_test.goUnique health degradation

Redis

Current TestNew LocationRationale
TestRedisPattern_Newtests/unit/backends/redis/redis_unit_test.goMetadata validation
TestRedisPattern_Initializetests/unit/backends/redis/redis_unit_test.goConfig validation
TestRedisPattern_SetGetDELETECovered by acceptance tests
TestRedisPattern_SetWithTTLDELETECovered by acceptance tests
TestRedisPattern_GetNonExistentDELETECovered by acceptance tests
TestRedisPattern_DeleteDELETECovered by acceptance tests
TestRedisPattern_ExistsDELETECovered by acceptance tests
TestRedisPattern_Healthtests/unit/backends/redis/redis_unit_test.goHealth check validation
TestRedisPattern_HealthUnhealthytests/unit/backends/redis/redis_unit_test.goUnhealthy state
TestRedisPattern_Stoptests/unit/backends/redis/redis_unit_test.goLifecycle cleanup

NATS

Current TestNew LocationRationale
TestNATSPattern_Initializetests/unit/backends/nats/nats_unit_test.goConfig validation
TestNATSPattern_InitializeWithDefaultstests/unit/backends/nats/nats_unit_test.goDefault config
TestNATSPattern_InitializeFailuretests/unit/backends/nats/nats_unit_test.goError handling
TestNATSPattern_NameAndVersiontests/unit/backends/nats/nats_unit_test.goMetadata validation
TestNATSPattern_Healthtests/unit/backends/nats/nats_unit_test.goHealth check
TestNATSPattern_HealthAfterDisconnecttests/unit/backends/nats/nats_unit_test.goUnhealthy state
TestNATSPattern_UnsubscribeNonExistenttests/unit/backends/nats/nats_unit_test.goError handling
TestNATSPattern_PublishWithoutConnectiontests/unit/backends/nats/nats_unit_test.goError handling
TestNATSPattern_SubscribeWithoutConnectiontests/unit/backends/nats/nats_unit_test.goError handling
TestNATSPattern_StopWithActiveSubscriptionstests/unit/backends/nats/nats_unit_test.goLifecycle cleanup
TestNATSPattern_PublishSubscribeDELETECovered by acceptance tests
TestNATSPattern_MultiplePubSubDELETECovered by acceptance tests
TestNATSPattern_FanoutMIGRATE to tests/acceptance/patterns/consumer/fanout_test.goShould test all backends
TestNATSPattern_MessageOrderingMIGRATE to tests/acceptance/patterns/consumer/ordering_test.goShould test all backends
TestNATSPattern_UnsubscribeStopsMessagesMIGRATE to tests/acceptance/patterns/consumer/unsubscribe_test.goShould test all backends
TestNATSPattern_ConcurrentPublishVERIFY in tests/acceptance/patterns/consumer/concurrent_test.goShould include NATS
TestNATSPattern_PublishWithMetadataMIGRATE to tests/acceptance/patterns/consumer/metadata_test.goShould test all backends

Last updated: 2025-10-14