go-test

单元测试

基本用法

Go语言测试
常用reflect.DeepEqual()slice进行比较

跳过某些测试用例

func TestTimeConsuming(t *testing.T) {if testing.Short() {t.Skip("short模式下会跳过该测试用例")}...
}

当执行go test -short时就不会执行上面的TestTimeConsuming测试用例。

子测试常单元测试中需要多组测试数据保证测试的效果。Go1.7+中新增了子测试,支持在测试函数中使用t.Run执行一组测试用例,这样就不需要为不同的测试数据定义多个测试函数了。

func TestXXX(t *testing.T){t.Run("case1", func(t *testing.T){...})t.Run("case2", func(t *testing.T){...})t.Run("case3", func(t *testing.T){...})
}

表格驱动测试
介绍
表格驱动测试不是工具、包或其他任何东西,它只是编写更清晰测试的一种方式和视角。表格驱动测试可以涵盖很多方面:表格里的每一个条目都是一个完整的测试用例,包含输入和预期结果,有时还包含测试名称等附加信息,以使测试输出易于阅读。使用表格驱动测试能够很方便的维护多个测试用例,避免在编写单元测试时频繁的复制粘贴。表格驱动测试的步骤通常是定义一个测试用例表格,然后遍历表格,并使用t.Run对每个条目执行必要的测试。

var flagtests = []struct {in  stringout string
}{{"%a", "[%a]"},{"%-a", "[%-a]"},{"%+a", "[%+a]"},{"%#a", "[%#a]"},{"% a", "[% a]"},{"%0a", "[%0a]"},{"%1.2a", "[%1.2a]"},{"%-1.2a", "[%-1.2a]"},{"%+1.2a", "[%+1.2a]"},{"%-+1.2a", "[%+-1.2a]"},{"%-+1.2abc", "[%+-1.2a]bc"},{"%-1.2abc", "[%-1.2a]bc"},
}
func TestFlagParser(t *testing.T) {var flagprinter flagPrinterfor _, tt := range flagtests {t.Run(tt.in, func(t *testing.T) {s := Sprintf(tt.in, &flagprinter)if s != tt.out {t.Errorf("got %q, want %q", s, tt.out)}})}
}var flagtests = []struct {in  stringout string
}{{"%a", "[%a]"},{"%-a", "[%-a]"},{"%+a", "[%+a]"},{"%#a", "[%#a]"},{"% a", "[% a]"},{"%0a", "[%0a]"},{"%1.2a", "[%1.2a]"},{"%-1.2a", "[%-1.2a]"},{"%+1.2a", "[%+1.2a]"},{"%-+1.2a", "[%+-1.2a]"},{"%-+1.2abc", "[%+-1.2a]bc"},{"%-1.2abc", "[%-1.2a]bc"},
}
func TestFlagParser(t *testing.T) {var flagprinter flagPrinterfor _, tt := range flagtests {t.Run(tt.in, func(t *testing.T) {s := Sprintf(tt.in, &flagprinter)if s != tt.out {t.Errorf("got %q, want %q", s, tt.out)}})}
}

通常表格是匿名结构体切片,可以定义结构体或使用已经存在的结构进行结构体数组声明。name属性用来描述特定的测试用例。

并行测试
表格驱动测试中通常会定义比较多的测试用例,而Go语言又天生支持并发,所以很容易发挥自身并发优势将表格驱动测试并行化。 想要在单元测试过程中使用并行测试,可以像下面的代码示例中那样通过添加t.Parallel()来实现。

func TestSplitAll(t *testing.T) {t.Parallel()  // 将 TLog 标记为能够与其他测试并行运行// 定义测试表格// 这里使用匿名结构体定义了若干个测试用例// 并且为每个测试用例设置了一个名称tests := []struct {name  stringinput stringsep   stringwant  []string}{{"base case", "a:b:c", ":", []string{"a", "b", "c"}},{"wrong sep", "a:b:c", ",", []string{"a:b:c"}},{"more sep", "abcd", "bc", []string{"a", "d"}},{"leading sep", "沙河有沙又有河", "沙", []string{"", "河有", "又有河"}},}// 遍历测试用例for _, tt := range tests {tt := tt  // 注意这里重新声明tt变量(避免多个goroutine中使用了相同的变量)t.Run(tt.name, func(t *testing.T) { // 使用t.Run()执行子测试t.Parallel()  // 将每个测试用例标记为能够彼此并行运行got := Split(tt.input, tt.sep)if !reflect.DeepEqual(got, tt.want) {t.Errorf("expected:%#v, got:%#v", tt.want, got)}})}
}

测试覆盖率
Go提供内置功能来检查你的代码覆盖率,即使用go test -cover来查看测试覆盖率。
Go还提供了一个额外的-coverprofile参数,用来将覆盖率相关的记录信息输出到一个文件。例如:

 go test -cover -coverprofile=c.out

上面的命令会将覆盖率相关的信息输出到当前文件夹下面的c.out文件中。
然后我们执行go tool cover -html=c.out,使用cover工具来处理生成的记录信息,该命令会打开本地的浏览器窗口生成一个HTML报告。(用绿色标记的语句块表示被覆盖了,而红色的表示没有被覆盖。)

testify/asserttestify

是一个社区非常流行的Go单元测试工具包,其中使用最多的功能就是它提供的断言工具——testify/asserttestify/require
1.安装

go get github.com/stretchr/testify

TestSplit测试函数中就使用了reflect.DeepEqual来判断期望结果与实际结果是否一致。

t.Run(tt.name, func(t *testing.T) { // 使用t.Run()执行子测试got := Split(tt.input, tt.sep)if !reflect.DeepEqual(got, tt.want) {t.Errorf("expected:%#v, got:%#v", tt.want, got)}
})

使用testify/assert之后就能将上述判断过程简化如下:

t.Run(tt.name, func(t *testing.T) { // 使用t.Run()执行子测试got := Split(tt.input, tt.sep)assert.Equal(t, got, tt.want)  // 使用assert提供的断言函数
})

有多个断言语句时,还可以使用assert := assert.New(t)创建一个assert对象,它拥有前面所有的断言方法,只是不需要再传入Testing.T参数了。

