当前位置: 首页 > news >正文

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依赖反转原则应该依赖抽象,而不是某个具体示例

相关文章:

go-test

单元测试 基本用法 Go语言测试 常用reflect.DeepEqual()对slice进行比较 跳过某些测试用例 func TestTimeConsuming(t *testing.T) {if testing.Short() {t.Skip("short模式下会跳过该测试用例")}... }当执行go test -short时就不会执行上面的TestTimeConsuming测…...

假设你新换了电脑,如何不用U盘的情况下实现软件文件转移?

要将笔记本和台式机连接到同一个局域网&#xff0c;并实现文件共享或使用文件传输协议进行文件传输&#xff0c;您可以按照以下步骤操作&#xff1a; 设置局域网连接共享文件夹使用文件传输协议 Step 1: 设置局域网连接 确保笔记本和台式机连接到同一个局域网。有几种常见的…...

聊聊 Docker

聊聊 Docker Docker 是什么&#xff1f; 定义 Docker 是一款 开源的应用容器引擎。 简单来说&#xff0c;就是 以容器虚拟化技术为基础的软件。可以把应用程序和所依赖的包一起打包到一个可移植的镜像中&#xff0c;发布到 Linux 或者 Windows 上运行。&#xff08;代码 运…...

运行软件mfc140u.dll丢失怎么办?mfc140u.dll的三个修复方法

最近我在使用一款软件时遇到了一个问题&#xff0c;提示缺少mfc140u.dll文件。。这个文件是我在使用某个应用程序时所需要的&#xff0c;但是由于某种原因&#xff0c;它变得无法正常使用了。经过一番搜索和了解&#xff0c;我了解到mfc140u.dll是Microsoft Visual Studio 2015…...

神经网络基础-神经网络补充概念-54-softmax回归

概念 Softmax回归&#xff08;Softmax Regression&#xff09;是一种用于多分类任务的机器学习算法&#xff0c;特别是在神经网络中常用于输出层来进行分类。它是Logistic回归在多分类问题上的推广。 原理 Softmax回归的主要思想是将原始的线性分数&#xff08;得分&#xf…...

米尔瑞萨RZ/G2L开发板-02 ffmpeg的使用和RTMP直播

最近不知道是不是熬夜太多&#xff0c;然后记忆力减退了&#xff1f; 因为板子回来以后我就迫不及待的试了一下板子&#xff0c;然后发现板子有SSH&#xff0c;但是并没有ffmpeg&#xff0c;最近总是在玩&#xff0c;然后今天说是把板子还原一下哇&#xff0c;然后把官方的固件…...

基于swing的在线考试系统java jsp线上试卷问答mysql源代码

本项目为前几天收费帮学妹做的一个项目&#xff0c;Java EE JSP项目&#xff0c;在工作环境中基本使用不到&#xff0c;但是很多学校把这个当作编程入门的项目来做&#xff0c;故分享出本项目供初学者参考。 一、项目描述 基于swing的在线考试系统 系统有2权限&#xff1a;管…...

C# 读取pcd点云文件数据

pcd文件有ascii 和二进制格式&#xff0c;ascii可以直接记事本打开&#xff0c;C#可以一行行读。但二进制格式的打开是乱码&#xff0c;如果尝试程序中读取&#xff0c;对比下看了数据也对不上。 这里可以使用pcl里的函数来读取pcd&#xff0c;无论二进制或ascii都可以正确读取…...

.NET CORE Api 上传excel解析并生成错误excel下载

写在前面的话&#xff1a; 【对外承接app API开发、网站建设、系统开发&#xff0c;有偿提供帮助&#xff0c;联系方式于文章最下方 】 因业务调整&#xff0c;不再需要生成错误无excel下载&#xff0c;所以先保存代码&#xff0c;回头再重新编辑 #region Excel校验部分if (f…...

数据结构,二叉树,前中后序遍历

二叉树的种类 最优二叉树 最优二叉树画法 排序取最小两个值和&#xff0c;得到新值加入排序重复1&#xff0c;2 前序、中序和后序遍历是树形数据结构&#xff08;如二叉树&#xff09;中常用的遍历方式&#xff0c;用于按照特定顺序遍历树的节点。这些遍历方式在不同应用中有不…...

项目实战笔记2:硬技能(上)

序&#xff1a; 本节串讲了项目管理硬技能&#xff0c;有些术语可以结合书或者网上资料来理解。没有想书上讲的那样一一列举。 做计划 首先强调为什么做计划&#xff1f; 计划就是各个角色协同工作的基准&#xff08;后面做风险监控、进度的监控&#xff09;&#xff0c;贯穿于…...

神经网络基础-神经网络补充概念-59-padding

概念 在深度学习中&#xff0c;“padding”&#xff08;填充&#xff09;通常是指在卷积神经网络&#xff08;Convolutional Neural Networks&#xff0c;CNNs&#xff09;等神经网络层中&#xff0c;在输入数据的周围添加额外的元素&#xff08;通常是零&#xff09;&#xf…...

