You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为自定义Go内存数据存储编写单元测试?

内存数据存储的单元测试方法

我要做简单的REST API,自己实现了一个内存数据存储,接口定义如下:

type datastore interface {
    Add(*Element) error
    Get(ElementID) (*Element, error)
    Update(*Element) error
    Delete(ElementID) error
    GetAll() []*Element
}

存储结构体和初始化函数:

type Datastore struct {
    mu     sync.Mutex
    bucket map[string]*Element
}

func NewDB() *Datastore {
    return &Datastore {
        bucket: make(map[string]*Element),
    }
}

我已经写了部分测试用例,比如GetAll的测试:

func TestGetAllTODOTasks(t *testing.T) {
    ts := NewDB()
    var elem = &Element{fieldA : "A" , fieldB : "B"}
    ts.Add(elem)

    want := []*Element{elem}

    if got := ts.GetAll(); !reflect.DeepEqual(got, want) {
        t.Errorf("Got %v wanted %v", got, want)
    }
}

但测试Update这类方法时,需要先调用Add再执行Update,比如:

func TestUpdateTODOTasks(t *testing.T) {
    ts := NewDB()
    var elem = &Element{ID: "test-id", fieldA : "A" , fieldB : "B"}
    err := ts.Add(elem)
    if err != nil {
        t.Errorf("=> failed to create: %v", err.Error())
    }
    var updated_elem = &Element{ID: elem.ID, fieldA : "A-updated" , fieldB : "B"}

    err = ts.Update(updated_elem )

    if err != nil {
        t.Errorf("=> failed to update: %v", err.Error())
    }
}

想了解这类内存数据存储的标准单元测试方法。


标准单元测试实践

1. 每个测试用例独立初始化存储实例

每个测试都通过NewDB()创建全新的存储实例,确保测试用例之间完全隔离,不会互相污染数据。这是内存存储测试的基础,避免前一个测试的残留数据影响后续测试结果。

2. 覆盖所有接口方法的「正常场景」和「异常场景」

针对每个接口方法,要覆盖成功路径和失败路径:

  • Add方法:
    • 正常添加单个元素,验证Get/GetAll能正确获取
    • 添加重复ID的元素,验证返回预期错误
  • Get方法:
    • 获取存在的元素,验证数据一致
    • 获取不存在的ID,验证返回预期错误
  • Update方法:
    • 更新已存在的元素,验证数据被正确修改
    • 更新不存在的ID,验证返回预期错误
    • 更新时传入不合法的元素(如ID为空),验证错误处理
  • Delete方法:
    • 删除已存在的元素,验证Get/GetAll无法再获取
    • 删除不存在的ID,验证返回预期错误
  • GetAll方法:
    • 空存储时返回空切片
    • 存储多个元素时返回完整列表

3. 抽离重复的测试辅助函数

像Update、Delete这类需要先添加元素的测试,可以把「创建并添加元素」的逻辑抽成辅助函数,减少代码重复:

func setupTestElement(t *testing.T, ds *Datastore) *Element {
    t.Helper() // 标记为辅助函数,错误信息会指向调用处而非函数内部
    elem := &Element{ID: "test-id", FieldA: "A", FieldB: "B"}
    if err := ds.Add(elem); err != nil {
        t.Fatalf("failed to setup test element: %v", err)
    }
    return elem
}

简化后的Update测试示例:

func TestUpdate_Success(t *testing.T) {
    ds := NewDB()
    originalElem := setupTestElement(t, ds)

    // 构造更新后的元素
    updatedElem := &Element{
        ID:      originalElem.ID,
        FieldA:  "A-updated",
        FieldB:  originalElem.FieldB,
    }

    if err := ds.Update(updatedElem); err != nil {
        t.Fatalf("update failed unexpectedly: %v", err)
    }

    // 验证更新结果
    retrievedElem, err := ds.Get(originalElem.ID)
    if err != nil {
        t.Fatalf("failed to get updated element: %v", err)
    }
    if !reflect.DeepEqual(retrievedElem, updatedElem) {
        t.Errorf("element not updated correctly: got %v, want %v", retrievedElem, updatedElem)
    }
}

func TestUpdate_NotFound(t *testing.T) {
    ds := NewDB()
    nonExistentElem := &Element{ID: "non-existent", FieldA: "test"}

    err := ds.Update(nonExistentElem)
    if err == nil {
        t.Error("expected error when updating non-existent element, got nil")
    }
    // 可进一步验证错误类型是否符合预期,比如自定义ErrNotFound
    // if !errors.Is(err, ErrNotFound) {
    //     t.Errorf("expected error %v, got %v", ErrNotFound, err)
    // }
}

4. 验证操作后的实际数据状态

不要只检查方法返回的错误是否符合预期,一定要通过其他接口验证数据的实际状态:

  • Update后用Get确认数据已修改
  • Delete后用Get或GetAll确认元素已移除
  • Add后用GetAll确认元素存在

5. 测试并发安全性

因为Datastore用了sync.Mutex,必须测试并发读写场景下的安全性,避免竞态条件:

func TestDatastore_ConcurrentAccess(t *testing.T) {
    ds := NewDB()
    const numOps = 1000

    var wg sync.WaitGroup
    wg.Add(numOps * 2)

    // 并发添加元素
    for i := 0; i < numOps; i++ {
        go func(id int) {
            defer wg.Done()
            elem := &Element{ID: fmt.Sprintf("id-%d", id), FieldA: fmt.Sprintf("val-%d", id)}
            if err := ds.Add(elem); err != nil {
                t.Errorf("concurrent add failed: %v", err)
            }
        }(i)
    }

    // 并发获取元素
    for i := 0; i < numOps; i++ {
        go func(id int) {
            defer wg.Done()
            _, err := ds.Get(fmt.Sprintf("id-%d", id))
            // 允许找不到,因为添加和获取可能有先后顺序
            if err != nil && !errors.Is(err, ErrNotFound) {
                t.Errorf("concurrent get failed: %v", err)
            }
        }(i)
    }

    wg.Wait()

    // 最终验证元素数量是否正确
    if len(ds.GetAll()) != numOps {
        t.Errorf("expected %d elements, got %d", numOps, len(ds.GetAll()))
    }
}

6. 使用子测试组织相关测试用例

用testing.T.Run把同一方法的不同测试场景组织成子测试,让测试结果更清晰:

func TestUpdate(t *testing.T) {
    t.Run("Success", func(t *testing.T) {
        // 成功更新的测试逻辑
    })

    t.Run("NotFound", func(t *testing.T) {
        // 更新不存在元素的测试逻辑
    })

    t.Run("InvalidID", func(t *testing.T) {
        // 更新ID为空的元素的测试逻辑
    })
}

内容的提问来源于stack exchange,提问作者maantos

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.30 06:27:49