func TestSomething(t *testing.T) {assert := assert.New(t)// assert equalityassert.Equal(123, 123, "they should be equal")// assert inequalityassert.NotEqual(123, 456, "they should not be equal")// assert for nil (good for errors)assert.Nil(object)// assert for not nil (good when you expect something)if assert.NotNil(object) {// now we know that object isn't nil, we are safe to make// further assertions without causing any errorsassert.Equal("Something", object.Value)}

testify/require拥有testify/assert所有断言函数,它们的唯一区别就是——testify/require遇到失败的用例会立即终止本次测试。

此外,testify包还提供了mock、http等其他测试工具.
testify官方文档

网络测试

在Web开发场景下的单元测试,如果涉及到HTTP请求推荐使用Go标准库 net/http/httptest 进行测试,能够显著提高测试效率。

gin框架为例,演示如何为http server编写单元测试
假设我们的业务逻辑是搭建一个http server端,对外提供HTTP服务。我们编写了一个helloHandler函数,用来处理用户请求。

// gin.go
package httptest_demoimport ("fmt""net/http""github.com/gin-gonic/gin"
)// Param 请求参数
type Param struct {Name string `json:"name"`
}// helloHandler /hello请求处理函数
func helloHandler(c *gin.Context) {var p Paramif err := c.ShouldBindJSON(&p); err != nil {c.JSON(http.StatusOK, gin.H{"msg": "we need a name",})return}c.JSON(http.StatusOK, gin.H{"msg": fmt.Sprintf("hello %s", p.Name),})
}// SetupRouter 路由
func SetupRouter() *gin.Engine {router := gin.Default()router.POST("/hello", helloHandler)return router
}

为helloHandler函数编写单元测试,这种情况下我们就可以使用httptest这个工具mock一个HTTP请求和响应记录器,让我们的server端接收并处理我们mock的HTTP请求,同时使用响应记录器来记录server端返回的响应内容。

// gin_test.go
package httptest_demoimport ("encoding/json""net/http""net/http/httptest""strings""testing""github.com/stretchr/testify/assert"
)func Test_helloHandler(t *testing.T) {// 定义两个测试用例tests := []struct {name   stringparam  stringexpect string}{{"base case", `{"name": "liwenzhou"}`, "hello liwenzhou"},{"bad case", "", "we need a name"},}r := SetupRouter()for _, tt := range tests {t.Run(tt.name, func(t *testing.T) {// mock一个HTTP请求req := httptest.NewRequest("POST",                      // 请求方法"/hello",                    // 请求URLstrings.NewReader(tt.param), // 请求参数)// mock一个响应记录器w := httptest.NewRecorder()// 让server端处理mock请求并记录返回的响应内容r.ServeHTTP(w, req)// 校验状态码是否符合预期assert.Equal(t, http.StatusOK, w.Code)// 解析并检验响应内容是否复合预期var resp map[string]stringerr := json.Unmarshal([]byte(w.Body.String()), &resp)assert.Nil(t, err)assert.Equal(t, tt.expect, resp["msg"])})}
}

介绍了如何在HTTP Server服务类场景下为请求处理函数编写单元测试,那么如果我们是在代码中请求外部API的场景(比如通过API调用其他服务获取返回值)又该怎么编写单元测试呢?
例如,我们有以下业务逻辑代码,依赖外部API:http://your-api.com/post提供的数据。

// api.go// ReqParam API请求参数
type ReqParam struct {X int `json:"x"`
}// Result API返回结果
type Result struct {Value int `json:"value"`
}func GetResultByAPI(x, y int) int {p := &ReqParam{X: x}b, _ := json.Marshal(p)// 调用其他服务的APIresp, err := http.Post("http://your-api.com/post","application/json",bytes.NewBuffer(b),)if err != nil {return -1}body, _ := ioutil.ReadAll(resp.Body)var ret Resultif err := json.Unmarshal(body, &ret); err != nil {return -1}// 这里是对API返回的数据做一些逻辑处理return ret.Value + y
}

在对类似上述这类业务代码编写单元测试的时候,如果不想在测试过程中真正去发送请求或者依赖的外部接口还没有开发完成时,我们可以在单元测试中对依赖的API进行mock.
推荐使用gock这个库
gock 安装

go get -u gopkg.in/h2non/gock.v1
// api_test.go
package gock_demoimport ("testing""github.com/stretchr/testify/assert""gopkg.in/h2non/gock.v1"
)func TestGetResultByAPI(t *testing.T) {defer gock.Off() // 测试执行后刷新挂起的mock// mock 请求外部api时传参x=1返回100gock.New("http://your-api.com").Post("/post").MatchType("json").JSON(map[string]int{"x": 1}).Reply(200).JSON(map[string]int{"value": 100})// 调用我们的业务函数res := GetResultByAPI(1, 1)// 校验返回结果是否符合预期assert.Equal(t, res, 101)// mock 请求外部api时传参x=2返回200gock.New("http://your-api.com").Post("/post").MatchType("json").JSON(map[string]int{"x": 2}).Reply(200).JSON(map[string]int{"value": 200})// 调用我们的业务函数res = GetResultByAPI(2, 2)// 校验返回结果是否符合预期assert.Equal(t, res, 202)assert.True(t, gock.IsDone()) // 断言mock被触发
}

Go单测 MySQL和Redis测试

1. go-sqlmock
sqlmock是一个实现 sql/driver 的mock库。它不需要建立真正的数据库连接就可以在测试中模拟任何 sql 驱动程序的行为。使用它可以很方便的在编写单元测试的时候mock sql语句的执行结果。

安装:go get github.com/DATA-DOG/go-sqlmock

使用示例
用的是go-sqlmock官方文档中提供的基础示例代码;实现了一个recordStats函数用来记录用户浏览商品时产生的相关数据。具体实现的功能是在一个事务中进行以下两次SQL操作: - 在products表中将当前商品的浏览次数+1 - 在product_viewers表中记录浏览当前商品的用户id

// app.go
package mainimport "database/sql"// recordStats 记录用户浏览产品信息
func recordStats(db *sql.DB, userID, productID int64) (err error) {// 开启事务// 操作products和product_viewers两张表tx, err := db.Begin()if err != nil {return}defer func() {switch err {case nil:err = tx.Commit()default:tx.Rollback()}}()// 更新products表if _, err = tx.Exec("UPDATE products SET views = views + 1"); err != nil {return}// product_viewers表中插入一条数据if _, err = tx.Exec("INSERT INTO product_viewers (user_id, product_id) VALUES (?, ?)",userID, productID); err != nil {return}return
}func main() {// 注意:测试的过程中并不需要真正的连接db, err := sql.Open("mysql", "root@/blog")if err != nil {panic(err)}defer db.Close()// userID为1的用户浏览了productID为5的产品if err = recordStats(db, 1 /*some user id*/, 5 /*some product id*/); err != nil {panic(err)}
}

为代码中的recordStats函数编写单元测试,但是又不想在测试过程中连接真实的数据库进行测试。这个时候我们就可以像下面示例代码中那样使用sqlmock工具去mock数据库操作

package mainimport ("fmt""testing""github.com/DATA-DOG/go-sqlmock"
)// TestShouldUpdateStats sql执行成功的测试用例
func TestShouldUpdateStats(t *testing.T) {// mock一个*sql.DB对象,不需要连接真实的数据库db, mock, err := sqlmock.New()if err != nil {t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)}defer db.Close()// mock执行指定SQL语句时的返回结果mock.ExpectBegin()mock.ExpectExec("UPDATE products").WillReturnResult(sqlmock.NewResult(1, 1))mock.ExpectExec("INSERT INTO product_viewers").WithArgs(2, 3).WillReturnResult(sqlmock.NewResult(1, 1))mock.ExpectCommit()// 将mock的DB对象传入我们的函数中if err = recordStats(db, 2, 3); err != nil {t.Errorf("error was not expected while updating stats: %s", err)}// 确保期望的结果都满足if err := mock.ExpectationsWereMet(); err != nil {t.Errorf("there were unfulfilled expectations: %s", err)}
}// TestShouldRollbackStatUpdatesOnFailure sql执行失败回滚的测试用例
func TestShouldRollbackStatUpdatesOnFailure(t *testing.T) {db, mock, err := sqlmock.New()if err != nil {t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)}defer db.Close()mock.ExpectBegin()mock.ExpectExec("UPDATE products").WillReturnResult(sqlmock.NewResult(1, 1))mock.ExpectExec("INSERT INTO product_viewers").WithArgs(2, 3).WillReturnError(fmt.Errorf("some error"))mock.ExpectRollback()// now we execute our methodif err = recordStats(db, 2, 3); err == nil {t.Errorf("was expecting an error, but there was none")}// we make sure that all expectations were metif err := mock.ExpectationsWereMet(); err != nil {t.Errorf("there were unfulfilled expectations: %s", err)}
}