【开源免费】ChatGPT-Java版SDK重磅更新收获2.3k,支持插件模式、实现ChatGpt联网操作。

everybody 七夕来了还单着么&#xff1f; 一、简介 ChatGPT Java版SDK开源地址&#xff1a;https://github.com/Grt1228/chatgpt-java&#xff0c;目前收获将近2200个star&#x1f31f;。 最新版&#xff1a;1.1.1-beta0 <dependency><groupId>com.unfbx</g…...

情报与GPT技术大幅降低鱼叉攻击成本

邮件鱼叉攻击&#xff08;spear phishing attack&#xff09;是一种高度定制化的网络诈骗手段&#xff0c;攻击者通常假装是受害人所熟知的公司或组织发送电子邮件&#xff0c;以骗取受害人的个人信息或企业机密。 以往邮件鱼叉攻击需要花费较多的时间去采集情报、深入了解受…...

Swift 周报 第三十五期

文章目录 前言新闻和社区五天市值蒸发 2000 亿美元&#xff0c;苹果公司怎么了&#xff1f;在你的 App 中帮助顾客解决账单问题需要声明原因的 API 列表现已推出 提案通过的提案正在审查的提案 Swift论坛推荐博文话题讨论关于我们 前言 本期是 Swift 编辑组整理周报的第三十五…...

uni-app + SpringBoot +stomp 支持websocket 打包app

文章目录 一、概述&#xff1a;二、配置&#xff1a;1. 后端配置2. uni-app(app端)3. 使用 一、概述&#xff1a; websocket 协议是在http 协议的基础上的升级&#xff0c;通过一次http 请求建立长连接&#xff0c;转而变为TCP 的全双工通信&#xff1b;而http 协议是一问一答…...

LeetCode--HOT100题(35)

目录 题目描述&#xff1a;23. 合并 K 个升序链表&#xff08;困难&#xff09;题目接口解题思路1代码解题思路2代码 PS: 题目描述&#xff1a;23. 合并 K 个升序链表&#xff08;困难&#xff09; 给你一个链表数组&#xff0c;每个链表都已经按升序排列。 请你将所有链表合…...

idea插件grep console最佳实践

首发博客地址 https://blog.zysicyj.top/ 参考博客&#xff1a;https://blog.csdn.net/ayunnuo/article/details/123997304 效果 配置 具体颜色 日志级别前景色背景色Error#FF0000#370000Warn#FFC033#1A0037Info#00FFF3无Debug#808080无 本文由 mdnice 多平台发布...

Android 12 源码分析 —— 应用层 二(SystemUI大体组织和启动过程)

Android 12 源码分析 —— 应用层 二&#xff08;SystemUI大体组织和启动过程&#xff09; 在前一篇文章中&#xff0c;我们介绍了SystemUI怎么使用IDE进行编辑和调试。这是分析SystemUI的最基础&#xff0c;希望读者能尽量掌握。 本篇文章&#xff0c;将会介绍SystemUI的大概…...

【C#】通用类型转换

【C#】通用类型转换 自动类型转换&#xff08;隐式类型转换&#xff09;强制类型转换&#xff08;显式类型转换&#xff09;通过函数进行转换&#xff08;通过方法进行类型转换&#xff09;使用 as 操作符转换通用类型转换方法实现 数据类型转换就是将数据&#xff08;变量、数…...

传统DNS、负载均衡服务发现框架与专业服务发现框架(Eurek、nacos)分析

1、DNS 服务器 DNS 服务器可以在一定程度上用作服务发现的机制&#xff0c;以下是其冲动服务发现的一些利弊 优势 广泛性&#xff1a; DNS是互联网的标准协议之一&#xff0c;已经广泛地被支持和使用。因此&#xff0c;使用DNS作为服务发现的机制可以借助现有的网络基础设施…...

js中数组常用操作函数

js数组经常会用到&#xff0c;当涉及到 JavaScript 数组的函数&#xff0c;有许多常用的函数可用于对数组进行操作和转换。以下是一些常见的数组函数的讲解 splice() splice() 函数用于修改数组&#xff0c;可以删除、插入或替换数组中的元素。 var fruits [apple, banana,…...

Windows、Mac、Linux端口占用解决

Windows、Mac、Linux端口占用解决 简介 在使用计算机网络时&#xff0c;经常会遇到端口被占用的问题。当一个应用程序尝试使用已经被其他程序占用的端口时&#xff0c;会导致端口冲突&#xff0c;使应用程序无法正常运行。本文将介绍在Windows、Mac和Linux操作系统上解决端口…...

企业文件透明加密软件——「天锐绿盾」数据防泄密管理软件系统

PC访问地址&#xff1a; 首页 一、文档透明加密软件 文档透明加密功能&#xff1a;在不影响单位内部员工对电脑任何正常操作的前提下&#xff0c;文档在复制、新建、修改时被系统强制自动加密。文档只能在单位内部电脑上正常使用&#xff0c;在外部电脑上使用是乱码或无法打…...

Postman接口自动化测试实例

