在我的道路学习过程中,我遇到了一些令人惊叹的图书馆和Utilites,我最喜欢的集成测试之一是dockertest。
每当我使用Postgres,Mongo,MySQL或其他不属于代码库一部分的服务的服务时,我通常会创建一个docker-compose文件,以便隔离我的开发环境。然后,当我在一个特定项目中工作时,我要做的就是docker-compose up -d
开始,而docker-compose down
当我完成一天后。
例如,如果我只需要一个Postgres数据库,我通常会有类似
的东西
services:
postgres:
image: postgres:15
ports:
- 5432:5432
volumes:
- pg-db:/var/lib/postgresql/data
environment:
POSTGRES_HOST_AUTH_METHOD: trust
POSTGRES_DB: example
volumes:
pg-data:
这非常适合开发,但是在运行集成测试时,如果我想要一个干净的数据库,则意味着我需要一些其他步骤。我可以:
- 手动删除并创建数据库
- 删除并创建数据库作为集成测试的一部分
- 许多其他选项
而不是创建一些自动化以删除和创建数据库或表,而是让我们创建一个完整的清洁隔离实例,因此我们知道每次都不毫无疑问。这是从Devops中的cattle not pets理想学进行的。
我们只是在这里讨论Postgres,但是其他一些Docker Services可能需要很多手动步骤才能进入测试运行状况良好。
例子
我创建了一个示例项目here来显示所有行动,但是我将努力完成我采取的每个步骤。
我创建的项目非常简单,主要工作是在以下代码中:
package database
import (
"fmt"
"log"
"os"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
type (
Person struct {
gorm.Model
Name string
Age uint
}
)
var db *gorm.DB
func Connect() {
log.Println("Setting up the database")
pgUrl := fmt.Sprintf("postgresql://postgres@127.0.0.1:%s/example", os.Getenv("POSTGRES_PORT"))
log.Printf("Connecting to %s\n", pgUrl)
var err error
db, err = gorm.Open(postgres.Open(pgUrl), &gorm.Config{})
if err != nil {
panic("failed to connect database")
}
// Migrate the schema
db.AutoMigrate(&Person{})
}
func CreatePerson() {
log.Println("Creating a new person in the database")
person := Person{Name: "Danny", Age: 42}
db.Create(&person)
log.Println("Trying to write a new person to the database")
}
func CountPeople() int {
var count int64
db.Model(&Person{}).Count(&count)
return int(count)
}
因此,我们有一种连接的方法,一种创建新人记录的方法以及一种计算记录的方法。这些都是从主入口点调用的
func main() {
database.Connect()
database.CreatePerson()
count := database.CountPeople()
log.Printf("Database has %d people", count)
}
如果我从命令行(一旦docker组成了),则结果会增加每次运行的计数(如它应该)
go-dockertest-example λ git main → go run main.go
2023/09/03 13:41:06 Setting up the database
2023/09/03 13:41:06 Connecting to postgresql://postgres@127.0.0.1:5432/example
2023/09/03 13:41:06 Creating a new person in the database
2023/09/03 13:41:06 Trying to write a new person to the database
2023/09/03 13:41:06 Database has 3 people
go-dockertest-example λ git main → go run main.go
2023/09/03 13:41:07 Setting up the database
2023/09/03 13:41:07 Connecting to postgresql://postgres@127.0.0.1:5432/example
2023/09/03 13:41:08 Creating a new person in the database
2023/09/03 13:41:08 Trying to write a new person to the database
2023/09/03 13:41:08 Database has 4 people
考试
现在让我们看看测试。首先是Dockertest的设置:
func TestMain(m *testing.M) {
// Start a new docker pool
pool, err := dockertest.NewPool("")
if err != nil {
log.Fatalf("Could not construct pool: %s", err)
}
// Uses pool to try to connect to Docker
err = pool.Client.Ping()
if err != nil {
log.Fatalf("Could not connect to Docker: %s", err)
}
pg, err := pool.RunWithOptions(&dockertest.RunOptions{
Repository: "postgres",
Tag: "15",
Env: []string{
"POSTGRES_DB=example",
"POSTGRES_HOST_AUTH_METHOD=trust",
"listen_addresses = '*'",
},
}, func(config *docker.HostConfig) {
// set AutoRemove to true so that stopped container goes away by itself
config.AutoRemove = true
config.RestartPolicy = docker.RestartPolicy{
Name: "no",
}
})
if err != nil {
log.Fatalf("Could not start resource: %s", err)
}
pg.Expire(10)
// Set this so our app can use it
postgresPort := pg.GetPort("5432/tcp")
os.Setenv("POSTGRES_PORT", postgresPort)
// Wait for the Postgres to be ready
if err := pool.Retry(func() error {
_, connErr := gorm.Open(postgres.Open(fmt.Sprintf("postgresql://postgres@localhost:%s/example", postgresPort)), &gorm.Config{})
if connErr != nil {
return connErr
}
return nil
}); err != nil {
panic("Could not connect to postgres: " + err.Error())
}
code := m.Run()
os.Exit(code)
}
因此,首先我们创建一个新的Docker池,确保池对Ping响应,然后启动Postgres实例。当您启动新实例时,它将抓住一个随机端口,因此您需要一种将其传递给服务的方法,这就是为什么我们拥有POSTGRES_PORT
env var的原因。在模块设置的末尾,我们设置了ENV,以便测试将使用它。
// Set this so our app can use it
postgresPort := pg.GetPort("5432/tcp")
os.Setenv("POSTGRES_PORT", postgresPort)
有一个以等待Postgres准备就绪的部分,您可以以不同的方式使用它,但是基本上您正在使用某种条件来测试实例已准备就绪。这可能是从检查端口是可寻址的,到致电HTTP HealthCheck。
// Wait for the Postgres to be ready
if err := pool.Retry(func() error {
_, connErr := gorm.Open(postgres.Open(fmt.Sprintf("postgresql://postgres@localhost:%s/example", postgresPort)), &gorm.Config{})
if connErr != nil {
return connErr
}
return nil
}); err != nil {
panic("Could not connect to postgres: " + err.Error())
}
然后有:
// Make sure we expire the instance after 10 seconds
postgres.Expire(10)
这确保如果清理未成功,我们将在10秒后删除Docker实例。
最后,我们有实际的测试
func TestCreatePerson(t *testing.T) {
// Connect to the database
database.Connect()
// Create a person in the database
database.CreatePerson()
// Check that the person was created
count := database.CountPeople()
if count != 1 {
t.Errorf("Expected 1 person to be in the database, got %d", count)
}
}
每次测试运行时,数据库中只有一个记录。