上面的代码中,定义了一个执行成功的测试用例和一个执行失败回滚的测试用例,确保我们代码中的每个逻辑分支都能被测试到,提高单元测试覆盖率的同时也保证了代码的健壮性。
在很多使用ORM工具的场景下,也可以使用go-sqlmock库mock数据库操作进行测试。

2.miniredis
除了经常用到MySQL外,Redis在日常开发中也会经常用到;miniredis是一个纯go实现的用于单元测试的redis server。它是一个简单易用的、基于内存的redis替代品,它具有真正的TCP接口,你可以把它当成是redis版本的net/http/httptest

安装go get github.com/alicebob/miniredis/v2

使用案例
这里以github.com/go-redis/redis库为例,编写了一个包含若干Redis操作的DoSomethingWithRedis函数。

// redis_op.go
package miniredis_demoimport ("context""github.com/go-redis/redis/v8" // 注意导入版本"strings""time"
)const (KeyValidWebsite = "app:valid:website:list"
)func DoSomethingWithRedis(rdb *redis.Client, key string) bool {// 这里可以是对redis操作的一些逻辑ctx := context.TODO()if !rdb.SIsMember(ctx, KeyValidWebsite, key).Val() {return false}val, err := rdb.Get(ctx, key).Result()if err != nil {return false}if !strings.HasPrefix(val, "https://") {val = "https://" + val}// 设置 blog key 五秒过期if err := rdb.Set(ctx, "blog", val, 5*time.Second).Err(); err != nil {return false}return true
}

下面的代码是我使用miniredis库为DoSomethingWithRedis函数编写的单元测试代码,其中miniredis不仅支持mock常用的Redis操作,还提供了很多实用的帮助函数,例如检查key的值是否与预期相等的s.CheckGet()和帮助检查key过期时间的s.FastForward()

// redis_op_test.gopackage miniredis_demoimport ("github.com/alicebob/miniredis/v2""github.com/go-redis/redis/v8""testing""time"
)func TestDoSomethingWithRedis(t *testing.T) {// mock一个redis servers, err := miniredis.Run()if err != nil {panic(err)}defer s.Close()// 准备数据s.Set("q1mi", "liwenzhou.com")s.SAdd(KeyValidWebsite, "q1mi")// 连接mock的redis serverrdb := redis.NewClient(&redis.Options{Addr: s.Addr(), // mock redis server的地址})// 调用函数ok := DoSomethingWithRedis(rdb, "q1mi")if !ok {t.Fatal()}// 可以手动检查redis中的值是否复合预期if got, err := s.Get("blog"); err != nil || got != "https://liwenzhou.com" {t.Fatalf("'blog' has the wrong value")}// 也可以使用帮助工具检查s.CheckGet(t, "blog", "https://liwenzhou.com")// 过期检查s.FastForward(5 * time.Second) // 快进5秒if s.Exists("blog") {t.Fatal("'blog' should not have existed anymore")}
}

除了使用miniredis搭建本地redis server这种方法外,还可以使用各种打桩工具对具体方法进行打桩。在编写单元测试时具体使用哪种mock方式还是要根据实际情况来决定。

mock接口测试

如何在单元测试中使用gomockgostub工具mock接口和打桩

1.gomock

gomock是Go官方提供的测试框架,它在内置的testing包或其他环境中都能够很方便的使用。我们使用它对代码中的那些接口类型进行mock,方便编写单元测试。

安装mockgen
go install github.com/golang/mock/mockgen@v1.6.0
(2023 6月停止维护)
go get github.com/uber-go/mock   (uber提供的代替)

运行mockgen
mockgen 有两种操作模式:源码(source)模式和反射(reflect)模式。
源码模式
源码模式根据源文件mock接口。它是通过使用-source标志启用。在这个模式下可能有用的其他标志是 -imports-aux_files

	example:mockgen -source=foo.go [other options]

反射模式
反射模式通过构建使用反射来理解接口的程序,来mock接口。它是通过传递两个非标志参数来启用的:一个导入路径和一个逗号分隔的符号列表。可以使用 ”.”引用当前路径的包。

example:mockgen database/sql/driver Conn,Driver# Convenient for `go:generate`.mockgen . Conn,Driver

flags
mockgen 命令用来为给定一个包含要mock的接口的Go源文件,生成mock类源代码。它支持以下标志:

  • -source:包含要mock的接口的文件。
  • -destination:生成的源代码写入的文件。如果不设置此项,代码将打印到标准输出。
  • -package:用于生成的模拟类源代码的包名。如果不设置此项包名默认在原包名前添加mock_前缀。
  • -imports:在生成的源代码中使用的显式导入列表。值为foo=bar/baz形式的逗号分隔的元素列表,其中bar/baz是要导入的包,foo是要在生成的源代码中用于包的标识符。
  • -aux_files:需要参考以解决的附加文件列表,例如在不同文件中定义的嵌入式接口。指定的值应为foo=bar/baz.go形式的以逗号分隔的元素列表,其中bar/baz.go是源文件,foo是-source文件使用的文件的包名。
  • -build_flags:(仅反射模式)一字不差地传递标志给go build
  • -mock_names:生成的模拟的自定义名称列表。这被指定为一个逗号分隔的元素列表,形式为Repository = MockSensorRepository,Endpoint=MockSensorEndpoint,其中Repository是接口名称,mockSensorrepository是所需的mock名称(mock工厂方法和mock记录器将以mock命名)。如果其中一个接口没有指定自定义名称,则将使用默认命名约定。
  • -self_package:生成的代码的完整包导入路径。使用此flag的目的是通过尝试包含自己的包来防止生成代码中的循环导入。如果mock的包被设置为它的一个输入(通常是主输入),并且输出是stdio,那么mockgen就无法检测到最终的输出包,这种情况就会发生。设置此标志将告诉mockgen 排除哪个导入
  • -copyright_file:用于将版权标头添加到生成的源代码中的版权文件
  • -debug_parser:仅打印解析器结果
  • -exec_only:(反射模式) 如果设置,则执行此反射程序
  • -prog_only:(反射模式)只生成反射程序;将其写入标准输出并退出。
  • -write_package_comment:如果为true,则写入包文档注释 (godoc)。(默认为true)

