Golang中SQLite函数测试遇到问题如何解决

Golang中SQLite函数测试遇到问题如何解决 各位Gopher们!我发帖是因为在我的第一个Go项目good中需要大量帮助。基本上,这是一个与SQLite数据库交互的情绪追踪器。

我的问题是:我一直在处理sqlite_test.go,发现测试会以不同方式失败,具体取决于我是否使用TestMain(稍后会详细说明)。

首先,这是sqlite.go——在test1test2分支中内容相同。现在,这是我在test1中的测试情况,其中每个测试相对独立(虽然也有些混乱)。

=== RUN   TestInsertEvents
=== RUN   TestInsertEvents/2006-01-02_15:04:05+00:00
=== RUN   TestInsertEvents/2019-06-27_19:25:09-07:00
--- PASS: TestInsertEvents (0.00s)
    --- PASS: TestInsertEvents/2006-01-02_15:04:05+00:00 (0.00s)
    --- PASS: TestInsertEvents/2019-06-27_19:25:09-07:00 (0.00s)
=== RUN   TestEvents
=== RUN   TestEvents/reviewing_1_day_back
DEBU[0000] Getting all events starting from 2019-06-26.
DEBU[0000] Querying activities for event 1.
--- FAIL: TestEvents (0.00s)
    --- FAIL: TestEvents/reviewing_1_day_back (0.00s)
        sqlite_test.go:90: Error upon querying event: no such table: activities
FAIL
exit status 1
FAIL    github.com/Goorzhel/good        0.004s

很奇怪,对吧?也许我可以把这归因于每个测试都在自己的goroutine中运行……说实话,我对这里发生的事情的心理模型还不够完善,无法准确判断到底是什么问题。但我非常确定activities应该存在。

但等等,还有更奇怪的。这是test2,其中我使用了TestMain。

=== RUN   TestInsertEvents/2006-01-02_15:04:05+00:00
panic: runtime error: invalid memory address or nil pointer dereference [recovered]
        panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x61d31a]

goroutine 8 [running]:
testing.tRunner.func1(0xc00010e200)
        /usr/lib/go/src/testing/testing.go:830 +0x392
panic(0x754c60, 0xa09f00)
        /usr/lib/go/src/runtime/panic.go:522 +0x1b5
database/sql.(*DB).conn(0x0, 0x801b80, 0xc0000140a8, 0x1, 0x0, 0x0, 0x0)
        /usr/lib/go/src/database/sql/sql.go:1080 +0x3a
database/sql.(*DB).query(0x0, 0x801b80, 0xc0000140a8, 0xc00001e1b0, 0x29, 0x0, 0x0, 0x0, 0xa28101, 0x7f0f35936008, ...)
        /usr/lib/go/src/database/sql/sql.go:1513 +0x66
database/sql.(*DB).QueryContext(0x0, 0x801b80, 0xc0000140a8, 0xc00001e1b0, 0x29, 0x0, 0x0, 0x0, 0x40, 0xc00001e1b0, ...)
        /usr/lib/go/src/database/sql/sql.go:1495 +0xd1
database/sql.(*DB).QueryRowContext(...)
        /usr/lib/go/src/database/sql/sql.go:1596
database/sql.(*DB).QueryRow(0x0, 0xc00001e1b0, 0x29, 0x0, 0x0, 0x0, 0x29)
        /usr/lib/go/src/database/sql/sql.go:1607 +0x8d
github.com/Goorzhel/good.(*dbConn).IDByName(0xa264d8, 0x7a52a8, 0x5, 0x7a5302, 0x5, 0x539, 0x877c38, 0x9eb564)
        /home/ca/projects/good/sqlite.go:80 +0x1ed
github.com/Goorzhel/good.(*dbConn).InsertEvent(0xa264d8, 0xa0e840, 0x5d157c36, 0xc000048f98)
        /home/ca/projects/good/sqlite.go:159 +0x72
github.com/Goorzhel/good.testInsertEventFunc.func1(0xc00010e200)
        /home/ca/projects/good/sqlite_test.go:73 +0x37
testing.tRunner(0xc00010e200, 0xc00000e800)
        /usr/lib/go/src/testing/testing.go:865 +0xc0
created by testing.(*T).Run
        /usr/lib/go/src/testing/testing.go:916 +0x35a
exit status 2
FAIL    github.com/Goorzhel/good        0.005s

哇!它段错误了!我就知道good.dbConn.IDByName有问题。但是,除了用go-delve/delve随便戳戳之外,我找不出问题所在。(我再次把这归因于我是Go新手,而且对调试堆栈跟踪完全没有经验,还有编写单元测试也不熟练,但我跑题了。)

