前⾯我们学习了很多的知识,也看到了⼀些例⼦,⼤家也对Go Test相关的之后有了⼀个全⾯的了解。这⼀章我通过⼀个常⻅的web程序示例,演示如何组织和应⽤单元测试。

⾸先这是⼀个普通的多层的web应⽤程序,按照分层的架构程序也分成了⼏个package:

  • model: 数据模型, 定义了Book类型和Author,只是类型的定义,并没有业务逻辑,不需要单元测试
  • dao: 提供数据库的访问。作为例⼦这⾥提供了⼀个 InsertBook 的实现,往数据库中插⼊⼀条书籍的信息
  • service:提供CreateBook⽅法,实际可以检查书籍是否存在,作者是否存在等逻辑判断,如果满⾜则调⽤dao的函数插⼊⼀条书籍信息
  • api:也叫controller,提供CreateBook handler,检查request的参数,如果满⾜则调⽤service层创建书籍

DAO层测试

dao层依赖数据库,根据先前的介绍,有多重⽅法可以提供测试。

这⼀次我想通过使⽤sqlmock的⽅式提供测试,不依赖任何数据库或者容器。

func TestBookStore_InsertBook(t *testing.T) {
    db, mock, err := sqlmock.New()
    assert.NoError(t, err, "an error '%s' was not expected when opening a stub
    database connection", err)
    defer db.Close()
    store := &bookStore{
        db: db,
    }
    book := &model.Book{
        Title: "The Go Programming Language",
        AuthorID: 1,
        ISBN: "978-0134190440",
        Subject: "computers",
    }
    mock.ExpectExec("INSERT INTO books").WillReturnResult(sqlmock.NewResult(1,
        1))
    err = store.InsertBook(context.TODO(), book)
    require.NoError(t, err)
    assert.NotZero(t, book.ID)
}

这⾥使⽤sqlmock mock Exec⽅法,假定它执⾏成功,返回lastID和影响⾏数。执⾏这个单元测试可以看到测试成功。

Service层的测试

service依赖dao层,依然可以使⽤sqlmock模拟测试,但是这⼀次我想换⼀种测试⽅法。使⽤嵌⼊式数据sqlite3进⾏真正的数据库访问。

func TestBookService_CreateBook(t *testing.T) {
    db := util.CreateTestDB(t)
    defer db.Close()
    bookService := NewBookService(db)
    book := &model.Book{
        Title: "The Go Programming Language",
        AuthorID: 1,
        ISBN: "978-0134190440",
        Subject: "computers",
    }
    err := bookService.CreateBook(context.TODO(), book)
    require.NoError(t, err)
    assert.NotZero(t, book.ID)
}
func CreateTestDB(t *testing.T) *sql.DB {
    db, err := sql.Open("sqlite3", "file:../testdata/test.db?cache=shared")
    assert.NoError(t, err)
    db.Exec(`
    CREATE TABLE IF NOT EXISTS books (
    Id INTEGER PRIMARY KEY AUTOINCREMENT,
    title TEXT,
    isbn TEXT,
    subject TEXT,
    author_id INTEGER
    );
    DELETE FROM books;
    `)
    return db
}

⼀般我们会把测试⽤的材料放在testdata⽂件夹下。testdata⽂件夹是⼀个特殊的⽂件夹。go编译器不会把它作为⼀个⼦package进⾏编译。

API 层的测试

同样, API层也有多重测试⽅法。⾸先我们看使⽤嵌⼊式数据库的⽅法,它把service层、dao层串联起来,其实是⼀个集成测试:

package api
import (
    "encoding/json"
    "io"
    "net/http"
    "net/http/httptest"
    "net/url"
    "strings"
    "testing"
    "github.com/smallnest/go_test_workshop/s7/book/model"
    "github.com/smallnest/go_test_workshop/s7/book/service"
    "github.com/smallnest/go_test_workshop/s7/book/util"
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
)
func TestBookController_GetBook(t *testing.T) {
    db := util.CreateTestDB(t)
    defer db.Close()
    bookService := service.NewBookService(db)
    bc := &BookController{bookService: bookService}
    data := url.Values{}
    data.Set("title", "The Go Programming Language")
    data.Set("autherID", "1")
    data.Set("isbn", "978-0134190440")
    data.Set("subject", "computers")
    r := httptest.NewRequest("POST", "http://example.com/foo",
        strings.NewReader(data.Encode()))
    r.Header.Add("Content-Type", "application/x-www-form-urlencoded")
    w := httptest.NewRecorder()
    bc.CreateBook(w, r)
    resp := w.Result()
    require.Equal(t, http.StatusOK, resp.StatusCode)
    body, _ := io.ReadAll(resp.Body)
    var book model.Book
    err := json.Unmarshal(body, &book)
    require.NoError(t, err)
    assert.NotZero(t, book.ID)
}

我们也可以使⽤gomonkey只针对api这⼀层进⾏测试。哪⼀种测试⽅法好呢?并⽆定式,哪⼀种写起来更⽅便,能够覆盖你的测试场景就⽤哪种。你最熟练使⽤哪⼀种就⽤哪⼀种。

package api
import (
    "context"
    "encoding/json"
    "io"
    "net/http"
    "net/http/httptest"
    "net/url"
    "strings"
    "testing"
    "github.com/agiledragon/gomonkey/v2"
    "github.com/smallnest/go_test_workshop/s7/book/model"
    "github.com/smallnest/go_test_workshop/s7/book/service"
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
)
func TestBookController_GetBook_By_Monkey(t *testing.T) {
    bookService := service.NewBookService(nil)
    bc := &BookController{bookService: bookService}
    data := url.Values{}
    data.Set("title", "The Go Programming Language")
    data.Set("autherID", "1")
    data.Set("isbn", "978-0134190440")
    data.Set("subject", "computers")
    r := httptest.NewRequest("POST", "http://example.com/foo",
        strings.NewReader(data.Encode()))
    r.Header.Add("Content-Type", "application/x-www-form-urlencoded")
    book := &model.Book{
        ID: 6,
        Title: "The Go Programming Language",
        AuthorID: 1,
        ISBN: "978-0134190440",
        Subject: "computers",
    }
    patches := gomonkey.ApplyMethodFunc(bookService, "CreateBook", func(ctx
    context.Context, b *model.Book) error {
        b.ID = 6
        return nil
    })
    defer patches.Reset()
    w := httptest.NewRecorder()
    bc.CreateBook(w, r)
    resp := w.Result()
    require.Equal(t, http.StatusOK, resp.StatusCode)
    body, _ := io.ReadAll(resp.Body)
    err := json.Unmarshal(body, &book)
    require.NoError(t, err)
    assert.NotZero(t, book.ID)
}
作者:admin  创建时间:2024-10-22 01:56
最后编辑:admin  更新时间:2024-10-22 02:10