构建mock
以日常开发中经常用到的数据库操作为例,讲解一下如何使用gomock来mock接口的单元测试。
假设有查询MySQL数据库的业务代码如下,其中DB是一个自定义的接口类型:

// db.go// DB 数据接口
type DB interface {Get(key string)(int, error)Add(key string, value int) error
}// GetFromDB 根据key从DB查询数据的函数
func GetFromDB(db DB, key string) int {if v, err := db.Get(key);err == nil{return v}return -1
}

我们现在要为GetFromDB函数编写单元测试代码,可是我们又不能在单元测试过程中连接真实的数据库,这个时候就需要mock DB这个接口来方便进行单元测试。

使用上面提到的 mockgen 工具来为生成相应的mock代码。通过执行下面的命令,我们就能在当前项目下生成一个mocks文件夹,里面存放了一个db_mock.go文件。

 mockgen -source=db.go -destination=mocks/db_mock.go -package=mocks

db_mock.go文件中的内容就是mock相关接口的代码了。

我们通常不需要编辑它,只需要在单元测试中按照规定的方式使用它们就可以了。例如,我们编写TestGetFromDB 函数如下:

// db_test.gofunc TestGetFromDB(t *testing.T) {// 创建gomock控制器,用来记录后续的操作信息ctrl := gomock.NewController(t)// 断言期望的方法都被执行// Go1.14+的单测中不再需要手动调用该方法defer ctrl.Finish()// 调用mockgen生成代码中的NewMockDB方法// 这里mocks是我们生成代码时指定的package名称m := mocks.NewMockDB(ctrl)// 打桩(stub)// 当传入Get函数的参数为liwenzhou.com时返回1和nilm.EXPECT().Get(gomock.Eq("liwenzhou.com")). // 参数Return(1, nil).                  // 返回值Times(1)                         // 调用次数// 调用GetFromDB函数时传入上面的mock对象mif v := GetFromDB(m, "liwenzhou.com"); v != 1 {t.Fatal()}
}

打桩(stub)

软件测试中的打桩是指用一些代码(桩stub)代替目标代码,通常用来屏蔽或补齐业务逻辑中的关键代码方便进行单元测试。

屏蔽:不想在单元测试用引入数据库连接等重资源
补齐:依赖的上下游函数或方法还未实现

上面代码中就用到了打桩,当传入Get函数的参数为liwenzhou.com时就返回1, nil的返回值。

gomock支持针对参数返回值调用次数调用顺序等进行打桩操作。

参数
参数相关的用法有:

  • gomock.Eq(value):表示一个等价于value值的参数
  • gomock.Not(value):表示一个非value值的参数
  • gomock.Any():表示任意值的参数
  • gomock.Nil():表示空值的参数
  • SetArg(n, value):设置第n(从0开始)个参数的值,通常用于指针参数或切片

具体示例如下:

m.EXPECT().Get(gomock.Not(“q1mi”)).Return(10, nil)
m.EXPECT().Get(gomock.Any()).Return(20, nil)
m.EXPECT().Get(gomock.Nil()).Return(-1, nil)

单独说一下SetArg的适用场景,假设你有一个需要mock的接口如下:

type YourInterface {SetValue(arg *int)
}

此时,打桩的时候就可以使用SetArg来修改参数的值。

m.EXPECT().SetValue(gomock.Any()).SetArg(0, 7)  // 将SetValue的第一个参数设置为7
}

返回值
gomock中跟返回值相关的用法有以下几个:

  • Return():返回指定值
  • Do(func):执行操作,忽略返回值
  • DoAndReturn(func):执行并返回指定值

例如:

m.EXPECT().Get(gomock.Any()).Return(20, nil)
m.EXPECT().Get(gomock.Any()).Do(func(key string) {t.Logf("input key is %v\n", key)
})
m.EXPECT().Get(gomock.Any()).DoAndReturn(func(key string)(int, error) {t.Logf("input key is %v\n", key)return 10, nil
})

调用次数
使用gomock工具mock的方法都会有期望被调用的次数,默认每个mock方法只允许被调用一次

m.EXPECT().Get(gomock.Eq("liwenzhou.com")). // 参数Return(1, nil).                  // 返回值Times(1)                         // 设置Get方法期望被调用次数为1// 调用GetFromDB函数时传入上面的mock对象m
if v := GetFromDB(m, "liwenzhou.com"); v != 1 {t.Fatal()
}
// 再次调用上方mock的Get方法时不满足调用次数为1的期望
if v := GetFromDB(m, "liwenzhou.com"); v != 1 {t.Fatal()
}

gomock为我们提供了如下方法设置期望被调用的次数。

  • Times() 断言 Mock 方法被调用的次数。
  • MaxTimes() 最大次数。
  • MinTimes() 最小次数。
  • AnyTimes() 任意次数(包括 0 次)。

调用顺序
gomock还支持使用InOrder方法指定mock方法的调用顺序:

// 指定顺序
gomock.InOrder(m.EXPECT().Get("1"),m.EXPECT().Get("2"),m.EXPECT().Get("3"),
)// 按顺序调用
GetFromDB(m, "1")
GetFromDB(m, "2")
GetFromDB(m, "3")

此外知名的Go测试库testify目前也提供类似的mock工具—testify/mock和mockery。

GoStub

GoStub也是一个单元测试中的打桩工具,它支持为全局变量、函数等打桩。
不过我个人感觉它为函数打桩不太方便,我一般在单元测试中只会使用它来为全局变量打桩。

安装: go get github.com/prashantv/gostub

使用示例
官方文档中的示例代码演示如何使用gostub为全局变量打桩。

// app.go var (configFile = "config.json"maxNum = 10
)func GetConfig() ([]byte, error) {return ioutil.ReadFile(configFile)
}func ShowNumber()int{// ...return maxNum
}

上面代码中定义了两个全局变量和两个使用全局变量的函数,我们现在为这两个函数编写单元测试。