最奇怪的是,只有在go test时我才会看到问题。现在你可以克隆源代码,go build它,然后正常运行程序而不会看到任何问题。

我有点怀疑问题可能是我完全忽略了database/sql.Tx功能,但我不想再次搞错而白费功夫。

总之,Gopher们,你们能帮我从这个坑里爬出来吗?

附言:很可能在我解决这个问题之后,我会重新设置更改并删除这些分支——如果你们希望我把相关部分放在Gist或类似的地方,请告诉我。


更多关于Golang中SQLite函数测试遇到问题如何解决的实战教程也可以访问 https://www.itying.com/category-94-b0.html

4 回复

@kync 说得有道理:从逻辑上讲,如果不先插入事件,我无法对单元进行测试 db.Events()

这正是我需要摆脱那些反复困扰自己的陷阱的动力,其中包括我坚持使用内存 SQLite 数据库的做法。我最终选择使用一个稍后会被 os.Remove 删除的文件。

现在,我所有的测试都通过了,可以继续专注地构建这个东西了:

% go test
PASS
ok      github.com/Goorzhel/good        0.125s

感谢大家!

更多关于Golang中SQLite函数测试遇到问题如何解决的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


感谢提供的链接,但正如我在帖子中提到的,我的问题特别针对Go代码的测试:

最奇怪的是,只有在执行go test时才会出现故障。目前你可以克隆源代码,执行go build编译,然后正常运行程序而不会出现任何问题。

在单线程环境(编译后的二进制文件)中,基础的读写操作都没有出现问题。但根据我粗略的阅读,每个测试都在各自的goroutine中运行

所以,我可能遇到了某种竞态条件,但目前我还没有找到解决方法。

(我可以不修复这个问题继续推进,但我希望让这些测试正常工作,这样就不必依赖手动运行工具了。)

Go语言中的测试函数默认不会按顺序执行。

你需要付出一些努力才能让它们按顺序运行。

T和B的Run方法允许定义子测试和子基准测试,无需为每个测试单独定义函数。这使得可以实现诸如表驱动基准测试和创建分层测试等用途。它还提供了一种共享通用设置和清理代码的方法:

func TestFoo(t *testing.T) {
    // 
    t.Run("A=1", func(t *testing.T) { … })
    t.Run("A=2", func(t *testing.T) { … })
    t.Run("B=1", func(t *testing.T) { … })
    // 
}

每个子测试和子基准测试都有唯一的名称:由顶层测试的名称和传递给Run的名称序列组合而成,用斜杠分隔,可选的后缀序列号用于消除歧义。

-run和-bench命令行标志的参数是一个非锚定的正则表达式,用于匹配测试的名称。对于具有多个斜杠分隔元素的测试(如子测试),参数本身也是斜杠分隔的,表达式依次匹配每个名称元素。由于它是非锚定的,空表达式匹配任何字符串。例如,使用"匹配"表示"名称包含":

go test -run ''      # 运行所有测试
go test -run Foo     # 运行匹配"Foo"的顶层测试,例如"TestFooBar"
go test -run Foo/A=  # 对于匹配"Foo"的顶层测试,运行匹配"A="的子测试
go test -run /A=1    # 对于所有顶层测试,运行匹配"A=1"的子测试

https://tip.golang.org/pkg/testing/#hdr-Subtests_and_Sub_benchmarks

从你的测试失败信息来看,主要问题在于数据库连接和表初始化的时机。让我分析一下具体问题和解决方案。

问题分析

test1分支中,activities表不存在是因为表创建操作没有在测试中正确执行。在test2分支中,空指针解引用是因为数据库连接没有正确初始化。

解决方案

以下是修复后的测试代码示例:

package main

import (
    "database/sql"
    "os"
    "testing"
    "time"

    _ "github.com/mattn/go-sqlite3"
)

func TestMain(m *testing.M) {
    // 初始化测试数据库
    db, err := sql.Open("sqlite3", ":memory:")
    if err != nil {
        panic(err)
    }
    defer db.Close()

    // 创建表结构
    _, err = db.Exec(`
        CREATE TABLE IF NOT EXISTS activities (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            name TEXT NOT NULL UNIQUE
        );
        CREATE TABLE IF NOT EXISTS events (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            activity_id INTEGER NOT NULL,
            timestamp DATETIME NOT NULL,
            FOREIGN KEY (activity_id) REFERENCES activities (id)
        );
    `)
    if err != nil {
        panic(err)
    }

    // 预插入测试数据
    _, err = db.Exec("INSERT OR IGNORE INTO activities (name) VALUES (?)", "test_activity")
    if err != nil {
        panic(err)
    }

    // 运行测试
    code := m.Run()
    os.Exit(code)
}