一.实例背景 在实际业务中&#xff0c;经常会出现让用户输入用户密码进行验证的场景。而为了安全&#xff0c;一般都会先请求后台服务器获取一个随机数做为盐值&#xff0c;然后将盐值和用户输入的密码通过前端的加密算法生成加密后串传给后台服务器&#xff0c;后台服务器接到…...

软件团队降本增效-构建人员评价体系

在软件团队中&#xff0c;最大成本往往来自于人力。这是因为软件开发是一项高度技术密集和智力密集的工作&#xff0c;需要研发人员具备较高的专业知识和技能。研发人员的工作状态和主动性对产出和质量具有极大的影响。如果研发人员缺乏积极性和投入度&#xff0c;可能会导致项…...

Python实现SSA智能麻雀搜索算法优化随机森林分类模型(RandomForestClassifier算法)项目实战

说明&#xff1a;这是一个机器学习实战项目&#xff08;附带数据代码文档视频讲解&#xff09;&#xff0c;如需数据代码文档视频讲解可以直接到文章最后获取。 1.项目背景 麻雀搜索算法(Sparrow Search Algorithm, SSA)是一种新型的群智能优化算法&#xff0c;在2020年提出&a…...

web JS高德地图标点、点聚合、自定义图标、自定义窗体信息、换肤等功能实现和高复用性组件封装教程

文章目录 前言一、点聚合是什么&#xff1f;二、开发前准备三、API示例1.引入高德地图2.创建地图实例3.添加标点4.删除标点5.删除所有标点&#xff08;覆盖物&#xff09;6.聚合点7.自定义聚合点样式8.清除聚合9.打开窗体信息 四、实战开发需求要求效果图如下&#xff1a;封装思…...

AlpacaFarm: A Simulation Framework for Methods that Learn from Human Feedback

本文是LLM系列文章&#xff0c;针对《》的翻译。 AlpacaFarm:从人类反馈中学习方法的模拟框架 摘要1 引言2 背景与问题描述3 构造AlpacaFarm4 验证AlpacaFarm模拟器5 AlpacaFarm的基准参考方法6 相关工作7 不足和未来方向 摘要 像ChatGPT这样的大型语言模型由于能够很好地遵循…...

【Linux】Linux工具篇(yum、vim、gcc/g++、gdb、Makefile、git)

&#x1f680; 作者简介&#xff1a;一名在后端领域学习&#xff0c;并渴望能够学有所成的追梦人。 &#x1f681; 个人主页&#xff1a;不 良 &#x1f525; 系列专栏&#xff1a;&#x1f6f9;Linux &#x1f6f8;C &#x1f4d5; 学习格言&#xff1a;博观而约取&#xff…...

南京一等一网站建设/发布信息的免费平台

一&#xff1a;linux上安装 nginx 下载nginx:wget http://nginx.org/download/nginx-1.6.2.tar.gz 解压&#xff1a;tar zxvf nginx-1.6.2.tar.gz 进入nginx目录&#xff1a; cd nginx 安装&#xff1a;./configure --prefix/usr/local/nginx 出现一个错误提示说没安装 pcre【…...

西安app制作设计公司/本溪seo优化

一、B/S 架构带来的好处 用户使用简单&#xff1a;由于浏览器具有统一性&#xff0c;它不需要特殊的配置和网络连接&#xff0c;有效的屏蔽了不同服务提供商提供给用户使用服务的差异性。浏览器的交互特性使得用户使用它非常简便&#xff0c;而且用户行为的可继承性非常强&…...

如何建设视频资源电影网站/地推拉新app推广接单平台

有时候遇到apk文件zip加密了&#xff0c;classes.dex有密码保护&#xff0c;无法解压出来&#xff08;应当说是三个文件被密码保护 AndroidManifest.xml classes.dex resources.arsc&#xff09; 怎么做到classes.dex有密码保护这个效果呢&#xff1f; 使用了伪加密,修改zip的…...

ai智能写作网站/搜索引擎营销的模式有哪些

摘要&#xff1a;WordPress feed功能可以进行RSS阅读器订阅&#xff0c;让读者实时接收博客的更新内容。但rss在国内及当前的访客中并不...WordPress feed功能可以进行RSS阅读器订阅&#xff0c;让读者实时接收博客的更新内容。但rss在国内及当前的访客中并不普及&#xff0c;而…...

灯饰外贸网站/郑州网站推广公司咨询

1、面试官问输入网址后,会经历哪几个步骤&#xff1f; DNS HTTPS(TCP).......就知道这两个 DNS解析TCP连接发送http请求 HTTP请求报文的方法是 get &#xff0c;如果浏览器存储了该域名下的 Cookies&#xff0c;那么会把 Cookies放入 HTTP请求头里发给服务器&#xff0c…...

企业网站模板 网页模板/seo优化多久能上排名

为什么80%的码农都做不了架构师&#xff1f;>>> ‍ Ravindra Pilli现在是MARLABS公司的一名移动测试架构师。8年多&#xff0c;他当过测试工程师&#xff0c;高级测试工程师&#xff0c;测试TL和移动测试架构师。他曾在埃森哲、三星、爱立信干过。Ravindra是测试卓…...