// app_test.goimport ("github.com/prashantv/gostub""testing"
)func TestGetConfig(t *testing.T) {// 为全局变量configFile打桩,给它赋值一个指定文件stubs := gostub.Stub(&configFile, "./test.toml")defer stubs.Reset()  // 测试结束后重置// 下面是测试的代码data, err := GetConfig()if err != nil {t.Fatal()}// 返回的data的内容就是上面/tmp/test.config文件的内容t.Logf("data:%s\n", data)
}func TestShowNumber(t *testing.T) {stubs := gostub.Stub(&maxNum, 20)defer stubs.Reset()// 下面是一些测试的代码res := ShowNumber()if res != 20 {t.Fatal()}
}

从上面的示例中我们可以看到,在单元测试中使用gostub可以很方便的对全局变量进行打桩,将其mock成我们预期的值从而进行测试。

使用monkey打桩

一个更强大的打桩工具——monkey,它支持为任意函数及方法进行打桩。
介绍
monkey是一个Go单元测试中十分常用的打桩工具,它在运行时通过汇编语言重写可执行文件,将目标函数或方法的实现跳转到桩实现,其原理类似于热补丁。

monkey库很强大,但是使用时需注意以下事项:

  • monkey不支持内联函数,在测试的时候需要通过命令行参数-gcflags=-l关闭Go语言的内联优化。
  • monkey 不是线程安全的,所以不要把它用到并发的单元测试中。

安装

go get bou.ke/monkey

使用示例
假设你们公司中台提供了一个用户中心的库varys,使用这个库可以很方便的根据uid获取用户相关信息。但是当你编写代码的时候这个库还没实现,或者这个库要经过内网请求但你现在没这能力,这个时候要为MyFunc编写单元测试,就需要做一些mock工作。

// func.gofunc MyFunc(uid int64)string{u, err := varys.GetInfoByUID(uid)if err != nil {return "welcome"}// 这里是一些逻辑代码...return fmt.Sprintf("hello %s\n", u.Name)
}

使用monkey库对varys.GetInfoByUID进行打桩。

// func_test.gofunc TestMyFunc(t *testing.T) {// 对 varys.GetInfoByUID 进行打桩// 无论传入的uid是多少,都返回 &varys.UserInfo{Name: "liwenzhou"}, nilmonkey.Patch(varys.GetInfoByUID, func(int64)(*varys.UserInfo, error) {return &varys.UserInfo{Name: "liwenzhou"}, nil})ret := MyFunc(123)if !strings.Contains(ret, "liwenzhou"){t.Fatal()}
}

执行单元测试: 注意:这里为防止内联优化添加了-gcflags=-l参数。
go test -run TestMyFunc -v -gcflags=-l

除了对函数进行mock外monkey也支持对方法进行mock。

// method.gotype User struct {Name stringBirthday string
}// CalcAge 计算用户年龄
func (u *User) CalcAge() int {t, err := time.Parse("2006-01-02", u.Birthday)if err != nil {return -1}return int(time.Now().Sub(t).Hours()/24.0)/365
}// GetInfo 获取用户相关信息
func (u *User) GetInfo()string{age := u.CalcAge()if age <= 0 {return fmt.Sprintf("%s很神秘,我们还不了解ta。", u.Name)}return fmt.Sprintf("%s今年%d岁了,ta是我们的朋友。", u.Name, age)
}

GetInfo编写单元测试的时候CalcAge方法的功能还未完成,这个时候我们可以使用monkey进行打桩。

// method_test.gofunc TestUser_GetInfo(t *testing.T) {var u = &User{Name:     "q1mi",Birthday: "1990-12-20",}// 为对象方法打桩monkey.PatchInstanceMethod(reflect.TypeOf(u), "CalcAge", func(*User)int {return 18})ret := u.GetInfo()  // 内部调用u.CalcAge方法时会返回18if !strings.Contains(ret, "朋友"){t.Fatal()}
}

monkey基本上能满足我们在单元测试中打桩的任何需求。

社区中还有一个参考monkey库实现的gomonkey库,原理和使用过程基本相似,。除此之外社区里还有一些其他打桩工具如GoStub

go-convey

1.安装
需要使用goconvey的Web UI程序,请执行下面的命令安装可执行程序
go install github.com/smartystreets/goconvey@latest
在项目中引入依赖
go get github.com/smartystreets/goconvey
官网:goconvey.co

2.两个关键方法Convey()So()
例如:Convey("备注",t *testing.T,func{})
So(testFunc(),ShouldEqual,xx)
convey() 、so()方法可以嵌套使用)


