Skip to content

Commit dd1c4af

Browse files
author
Test
committed
test: add integration tests with testcontainers-go and unit tests
Add test coverage for collector, doctor, config, retry, and engine packages using testcontainers-go with a real MongoDB container for integration tests and pure unit tests for config/retry logic.
1 parent f33b7b2 commit dd1c4af

8 files changed

Lines changed: 1213 additions & 23 deletions

File tree

go.mod

Lines changed: 58 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,82 @@
11
module github.com/ppiankov/mongopulse
22

3-
go 1.24.0
3+
go 1.25.0
44

55
require (
66
github.com/prometheus/client_golang v1.23.2
77
github.com/spf13/cobra v1.10.2
8+
github.com/testcontainers/testcontainers-go/modules/mongodb v0.41.0
89
go.mongodb.org/mongo-driver/v2 v2.5.0
910
)
1011

1112
require (
13+
dario.cat/mergo v1.0.2 // indirect
14+
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
15+
github.com/Microsoft/go-winio v0.6.2 // indirect
1216
github.com/beorn7/perks v1.0.1 // indirect
17+
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
1318
github.com/cespare/xxhash/v2 v2.3.0 // indirect
19+
github.com/containerd/errdefs v1.0.0 // indirect
20+
github.com/containerd/errdefs/pkg v0.3.0 // indirect
21+
github.com/containerd/log v0.1.0 // indirect
22+
github.com/containerd/platforms v0.2.1 // indirect
23+
github.com/cpuguy83/dockercfg v0.3.2 // indirect
24+
github.com/davecgh/go-spew v1.1.1 // indirect
25+
github.com/distribution/reference v0.6.0 // indirect
26+
github.com/docker/docker v28.5.2+incompatible // indirect
27+
github.com/docker/go-connections v0.6.0 // indirect
28+
github.com/docker/go-units v0.5.0 // indirect
29+
github.com/ebitengine/purego v0.10.0 // indirect
30+
github.com/felixge/httpsnoop v1.0.4 // indirect
31+
github.com/go-logr/logr v1.4.3 // indirect
32+
github.com/go-logr/stdr v1.2.2 // indirect
33+
github.com/go-ole/go-ole v1.2.6 // indirect
34+
github.com/google/uuid v1.6.0 // indirect
35+
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
1436
github.com/inconshreveable/mousetrap v1.1.0 // indirect
15-
github.com/klauspost/compress v1.18.0 // indirect
16-
github.com/kr/text v0.2.0 // indirect
37+
github.com/klauspost/compress v1.18.2 // indirect
38+
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
39+
github.com/magiconair/properties v1.8.10 // indirect
40+
github.com/moby/docker-image-spec v1.3.1 // indirect
41+
github.com/moby/go-archive v0.2.0 // indirect
42+
github.com/moby/patternmatcher v0.6.0 // indirect
43+
github.com/moby/sys/sequential v0.6.0 // indirect
44+
github.com/moby/sys/user v0.4.0 // indirect
45+
github.com/moby/sys/userns v0.1.0 // indirect
46+
github.com/moby/term v0.5.2 // indirect
47+
github.com/morikuni/aec v1.0.0 // indirect
1748
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
49+
github.com/opencontainers/go-digest v1.0.0 // indirect
50+
github.com/opencontainers/image-spec v1.1.1 // indirect
51+
github.com/pkg/errors v0.9.1 // indirect
52+
github.com/pmezard/go-difflib v1.0.0 // indirect
53+
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
1854
github.com/prometheus/client_model v0.6.2 // indirect
1955
github.com/prometheus/common v0.66.1 // indirect
2056
github.com/prometheus/procfs v0.16.1 // indirect
57+
github.com/shirou/gopsutil/v4 v4.26.2 // indirect
58+
github.com/sirupsen/logrus v1.9.3 // indirect
2159
github.com/spf13/pflag v1.0.9 // indirect
60+
github.com/stretchr/testify v1.11.1 // indirect
61+
github.com/testcontainers/testcontainers-go v0.41.0 // indirect
62+
github.com/tklauser/go-sysconf v0.3.16 // indirect
63+
github.com/tklauser/numcpus v0.11.0 // indirect
2264
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
2365
github.com/xdg-go/scram v1.2.0 // indirect
2466
github.com/xdg-go/stringprep v1.0.4 // indirect
2567
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
68+
github.com/yusufpapurcu/wmi v1.2.4 // indirect
69+
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
70+
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
71+
go.opentelemetry.io/otel v1.41.0 // indirect
72+
go.opentelemetry.io/otel/metric v1.41.0 // indirect
73+
go.opentelemetry.io/otel/trace v1.41.0 // indirect
2674
go.yaml.in/yaml/v2 v2.4.2 // indirect
27-
golang.org/x/crypto v0.33.0 // indirect
28-
golang.org/x/sync v0.16.0 // indirect
29-
golang.org/x/sys v0.35.0 // indirect
30-
golang.org/x/text v0.28.0 // indirect
31-
google.golang.org/protobuf v1.36.8 // indirect
75+
golang.org/x/crypto v0.48.0 // indirect
76+
golang.org/x/sync v0.19.0 // indirect
77+
golang.org/x/sys v0.41.0 // indirect
78+
golang.org/x/text v0.34.0 // indirect
79+
google.golang.org/grpc v1.79.1 // indirect
80+
google.golang.org/protobuf v1.36.11 // indirect
81+
gopkg.in/yaml.v3 v3.0.1 // indirect
3282
)