func TestInsertEvents(t *testing.T) {
    db, err := sql.Open("sqlite3", ":memory:")
    if err != nil {
        t.Fatalf("Failed to open database: %v", err)
    }
    defer db.Close()

    // 初始化表
    initTestDB(db)

    testCases := []struct {
        name      string
        timestamp string
    }{
        {"2006-01-02_15:04:05+00:00", "2006-01-02T15:04:05Z"},
        {"2019-06-27_19:25:09-07:00", "2019-06-27T19:25:09-07:00"},
    }

    for _, tc := range testCases {
        t.Run(tc.name, func(t *testing.T) {
            ts, err := time.Parse(time.RFC3339, tc.timestamp)
            if err != nil {
                t.Fatalf("Failed to parse timestamp: %v", err)
            }

            // 插入活动
            _, err = db.Exec("INSERT OR IGNORE INTO activities (name) VALUES (?)", "test_activity")
            if err != nil {
                t.Fatalf("Failed to insert activity: %v", err)
            }

            // 插入事件
            _, err = db.Exec(
                "INSERT INTO events (activity_id, timestamp) VALUES ((SELECT id FROM activities WHERE name = ?), ?)",
                "test_activity", ts,
            )
            if err != nil {
                t.Fatalf("Failed to insert event: %v", err)
            }
        })
    }
}

func TestEvents(t *testing.T) {
    db, err := sql.Open("sqlite3", ":memory:")
    if err != nil {
        t.Fatalf("Failed to open database: %v", err)
    }
    defer db.Close()

    // 初始化表和数据
    initTestDB(db)
    insertTestData(db)

    t.Run("reviewing_1_day_back", func(t *testing.T) {
        // 查询事件
        rows, err := db.Query(`
            SELECT e.id, e.timestamp, a.name 
            FROM events e 
            JOIN activities a ON e.activity_id = a.id 
            WHERE e.timestamp >= datetime('now', '-1 day')
        `)
        if err != nil {
            t.Fatalf("Failed to query events: %v", err)
        }
        defer rows.Close()

        var count int
        for rows.Next() {
            count++
        }
        t.Logf("Found %d events from the last day", count)
    })
}

func initTestDB(db *sql.DB) {
    _, err := db.Exec(`
        CREATE TABLE IF NOT EXISTS activities (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            name TEXT NOT NULL UNIQUE
        );
        CREATE TABLE IF NOT EXISTS events (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            activity_id INTEGER NOT NULL,
            timestamp DATETIME NOT NULL,
            FOREIGN KEY (activity_id) REFERENCES activities (id)
        );
    `)
    if err != nil {
        panic(err)
    }
}

func insertTestData(db *sql.DB) {
    // 插入测试活动
    activities := []string{"work", "exercise", "reading"}
    for _, activity := range activities {
        _, err := db.Exec("INSERT OR IGNORE INTO activities (name) VALUES (?)", activity)
        if err != nil {
            panic(err)
        }
    }

    // 插入测试事件
    _, err := db.Exec(`
        INSERT INTO events (activity_id, timestamp) 
        VALUES 
            ((SELECT id FROM activities WHERE name = 'work'), datetime('now')),
            ((SELECT id FROM activities WHERE name = 'exercise'), datetime('now', '-2 hours')),
            ((SELECT id FROM activities WHERE name = 'reading'), datetime('now', '-1 day'))
    `)
    if err != nil {
        panic(err)
    }
}

关键改进点

  1. 使用内存数据库: sql.Open("sqlite3", ":memory:") 避免文件系统竞争条件
  2. 确保表存在: 在每个测试中显式创建表结构
  3. 事务支持: 对于更复杂的操作,使用事务确保数据一致性
func TestWithTransaction(t *testing.T) {
    db, err := sql.Open("sqlite3", ":memory:")
    if err != nil {
        t.Fatal(err)
    }
    defer db.Close()
    
    initTestDB(db)

    tx, err := db.Begin()
    if err != nil {
        t.Fatal(err)
    }
    defer tx.Rollback()

    // 在事务中执行操作
    _, err = tx.Exec("INSERT INTO activities (name) VALUES (?)", "transaction_test")
    if err != nil {
        t.Fatal(err)
    }

    err = tx.Commit()
    if err != nil {
        t.Fatal(err)
    }
}

这样修改后,测试应该能够稳定运行,不会出现表不存在或空指针的问题。

回到顶部