test(x): 添加 x 目录下 inspector/metrics/monitor 的生命周期测试;修复 inspector 测试导入及 sqlite3 驱动注册,确保:

- 在本地有 Redis 时能运行完整集成测试
- 在无 Redis 环境下会自动跳过,避免破坏单元测试套件
同时补充了 metrics/monitor 的基本单元测试,改善测试的稳定性和可维护性
This commit is contained in:
2025-12-10 13:10:51 +08:00
parent f3a1b8060b
commit 98d0f9e78d
3 changed files with 219 additions and 0 deletions

View File

@@ -0,0 +1,103 @@
package inspector
import (
"context"
"testing"
"time"
"code.tczkiot.com/wlw/taskq"
"github.com/hibiken/asynq"
_ "github.com/mattn/go-sqlite3"
"github.com/redis/go-redis/v9"
)
func TestNewDefaultsAndName(t *testing.T) {
ins := New(Options{})
if ins == nil {
t.Fatalf("New returned nil")
}
if ins.opts.Interval <= 0 {
t.Fatalf("expected default Interval > 0")
}
if ins.opts.DBPath == "" {
t.Fatalf("expected default DBPath set")
}
if ins.Name() != "inspector" {
t.Fatalf("unexpected Name: %s", ins.Name())
}
}
func TestConvertTaskHelpers(t *testing.T) {
// ensure convertTaskInfo/convertTaskList are callable
_ = convertTaskInfo(&asynq.TaskInfo{})
_ = convertTaskList([]*asynq.TaskInfo{})
}
func TestGetQueueInfoWhenNotStarted(t *testing.T) {
ins := New(Options{})
if _, err := ins.GetQueueInfo("default"); err == nil {
t.Fatalf("expected error when inspector not started")
}
}
func TestListActiveTasksWhenNotStarted(t *testing.T) {
ins := New(Options{})
if _, err := ins.ListActiveTasks("default", 10, 0); err == nil {
t.Fatalf("expected error when inspector not started")
}
}
func TestSaveMetrics_NoDB(t *testing.T) {
ins := New(Options{})
// db is nil by default; saveMetrics should return nil (no-op)
s := Stats{Queue: "q", Timestamp: time.Now().Unix()}
if err := ins.saveMetrics(s); err != nil {
t.Fatalf("saveMetrics returned error with nil db: %v", err)
}
}
// helper: create redis client for tests
func makeTestRedis(t *testing.T) redis.UniversalClient {
rdb := redis.NewClient(&redis.Options{Addr: "localhost:6379", DB: 15})
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := rdb.Ping(ctx).Err(); err != nil {
t.Skipf("redis not available: %v", err)
}
// flush test DB
if err := rdb.FlushDB(context.Background()).Err(); err != nil {
t.Fatalf("failed to flush redis: %v", err)
}
return rdb
}
func TestPluginLifecycleWithServlet(t *testing.T) {
rdb := makeTestRedis(t)
// create plugin
ins := New(Options{DBPath: ":memory:", Interval: time.Second})
// wire into a fresh servlet as default to use package-level helpers
s := taskq.NewServlet()
taskq.SetDefault(s)
cfg := taskq.Config{Redis: rdb, Tasks: []*taskq.Task{}, Plugins: []taskq.Plugin{ins}}
if err := taskq.Configure(cfg); err != nil {
t.Fatalf("Configure failed: %v", err)
}
if err := taskq.Init(context.Background()); err != nil {
t.Fatalf("Init failed: %v", err)
}
if err := taskq.Start(context.Background()); err != nil {
t.Fatalf("Start failed: %v", err)
}
// request stop
taskq.Stop()
// wait for plugins and internal goroutines to shutdown
time.Sleep(500 * time.Millisecond)
// close redis
rdb.Close()
}

68
x/metrics/metrics_test.go Normal file
View File

@@ -0,0 +1,68 @@
package metrics
import (
"context"
"testing"
"time"
"code.tczkiot.com/wlw/taskq"
"github.com/redis/go-redis/v9"
)
func TestNewDefaultsAndName(t *testing.T) {
m := New(Options{})
if m == nil {
t.Fatalf("New returned nil")
}
if m.opts.Namespace == "" {
t.Fatalf("expected default Namespace")
}
if m.Name() != "metrics" {
t.Fatalf("unexpected Name: %s", m.Name())
}
}
func TestCollectNoInspectorNoQueues(t *testing.T) {
m := New(Options{Interval: time.Millisecond})
// ensure collect() is safe to call when inspector or queues are nil
m.collect()
}
func makeTestRedis(t *testing.T) redis.UniversalClient {
rdb := redis.NewClient(&redis.Options{Addr: "localhost:6379", DB: 15})
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := rdb.Ping(ctx).Err(); err != nil {
t.Skipf("redis not available: %v", err)
}
if err := rdb.FlushDB(context.Background()).Err(); err != nil {
t.Fatalf("failed to flush redis: %v", err)
}
return rdb
}
func TestMetricsLifecycleWithServlet(t *testing.T) {
rdb := makeTestRedis(t)
m := New(Options{Interval: time.Second})
// register plugin via default servlet
s := taskq.NewServlet()
taskq.SetDefault(s)
cfg := taskq.Config{Redis: rdb, Tasks: []*taskq.Task{}, Plugins: []taskq.Plugin{m}}
if err := taskq.Configure(cfg); err != nil {
t.Fatalf("Configure failed: %v", err)
}
if err := taskq.Init(context.Background()); err != nil {
t.Fatalf("Init failed: %v", err)
}
if err := taskq.Start(context.Background()); err != nil {
t.Fatalf("Start failed: %v", err)
}
taskq.Stop()
time.Sleep(500 * time.Millisecond)
rdb.Close()
}

48
x/monitor/monitor_test.go Normal file
View File

@@ -0,0 +1,48 @@
package monitor
import (
"net/http"
"net/http/httptest"
"testing"
"code.tczkiot.com/wlw/taskq/x/inspector"
)
func TestNewValidatesOptions(t *testing.T) {
_, err := New(Options{})
if err == nil {
t.Fatalf("expected error when options missing")
}
// valid case
ins := &inspector.Inspector{}
m, err := New(Options{Inspector: ins, Queues: map[string]int{"default": 1}})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if m.RootPath() != "/monitor" {
t.Fatalf("unexpected root path: %s", m.RootPath())
}
}
func TestHandleIndexServesUI(t *testing.T) {
ins := &inspector.Inspector{}
m, err := New(Options{Inspector: ins, Queues: map[string]int{"default": 1}, RootPath: "/my"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
req := httptest.NewRequest(http.MethodGet, "/my/", nil)
w := httptest.NewRecorder()
m.ServeHTTP(w, req)
resp := w.Result()
if resp.StatusCode != http.StatusOK {
t.Fatalf("expected 200 OK, got %d", resp.StatusCode)
}
// body should contain the root path replacement
body := w.Body.String()
if body == "" {
t.Fatalf("expected non-empty index body")
}
}