3.运行测试3.1 go原生测试方法:`go test` `go test -v`3.2 安装测试覆盖率工具`go get code.google.com/p/go.tools/cmd/cover`使用GoConvey提供的自动化编译测试:`goconvey`进入浏览器访问地址localhost:8080/查看结果**使用示例**
用`goconvey`来为最开始的基础示例中的Split函数编写单元测试。Split函数如下:
```go
// split.gofunc Split(s, sep string) (result []string) {result = make([]string, 0, strings.Count(s, sep)+1)i := strings.Index(s, sep)for i > -1 {result = append(result, s[:i])s = s[i+len(sep):]i = strings.Index(s, sep)}result = append(result, s)return
}

单元测试文件内容如下:


import ("testing"c "github.com/smartystreets/goconvey/convey"  // 别名导入
)func TestSplit(t *testing.T) {c.Convey("基础用例", t, func() {var (s      = "a:b:c"sep    = ":"expect = []string{"a", "b", "c"})got := Split(s, sep)c.So(got, c.ShouldResemble, expect) // 断言})c.Convey("不包含分隔符用例", t, func() {var (s      = "a:b:c"sep    = "|"expect = []string{"a:b:c"})got := Split(s, sep)c.So(got, c.ShouldResemble, expect) // 断言})
}

goconvey还支持在单元测试中根据需要嵌套调用,比如:

func TestSplit(t *testing.T) {// ...// 只需要在顶层的Convey调用时传入tc.Convey("分隔符在开头或结尾用例", t, func() {tt := []struct {name   strings      stringsep    stringexpect []string}{{"分隔符在开头", "*1*2*3", "*", []string{"", "1", "2", "3"}},{"分隔符在结尾", "1+2+3+", "+", []string{"1", "2", "3", ""}},}for _, tc := range tt {c.Convey(tc.name, func() { // 嵌套调用Conveygot := Split(tc.s, tc.sep)c.So(got, c.ShouldResemble, tc.expect)})}})
}

这样输出最终的测试结果时也会分层级显示。

断言方法
GoConvey为我们提供了很多种类断言方法在So()函数中使用。

一般相等类

So(thing1, ShouldEqual, thing2)
So(thing1, ShouldNotEqual, thing2)
So(thing1, ShouldResemble, thing2) // 用于数组、切片、map和结构体相等 So(thing1,
ShouldNotResemble, thing2)
So(thing1, ShouldPointTo, thing2)
So(thing1, ShouldNotPointTo, thing2)
So(thing1, ShouldBeNil)
So(thing1, ShouldNotBeNil)
So(thing1, ShouldBeTrue) So(thing1,
ShouldBeFalse) So(thing1, ShouldBeZeroValue)

数字数量比较类

So(1, ShouldBeGreaterThan, 0)
So(1, ShouldBeGreaterThanOrEqualTo, 0)
So(1, ShouldBeLessThan, 2)
So(1, ShouldBeLessThanOrEqualTo, 2)
So(1.1,ShouldBeBetween, .8, 1.2)
So(1.1, ShouldNotBeBetween, 2, 3)
So(1.1,ShouldBeBetweenOrEqual, .9, 1.1)
So(1.1, ShouldNotBeBetweenOrEqual,1000, 2000)
So(1.0, ShouldAlmostEqual, 0.99999999, .0001) //tolerance is optional; default 0.0000000001
So(1.0,ShouldNotAlmostEqual, 0.9, .0001)

包含类

So([]int{2, 4, 6}, ShouldContain, 4)
So([]int{2, 4, 6},ShouldNotContain, 5)
So(4, ShouldBeIn, …[]int{2, 4, 6})
So(4,ShouldNotBeIn, …[]int{1, 3, 5})
So([]int{}, ShouldBeEmpty)
So([]int{1}, ShouldNotBeEmpty)
So(map[string]string{“a”: “b”}, ShouldContainKey, “a”)
So(map[string]string{“a”: “b”},ShouldNotContainKey, “b”)
So(map[string]string{“a”: “b”},ShouldNotBeEmpty)
So(map[string]string{}, ShouldBeEmpty)
So(map[string]string{“a”: “b”}, ShouldHaveLength, 1) // supports map,slice, chan, and string

字符串类

So(“asdf”, ShouldStartWith, “as”)
So(“asdf”, ShouldNotStartWith, “df”)
So(“asdf”, ShouldEndWith, “df”)
So(“asdf”, ShouldNotEndWith, “df”)
So(“asdf”, ShouldContainSubstring, “稍等一下”) // optional ‘expected occurences’ arguments?
So(“asdf”, ShouldNotContainSubstring, “er”)
So(“adsf”, ShouldBeBlank) So(“asdf”, ShouldNotBeBlank)

panic类

So(func(), ShouldPanic)
So(func(), ShouldNotPanic)
So(func(),ShouldPanicWith, “”) // or errors.New(“something”)
So(func(),ShouldNotPanicWith, “”) // or errors.New(“something”)

类型检查类

So(1, ShouldHaveSameTypeAs, 0)
So(1, ShouldNotHaveSameTypeAs, “asdf”)

时间和时间间隔类

So(time.Now(), ShouldHappenBefore, time.Now())
So(time.Now(),ShouldHappenOnOrBefore, time.Now())
So(time.Now(), ShouldHappenAfter,time.Now())
So(time.Now(), ShouldHappenOnOrAfter, time.Now())
So(time.Now(), ShouldHappenBetween, time.Now(), time.Now())
So(time.Now(), ShouldHappenOnOrBetween, time.Now(), time.Now())
So(time.Now(), ShouldNotHappenOnOrBetween, time.Now(), time.Now())
So(time.Now(), ShouldHappenWithin, duration, time.Now())
So(time.Now(), ShouldNotHappenWithin, duration, time.Now())

自定义断言方法
如果上面列出来的断言方法都不能满足你的需要,那么你还可以按照下面的格式自定义一个断言方法。
注意:<>中的内容是你需要按照实际需求替换的内容。

func should<do-something>(actual interface{}, expected ...interface{}) string {if <some-important-condition-is-met(actual, expected)> {return ""   // 返回空字符串表示断言通过}return "<一些描述性消息详细说明断言失败的原因...>"
}

编写可测试的代码

编写可测试的代码可能比编写单元测试本身更加重要

剔除干扰因素

假设我们现在有一个根据时间,判断报警信息发送速率的模块,白天工作时间允许大量发送报警信息,而晚上则减小发送速率,凌晨不允许发送报警短信。

// judgeRate 报警速率决策函数
func judgeRate() int {now := time.Now()switch hour := now.Hour(); {case hour >= 8 && hour < 20:return 10case hour >= 20 && hour <= 23:return 1}return -1
}

这个函数内部使用了time.Now()来获取系统的当前时间作为判断的依据,看起来很合理。

但是这个函数现在隐式包含了一个不确定因素——时间。在不同的时刻我们调用这个函数都可能会得到不一样的结果。想象一下,我们该如何为这个函数编写单元测试呢?

如果不修改系统时间,那么我们就无法为这个函数编写单元测试,这个函数成了“不可测试的代码”(当然可以使用打桩工具对time.Now进行打桩,但那不是本文要强调的重点)。

接下来我们该如何改造它?

// judgeRateByTime 报警速率决策函数
func judgeRateByTime(now time.Time) int {switch hour := now.Hour(); {case hour >= 8 && hour < 20:return 10case hour >= 20 && hour <= 23:return 1}return -1
}

这样我们不仅解决了函数与系统时间的紧耦合,而且还扩展了函数的功能,现在我们可以根据需要获取任意时刻的速率值。为改造后的judgeRateByTime编写单元测试也更方便了。

func Test_judgeRateByTime(t *testing.T) {tests := []struct {name stringarg  time.Timewant int}{{name: "工作时间",arg:  time.Date(2022, 2, 18, 11, 22, 33, 0, time.UTC),want: 10,},{name: "晚上",arg:  time.Date(2022, 2, 18, 22, 22, 33, 0, time.UTC),want: 1,},{name: "凌晨",arg:  time.Date(2022, 2, 18, 2, 22, 33, 0, time.UTC),want: -1,},}for _, tt := range tests {t.Run(tt.name, func(t *testing.T) {if got := judgeRateByTime(tt.arg); got != tt.want {t.Errorf("judgeRateByTime() = %v, want %v", got, tt.want)}})}
}

接口抽象进行解耦

同样是函数中隐式依赖的问题,假设我们实现了一个获取店铺客单价的需求,它完成的功能就像下面的示例函数。

// GetAveragePricePerStore 每家店的人均价
func GetAveragePricePerStore(storeName string) (int64, error) {res, err := http.Get("https://liwenzhou.com/api/orders?storeName=" + storeName)if err != nil {return 0, err}defer res.Body.Close()var orders []Orderif err := json.NewDecoder(res.Body).Decode(&orders); err != nil {return 0, err}if len(orders) == 0 {return 0, nil}var (p int64n int64)for _, order := range orders {p += order.Pricen += order.Num}return p / n, nil
}

之前的章节中我们介绍了如何为上面的代码编写单元测试,但是我们如何避免每次单元测试时都发起真实的HTTP请求呢?亦或者后续我们改变了获取数据的方式(直接读取缓存或改为RPC调用)这个函数该怎么兼容呢?

我们将函数中获取数据的部分抽象为接口类型来优化我们的程序,使其支持模块化的数据源配置

// OrderInfoGetter 订单信息提供者
type OrderInfoGetter interface {GetOrders(string) ([]Order, error)
}

然后定义一个API类型,它拥有一个通过HTTP请求获取订单数据的GetOrders方法,正好实现OrderInfoGetter接口。

/ HttpApi HTTP API类型
type HttpApi struct{}// GetOrders 通过HTTP请求获取订单数据的方法
func (a HttpApi) GetOrders(storeName string) ([]Order, error) {res, err := http.Get("https://liwenzhou.com/api/orders?storeName=" + storeName)if err != nil {return nil, err}defer res.Body.Close()var orders []Orderif err := json.NewDecoder(res.Body).Decode(&orders); err != nil {return nil, err}return orders, nil
}

将原来的 GetAveragePricePerStore 函数修改为以下实现。

// GetAveragePricePerStore 每家店的人均价
func GetAveragePricePerStore(getter OrderInfoGetter, storeName string) (int64, error) {orders, err := getter.GetOrders(storeName)if err != nil {return 0, err}if len(orders) == 0 {return 0, nil}var (p int64n int64)for _, order := range orders {p += order.Pricen += order.Num}return p / n, nil
}

经过这番改动之后,我们的代码就能很容易地写出单元测试代码。例如,对于不方便直接请求的HTTP API, 我们就可以进行 mock 测试。

// Mock 一个mock类型
type Mock struct{}// GetOrders mock获取订单数据的方法
func (m Mock) GetOrders(string) ([]Order, error) {return []Order{{Price: 20300,Num:   2,},{Price: 642,Num:   5,},}, nil
}func TestGetAveragePricePerStore(t *testing.T) {type args struct {getter    OrderInfoGetterstoreName string}tests := []struct {name    stringargs    argswant    int64wantErr bool}{{name: "mock test",args: args{getter:    Mock{},storeName: "mock",},want:    12062,wantErr: false,},}for _, tt := range tests {t.Run(tt.name, func(t *testing.T) {got, err := GetAveragePricePerStore(tt.args.getter, tt.args.storeName)if (err != nil) != tt.wantErr {t.Errorf("GetAveragePricePerStore() error = %v, wantErr %v", err, tt.wantErr)return}if got != tt.want {t.Errorf("GetAveragePricePerStore() got = %v, want %v", got, tt.want)}})}
}

依赖注入代替隐式依赖

在应用程序中使用全局变量的方式引入日志库或数据库连接实例等。

package mainimport ("github.com/sirupsen/logrus"
)var log = logrus.New()type App struct{}func (a *App) Start() {log.Info("app start ...")
}func (a *app) Start() {a.Logger.Info("app start ...")// ...
}func main() {app := &App{}app.Start()
}

上面的代码中 App 中通过引用全局变量的方式将依赖项硬编码到代码中,这种情况下我们在编写单元测试时如何 mock log 变量呢?

此外这样的代码还存在一个更严重的问题——它与具体的日志库程序强耦合。当我们后续因为某些原因需要更换另一个日志库时,我们该如何修改代码呢?

我们应该将依赖项解耦出来,并且将依赖注入到我们的 App 实例中,而不是在其内部隐式调用全局变量。

type App struct {Logger
}func (a *App) Start() {a.Logger.Info("app start ...")// ...
}// NewApp 构造函数,将依赖项注入
func NewApp(lg Logger) *App {return &App{Logger: lg, // 使用传入的依赖项完成初始化}
}

上面的代码就很容易 mock log实例,完成单元测试。

依赖注入就是指在创建组件(Go 中的 struct)的时候接收它的依赖项,而不是它的初始化代码中引用外部或自行创建依赖项。

// Config 配置项结构体
type Config struct {// ...
}// LoadConfFromFile 从配置文件中加载配置
func LoadConfFromFile(filename string) *Config {return &Config{}
}// Server server 程序
type Server struct {Config *Config
}// NewServer Server 构造函数
func NewServer() *Server {return &Server{// 隐式创建依赖项Config: LoadConfFromFile("./config.toml"),}
}

上面的代码片段中就通过在构造函数中隐式创建依赖项,这样的代码强耦合、不易扩展,也不容易编写单元测试。我们完全可以通过使用依赖注入的方式,将构造函数中的依赖作为参数传递给构造函数。

// NewServer Server 构造函数
func NewServer(conf *Config) *Server {return &Server{// 隐式创建依赖项Config: conf,}
}

不要隐式引用外部依赖(全局变量、隐式输入等),而是通过依赖注入的方式引入依赖。经过这样的修改之后,构造函数NewServer 的依赖项就很清晰,同时也方便我们编写 mock 测试代码。

使用依赖注入的方式能够让我们的代码看起来更清晰,但是过多的构造函数也会让主函数的代码迅速膨胀,好在Go 语言提供了一些依赖注入工具(例如 wire ,可以帮助我们更好的管理依赖注入的代码。

SOLID原则

最后我们补充一个程序设计的SOLID原则,我们在程序设计时践行以下几个原则会帮助我们写出可测试的代码。

首字母指代概念
S单一职责原则每个类都应该只有一个职责
O开闭原则一个软件实体,如类、模块和函数应该对扩展开放,对修改关闭
L里式替换原则认为“程序中的对象应该是可以在不改变程序正确性的前提下被它的子类所替换的”的概念
I-接口隔离原则许多特定于客户端的接口优于一个通用接口
D依赖反转原则应该依赖抽象,而不是某个具体示例

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/45061.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

python http文件上传

server端代码 import os import cgi from http.server import SimpleHTTPRequestHandler, HTTPServer# 服务器地址和端口 host = 0.0.0.0 port = 8080# 处理文件上传的请求 class FileUploadHandler(SimpleHTTPRequestHandler):def do_POST(self):# 解析多部分表单数据form = …

【Spring系列篇--关于IOC的详解】

目录 面试经典题目&#xff1a; 1. 什么是spring&#xff1f;你对Spring的理解&#xff1f;简单来说&#xff0c;Spring是一个轻量级的控制反转(IoC)和面向切面(AOP)的容器框架。 2.什么是IoC&#xff1f;你对IoC的理解&#xff1f;IoC的重要性?将实例化对象的权利从程序员…

Centos 8 网卡connect: Network is unreachable错误解决办法

现象1、ifconfig没有ens160配置 [testlocalhost ~]$ ifconfig lo: flags73<UP,LOOPBACK,RUNNING> mtu 65536 inet 127.0.0.1 netmask 255.0.0.0 inet6 ::1 prefixlen 128 scopeid 0x10<host> loop txqueuelen 1000 (Local Loopba…

基于深度学习的指针式仪表倾斜校正方法——论文解读

中文论文题目:基于深度学习的指针式仪表倾斜校正方法 英文论文题目&#xff1a;Tilt Correction Method of Pointer Meter Based on Deep Learning 周登科、杨颖、朱杰、王库.基于深度学习的指针式仪表倾斜校正方法[J].计算机辅助设计与图形学学报, 2020, 32(12):9.DOI:10.3724…

CentOS Linux 78安全基线检查

阿里云标准-CentOS Linux 7/8安全基线检查 检查项类别描述加固建议等级密码复杂度检查身份鉴别检查密码长度和密码是否使用多种字符类型编辑/etc/security/pwquality.conf&#xff0c;把minlen(密码最小长度)设置为8-32位&#xff0c;把minclass(至少包含小写字母、大写字母、数…

代码随想录训练营day25| 216.组合总和III 17.电话号码的字母组合

TOC 前言 代码随想录算法训练营day25 一、Leetcode 216.组合总和III 1.题目 找出所有相加之和为 n 的 k 个数的组合&#xff0c;且满足下列条件&#xff1a; 只使用数字1到9 每个数字 最多使用一次 返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次&#xff0c;组…

【Java】智慧工地SaaS平台源码:AI/云计算/物联网/智慧监管

智慧工地是指运用信息化手段&#xff0c;围绕施工过程管理&#xff0c;建立互联协同、智能生产、科学管理的施工项目信息化生态圈&#xff0c;并将此数据在虚拟现实环境下与物联网采集到的工程信息进行数据挖掘分析&#xff0c;提供过程趋势预测及专家预案&#xff0c;实现工程…

《强化学习:原理与Python实战》——可曾听闻RLHF

前言&#xff1a; RLHF&#xff08;Reinforcement Learning with Human Feedback&#xff0c;人类反馈强化学习&#xff09;是一种基于强化学习的算法&#xff0c;通过结合人类专家的知识和经验来优化智能体的学习效果。它不仅考虑智能体的行为奖励&#xff0c;还融合了人类专家…

kafka安装说明以及在项目中使用

一、window 安装 1.1、下载安装包 下载kafka 地址&#xff0c;其中官方版内置zk&#xff0c; kafka_2.12-3.4.0.tgz其中这个名称的意思是 kafka3.4.0 版本 &#xff0c;所用语言 scala 版本为 2.12 1.2、安装配置 1、解压刚刚下载的配置文件&#xff0c;解压后如下&#x…

【机器学习】处理不平衡的数据集

一、介绍 假设您在一家给定的公司工作&#xff0c;并要求您创建一个模型&#xff0c;该模型根据您可以使用的各种测量来预测产品是否有缺陷。您决定使用自己喜欢的分类器&#xff0c;根据数据对其进行训练&#xff0c;瞧&#xff1a;您将获得96.2%的准确率&#xff01; …

PyTorch翻译官网教程-PROFILING YOUR PYTORCH MODULE

官网链接 Profiling your PyTorch Module — PyTorch Tutorials 2.0.1cu117 documentation 分析pytorch模块 PyTorch包含一个分析器API&#xff0c;用于识别代码中各种PyTorch操作的时间和内存成本。分析器可以很容易地集成到代码中&#xff0c;结果可以作为表格打印或以JSON…

Integer中缓存池讲解

文章目录 一、简介二、实现原理三、修改缓存范围 一、简介 Integer缓存池是一种优化技术&#xff0c;用于提高整数对象的重用和性能。在Java中&#xff0c;对于整数值在 -128 到 127 之间的整数对象&#xff0c;会被放入缓存池中&#xff0c;以便重复使用。这是因为在这个范围…

爬虫百度返回“百度安全验证”终极解决方案

这篇文章也可以在我的博客查看 爬不了啊&#xff01;&#xff01; 最近一哥们跟我说百度爬虫爬不了 弹出&#xff1a;“百度安全验证”&#xff0c;“网络不给力&#xff0c;请稍后重试” 说到爬虫&#xff0c;这里指的是Python中最常用的requests库 我说怎么爬不了了&#x…

Python绘制爱心代码(七夕限定版)

写在前面&#xff1a; 又到了一年一度的七夕节啦&#xff01;你还在发愁送女朋友什么礼物&#xff0c;不知道怎样表达你满满的爱意吗&#xff1f;别担心&#xff0c;我来帮你&#xff01;今天&#xff0c;我将教你使用Python绘制一个跳动的爱心&#xff0c;用创意和幽默为这个…

Angular安全专辑之二——‘unsafe-eval’不是以下内容安全策略中允许的脚本源

一&#xff1a;错误出现 这个错误的意思是&#xff0c;拒绝将字符串评估为 JavaScript&#xff0c;因为‘unsafe-eval’不是以下内容安全策略中允许的脚本源。 二&#xff1a;错误场景 testEval() {const data eval("var sum2 new Function(a, b, return a b); sum2(em…

JavaWeb_LeadNews_Day6-Kafka

JavaWeb_LeadNews_Day6-Kafka Kafka概述安装配置kafka入门kafka高可用方案kafka详解生产者同步异步发送消息生产者参数配置消费者同步异步提交偏移量 SpringBoot集成kafka 自媒体文章上下架实现思路具体实现 来源Gitee Kafka 概述 对比 选择 介绍 producer: 发布消息的对象称…

寻路算法小游戏

寻路算法小demo 寻路算法有两种&#xff0c;一种是dfs 深度优先算法&#xff0c;一种是 dfs 深度优先算法 深度优先搜索的步骤分为 1.递归下去 2.回溯上来。顾名思义&#xff0c;深度优先&#xff0c;则是以深度为准则&#xff0c;先一条路走到底&#xff0c;直到达到目标。这…

矩形重叠问题

矩形重叠 文章目录 题目描述解题思路方法一方法二 题目描述 矩形以列表 [x1, y1, x2, y2] 的形式表示&#xff0c;其中 (x1, y1) 为左下角的坐标&#xff0c;(x2, y2) 是右上角的坐标。矩形的上下边平行于 x 轴&#xff0c;左右边平行于 y 轴。 如果相交的面积为 正 &#xff0…

Hadoop小结(下)

HDFS 集群 HDFS 集群是建立在 Hadoop 集群之上的&#xff0c;由于 HDFS 是 Hadoop 最主要的守护进程&#xff0c;所以 HDFS 集群的配置过程是 Hadoop 集群配置过程的代表。 使用 Docker 可以更加方便地、高效地构建出一个集群环境。 每台计算机中的配置 Hadoop 如何配置集群…

2023-08-19力扣每日一题-水题/位运算解法

链接&#xff1a; 2235. 两整数相加 题意&#xff1a; ab 解&#xff1a; ab 补一个位运算写法&#xff0c;进位是(a&b)<<1&#xff0c;不进位的计算结果为a^b 实际代码&#xff1a; #include<iostream> using namespace std; int sum(int num1, int n…