go.sum

Lines changed: 149 additions & 15 deletions
Large diffs are not rendered by default.
Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
//go:build integration
2+
3+
package collector
4+
5+
import (
6+
"context"
7+
"testing"
8+
"time"
9+
10+
"github.com/prometheus/client_golang/prometheus"
11+
dto "github.com/prometheus/client_model/go"
12+
"go.mongodb.org/mongo-driver/v2/bson"
13+
14+
"github.com/ppiankov/mongopulse/internal/config"
15+
"github.com/ppiankov/mongopulse/internal/metrics"
16+
"github.com/ppiankov/mongopulse/internal/testutil"
17+
)
18+
19+
func gaugeValue(g *prometheus.GaugeVec, labels ...string) float64 {
20+
m := &dto.Metric{}
21+
if err := g.WithLabelValues(labels...).Write(m); err != nil {
22+
return 0
23+
}
24+
return m.GetGauge().GetValue()
25+
}
26+
27+
func counterValue(c *prometheus.CounterVec, labels ...string) float64 {
28+
m := &dto.Metric{}
29+
if err := c.WithLabelValues(labels...).Write(m); err != nil {
30+
return 0
31+
}
32+
return m.GetCounter().GetValue()
33+
}
34+
35+
func setupCollector(t *testing.T) (*Collector, *metrics.Metrics, string) {
36+
t.Helper()
37+
client, _ := testutil.StartMongo(t)
38+
39+
reg := prometheus.NewRegistry()
40+
m := metrics.New(reg)
41+
cfg := config.Config{
42+
DSN: []string{"mongodb://localhost:27017"},
43+
PollInterval: 5 * time.Second,
44+
SlowQueryThreshold: 5 * time.Second,
45+
RegressionThreshold: 2.0,
46+
StmtLimit: 50,
47+
}
48+
node := "test-node"
49+
c := New(client, node, m, cfg, nil, nil)
50+
return c, m, node
51+
}
52+
53+
func TestCollectServerStatus(t *testing.T) {
54+
c, m, node := setupCollector(t)
55+
ctx := context.Background()
56+
57+
ss, err := c.collectServerStatus(ctx)
58+
if err != nil {
59+
t.Fatalf("collectServerStatus: %v", err)
60+
}
61+
if ss == nil {
62+
t.Fatal("serverStatus result is nil")
63+
}
64+
65+
up := gaugeValue(m.Up, node)
66+
if up != 1 {
67+
t.Errorf("Up = %f, want 1", up)
68+
}
69+
70+
uptime := gaugeValue(m.Uptime, node)
71+
if uptime <= 0 {
72+
t.Errorf("Uptime = %f, want > 0", uptime)
73+
}
74+
75+
// Version info should have a non-empty version label.
76+
if v, ok := ss["version"].(string); !ok || v == "" {
77+
t.Error("serverStatus missing version field")
78+
}
79+
}
80+
81+
func TestCollectConnections(t *testing.T) {
82+
c, m, node := setupCollector(t)
83+
ctx := context.Background()
84+
85+
ss, err := c.collectServerStatus(ctx)
86+
if err != nil {
87+
t.Fatalf("collectServerStatus: %v", err)
88+
}
89+
90+
c.collectConnections(ctx, ss)
91+
92+
current := gaugeValue(m.ConnCurrent, node)
93+
if current <= 0 {
94+
t.Errorf("ConnCurrent = %f, want > 0", current)
95+
}
96+
97+
available := gaugeValue(m.ConnAvailable, node)
98+
if available <= 0 {
99+
t.Errorf("ConnAvailable = %f, want > 0", available)
100+
}
101+
}
102+
103+
func TestCollectOpcounters(t *testing.T) {
104+
c, m, node := setupCollector(t)
105+
ctx := context.Background()
106+
107+
// Get initial opcounters.
108+
ss1, err := c.collectServerStatus(ctx)
109+
if err != nil {
110+
t.Fatalf("collectServerStatus: %v", err)
111+
}
112+
c.collectOpcounters(ctx, ss1)
113+
insertBefore := counterValue(m.OpsTotal, node, "insert")
114+
115+
// Insert a document to increment insert counter.
116+
_, err = c.client.Database("testdb_opcounters").Collection("testcoll").InsertOne(ctx, bson.M{"key": "value"})
117+
if err != nil {
118+
t.Fatalf("insert: %v", err)
119+
}
120+
121+
// Re-collect.
122+
ss2, err := c.collectServerStatus(ctx)
123+
if err != nil {
124+
t.Fatalf("collectServerStatus: %v", err)
125+
}
126+
c.collectOpcounters(ctx, ss2)
127+
insertAfter := counterValue(m.OpsTotal, node, "insert")
128+
129+
if insertAfter <= insertBefore {
130+
t.Errorf("insert counter did not increase: before=%f, after=%f", insertBefore, insertAfter)
131+
}
132+
}
133+
134+
func TestCollectDbStats(t *testing.T) {
135+
c, m, node := setupCollector(t)
136+
ctx := context.Background()
137+
138+
// Insert data so dbStats has something to report.
139+
db := c.client.Database("testdb_dbstats")
140+
_, err := db.Collection("testcoll").InsertOne(ctx, bson.M{"data": "hello world"})
141+
if err != nil {
142+
t.Fatalf("insert: %v", err)
143+
}
144+
145+
c.collectDbStats(ctx)
146+
147+
dataSize := gaugeValue(m.DbDataSize, node, "testdb_dbstats")
148+
if dataSize <= 0 {
149+
t.Errorf("DbDataSize = %f, want > 0", dataSize)
150+
}
151+
}
152+
153+
func TestCollectCollections(t *testing.T) {
154+
c, m, node := setupCollector(t)
155+
ctx := context.Background()
156+
157+
db := c.client.Database("testdb_collections")
158+
_, err := db.Collection("mycoll").InsertOne(ctx, bson.M{"n": 1})
159+
if err != nil {
160+
t.Fatalf("insert: %v", err)
161+
}
162+
163+
c.collectCollections(ctx)
164+
165+
docCount := gaugeValue(m.CollDocCount, node, "testdb_collections", "mycoll")
166+
if docCount < 1 {
167+
t.Errorf("CollDocCount = %f, want >= 1", docCount)
168+
}
169+
}
170+
171+
func TestCollect_FullFlow(t *testing.T) {
172+
c, m, node := setupCollector(t)
173+
ctx := context.Background()
174+
175+
// Insert some data to make metrics non-trivial.
176+
db := c.client.Database("testdb_fullflow")
177+
_, err := db.Collection("items").InsertOne(ctx, bson.M{"item": "test"})
178+
if err != nil {
179+
t.Fatalf("insert: %v", err)
180+
}
181+
182+
// Run full collection — should not panic or error.
183+
c.Collect(ctx)
184+
185+
// Verify key metrics are set.
186+
up := gaugeValue(m.Up, node)
187+
if up != 1 {
188+
t.Errorf("Up = %f, want 1 after Collect()", up)
189+
}
190+
191+
uptime := gaugeValue(m.Uptime, node)
192+
if uptime <= 0 {
193+
t.Errorf("Uptime = %f, want > 0", uptime)
194+
}
195+
196+
pollDuration := gaugeValue(m.PollDuration, node)
197+
if pollDuration <= 0 {
198+
t.Errorf("PollDuration = %f, want > 0", pollDuration)
199+
}
200+
}
201+
202+
func TestCollect_ReplicationGracefulSkip(t *testing.T) {
203+
c, _, _ := setupCollector(t)
204+
ctx := context.Background()
205+
206+
// Standalone MongoDB — replication collector should not fail the whole Collect.
207+
err := c.collectReplication(ctx)
208+
if err == nil {
209+
t.Log("collectReplication returned nil — unexpected for standalone, but not fatal")
210+
}
211+
// The key assertion: it should return an error (standalone), but not panic.
212+
}
213+
214+
func TestToFloat64(t *testing.T) {
215+
t.Parallel()
216+
tests := []struct {
217+
name string
218+
in interface{}
219+
want float64
220+
ok bool
221+
}{
222+
{"float64", float64(3.14), 3.14, true},
223+
{"int32", int32(42), 42, true},
224+
{"int64", int64(100), 100, true},
225+
{"int", int(7), 7, true},
226+
{"string", "nope", 0, false},
227+
{"nil", nil, 0, false},
228+
{"bool", true, 0, false},
229+
}
230+
for _, tt := range tests {
231+
t.Run(tt.name, func(t *testing.T) {
232+
t.Parallel()
233+
got, ok := toFloat64(tt.in)
234+
if ok != tt.ok {
235+
t.Errorf("ok = %v, want %v", ok, tt.ok)
236+
}
237+
if got != tt.want {
238+
t.Errorf("value = %f, want %f", got, tt.want)
239+
}
240+
})
241+
}
242+
}
243+
244+
func TestIsSystemDB(t *testing.T) {
245+
t.Parallel()
246+
tests := []struct {
247+
name string
248+
want bool
249+
}{
250+
{"admin", true},
251+
{"local", true},
252+
{"config", true},
253+
{"mydb", false},
254+
{"", false},
255+
}
256+
for _, tt := range tests {
257+
t.Run(tt.name, func(t *testing.T) {
258+
t.Parallel()
259+
if got := isSystemDB(tt.name); got != tt.want {
260+
t.Errorf("isSystemDB(%q) = %v, want %v", tt.name, got, tt.want)
261+
}
262+
})
263+
}
264+
}

0 commit comments

Comments
 (0)