This document provides an overview of the testing approach used in BlockchainDB, evaluating what works well and what could be improved.
BlockchainDB uses Go's standard testing framework along with the testify package for assertions. The tests are primarily focused on functional verification and performance benchmarking of the various components.
The repository contains the following test files:
bfile_test.go- Buffered File componentbloom_test.go,bloomset_test.go- Bloom filter and the layered, persisted filter built on itconcurrency_test.go- Concurrent access toKVShardandKVViewcrash_test.go- SIGKILL injection: the durability contract in durability.md, enforced by killing a child process mid-write and verifying every checkpointed keydyna_test.go- The mutable (Dyna) layer: compaction, live-tail bounds, and crash-midway recoveryfastrandom_test.go- The deterministic generator used to build test datakeys_test.go- Key record marshallingkv_2_test.go- The two-layerKV2storekv_shard_test.go,kv_shard_writer_test.go- The sharded store and its multi-core ingest pathkv_test.go- End-to-end key/value round tripsloadtest_test.go- The-loadflag that gates the multi-GB load testsprofile_test.go,kv2_profile_test.go- Profiling harnessesreopen_test.go- The create -> write -> close -> open -> read round tripsegment_test.go- Block export/import between nodessegstore_test.go,segstore_bench_test.go-SegmentStorebehavior and its cost measurementsview_kv_test.go- The View KV implementation
The codebase includes several testing utilities:
- MakeDir - Creates a temporary directory for testing and returns a function to clean it up
- MakeFilename - Creates a temporary file for testing
- FastRandom - A deterministic random number generator for creating test data
Each major component of BlockchainDB has dedicated test files that verify its functionality. This ensures that individual components work as expected before they're integrated.
Many tests include performance measurements, reporting metrics like operations per second. This helps track performance characteristics and identify regressions.
Example from kv_test.go:
wps := cntWrites / time.Since(start).Seconds()
rps := cntReads / time.Since(start).Seconds()
fmt.Printf("Writes per second %10.3f Reads per second %10.3f\n", wps, rps)Tests verify data integrity by writing values and then reading them back to ensure they match. This is crucial for a database system.
Example from kv_test.go:
value2, err := kv.Get(key)
assert.NoError(t, err, "Failed to put")
assert.Equal(t, value, value2, "Didn't the the value back")Some tests check edge cases such as:
- Reading after closing and reopening files
- Handling non-existent keys
- Testing compression operations
- Verifying behavior with large datasets
Tests use defer statements with cleanup functions to ensure test resources are properly released, even if tests fail.
Example:
dir, rm := MakeDir()
defer rm()The repository doesn't appear to include test coverage reporting. Adding coverage analysis would help identify untested code paths.
Recommendation:
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.outThe tests directly interact with the filesystem, which can make tests slower and less reliable. Introducing interfaces and mocks for external dependencies would improve test isolation.
Many tests could benefit from a table-driven approach, which would make it easier to test multiple scenarios with less code duplication.
Example improvement:
func TestBloom(t *testing.T) {
testCases := []struct {
name string
numEntries int
falsePositive float64
}{
{"small", 1000, 0.1},
{"medium", 10000, 0.05},
{"large", 100000, 0.01},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
bloom := NewBloom(4.5)
fr := NewFastRandom([]byte{1})
// Test implementation...
})
}
}Several tests measure performance by reporting rates, which is useful
for tracking but noisy. Formal Go benchmarks using the testing.B
type are used for lookups (BenchmarkSegmentStoreGet) but not for
writes; adding them would give more consistent measurements.
Example:
func BenchmarkKVPut(b *testing.B) {
dir, rm := MakeDir()
defer rm()
kvs, _ := NewKVShard(dir, 100_000)
fr := NewFastRandom([]byte{1})
b.ResetTimer()
for i := 0; i < b.N; i++ {
key := fr.NextHash()
value := fr.RandBuff(100, 200)
kvs.PutPerm(key, value)
}
}Most tests focus on individual components. Adding more integration tests that verify the interaction between components would improve confidence in the system as a whole.
Some tests use hard-coded values for parameters like buffer sizes. Parameterizing these tests would help identify optimal configurations and edge cases.
Adding tests that deliberately inject errors (like disk full scenarios or corrupted files) would help verify error handling paths.
To run the tests in BlockchainDB:
# Run all tests
go test ./...
# Run a specific test
go test -run TestKV ./database/
# Run tests with verbose output
go test -v ./...
# Run tests and generate coverage report
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.htmlTwo flags change what runs:
-loadopts in to the multi-GB load tests, which build databases measured in tens of gigabytes and run for tens of minutes to hours. They are skipped by default sogo test ./...stays a suite someone can wait for; the skip line names the flag.-shortskips the measurement tests (TestSyncCost,TestDynaCost,TestMultiCoreScaling), which are correctness-neutral and exist to report numbers.
Split by cost, so that a mistake a compiler catches reports in a minute rather than behind the suite.
On every push and pull request (.github/workflows/ci.yml):
| job | what | why |
|---|---|---|
build |
go build, go vet, gofmt -l |
seconds; catches the unused import, the shadowed error, the unformatted file |
test |
go test -short ./... |
every correctness test; the measurement tests prove nothing on a shared runner |
race |
-race over the concurrency tests |
the sharded write path, the file pool, iteration alongside compaction |
Nightly (.github/workflows/nightly.yml): the full suite with
timings, the full suite under -race, and the crash-recovery tests
repeated 25 times.
That last job earns its place. The durability tests kill a child
process at a random moment, so each run samples a different window and
a single run proves little. Issue #35 — an empty key filter accepted
as covering segment (0,0), so Get reported keys absent that were
sitting on disk — failed twice in fifty-five runs. A pre-merge run
sees green and merges it. Repetition is what finds that class of bug,
and repetition is too slow to gate a push.
The suite is slow enough locally (~27 minutes, 21m17s with -short)
that this split is a necessity rather than a preference. Most of that
time is fsync: a block boundary seals all 512 shards, and a shard with
no writes still pays two fsyncs for a manifest it did not change. That
is issues #32 and #33.
A GitHub runner ran the same -short suite in 3m4s. The gap is
worth understanding rather than celebrating: the work is 93% fsync wait
(38s user and 52s system out of the local 21 minutes), and a runner's
virtual disk acknowledges an fsync far faster than a physical one
honouring a cache flush. The local number is the one that describes a
real node, and it is what #32 and #33 are measured against. A green
CI run says the code is correct, not that the fsync cost is acceptable.
The BlockchainDB testing approach provides good functional verification of components and includes performance measurements. However, there are opportunities to improve test coverage, isolation, and consistency through more structured testing approaches and better use of Go's testing features.