想象一下:你刚刚离开 Node 前往 Go 的应许之地。您已经了解了组合、接口、简单性、领域驱动设计(但一无所知)和单元测试。你觉得自己都长大了,准备好接受并发,然后骑车进入应许之地。你已经构建了一个甜蜜的 CRUD 应用程序,它将彻底改变我们做 X 的方式。因为这是一个肯定的成功;您当然已经联系了房地产经纪人,以查看您一直想要的顶层公寓,并在多个风险投资基金和投资者处发了推文。您基本上已经准备好上线,但在您将代码发布到野外之前,您需要拿出您最喜欢的终端并输入 go run main.go。它运行,光辉岁月!

您设置了多个演示和投资者会议。您将“CEO 和创始人”添加到您的 Linkedin 个人资料(当然还有 tinder)中,并告诉您的妈妈您现在随时要离开地下室。你出现在你的第一次会议上,做几个有力的姿势,然后开始演示那个真正关键的端点,即应用程序的主干。用右手拇指按 ENTER 键(左手拇指在精神上准备好弹出香槟),您继续。 Aaaaaand,它完全崩溃了。什么鬼。你不仅在你的超级重要会议上被嘲笑,而且你还必须告诉房地产经纪人你可能买不起顶层公寓,你还必须留在你妈妈的地下室(并继续试图说服你的火种日期,这绝对是一个临时解决方案)。


希望您没有经历过上述情况,只是因为您在 google 中输入了一些单词而来到这里。我将向您展示如何使用 docker 和一些漂亮的 Makefile cmd 在 Go 中设置和运行集成测试。

文章的目的

关于这个主题已经写了很多(查看文章末尾的资源列表)。这并不是试图改进当前可用的内容。它试图展示如何在本地和外部(CI env)设置和配置集成测试,这是可扩展和可移植的。我确信有一些方法可以改进我将在这里向您展示的内容,但是,主要目标是为您提供构建出色测试套件的工具。一旦您了解了这些概念和配置,根据您的需要定制它们就容易多了。此外,您可以更好地了解要搜索的内容(未知的未知数是一个棘手的主题),因此您可以解决以后可能出现的自己的问题。

要求

好吧,第一件事。你需要一些工具,所以请安装和设置

下列的:

  • 码头工人

给猫剥皮的方法不止一种

与软件中的所有内容一样,有很多方法可以做同样的事情,甚至有更多关于如何做这件事的意见。通过集成测试,我们可能会讨论如何设置/配置测试套件,使用什么库(如果有的话),甚至什么时候分类为集成与单元测试、E2E 测试等等。这会使概念变得比它们需要的更复杂,尤其是当您刚刚开始时。我认为处理这种情况的最佳方法之一是就某种指导原则/首要原则达成一致,这将成为我们工作的基础。我发现最好的原则之一是:

测试行为,而不是实现细节

为了对此进行扩展,我们希望我们的测试涵盖用户可能如何实际使用我们的软件的案例/场景。我在这里非常广泛地使用术语用户和软件,因为它是非常特定于上下文的。用户可能是在我们创建的代码库中调用某些方法的同事,也可能是使用我们 API 的人。我们的测试不应该真正担心我们实际上是如何实现代码的,只要给定 X 输入我们会看到 Y 响应。从某种意义上说,我们正在测试我们作为开发人员通过向世界公开我们的代码/功能/方法而制定的“合同”实际上是否被保留。

如果我们想要一个集成测试的更具体的定义,我们可以从谷歌的大书上掸掉,看看他们是如何定义它的:

...中等范围的测试(通常称为集成测试)旨在验证少数组件之间的交互;例如,在服务器和数据库之间

资料来源:Google 的软件工程,第 1 章。 11

如果我们从clean architecture借用一些术语,我们可以将其视为当我们在测试中包含来自基础设施层的代码时,我们处于集成测试领域。

我们在做什么

当我开始用 Go 编写集成测试时;我最挣扎的是如何配置和设置它以供实际使用。同样,这也将根据开发人员/用例/现实而有所不同,因为这种方法可能不适用于大型跨国公司。我的集成测试标准是它们应该在不同的操作系统上运行,易于运行并且与 CI/CD 工作流很好地配合(基本上,在 dockerized 环境中运行良好)。

我是 YAGNI 的坚定信徒(你不会需要它),所以我将向你展示两种设置集成测试的方法:

  • 香草仅使用标准库方法

  • 使用库

这应该有希望说明您可以如何开始相对简单(我们可以跳过 Docker 部分,但老实说,这会使设置我们的 CI/CD 流程变得有点棘手),然后根据需要添加。

我们正在测试什么

我将重复使用我关于如何构建 Go 应用程序的文章中的一些代码(可以在这里找到。如果你还没有阅读它,它基本上构建了一个小应用程序,让你跟踪你的锁定期间体重增加。这篇文章需要更新(我建议查看此 [存储库(https://github.com/bnkamalesh/goapp),以获取有关如何使用 DDD 构建 Go 应用程序的一个很好的示例)和干净的架构),所以我们只专注于添加基于代码的集成测试(测试行为与实现细节的一个很好的例子)。我们希望确保对我们服务的调用行为符合预期。

你可以在这里找到 repo。

“基础设施”设置

许多现代 Web 开发都使用 Docker,本教程也不例外。这不是关于 Docker 的教程,因此我不会详细介绍设置,但会提供一些关于如何开始的基础。有一些方法可以通过扩展 docker-compose 文件、使用 Dockerfile 中的标签等来改进此设置。但我经常发现这让我头痛多于收获。在某种程度上,我们违反了 DRY,但它确实使我们能够拥有完全独立的开发和测试环境。阅读本文后,您可以自己尝试缩短 docker 设置。

Docker测试环境搭建

FROM golang:1-alpine

RUN apk add --no-cache git gcc musl-dev

RUN go get -u github.com/rakyll/gotest

RUN go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest

WORKDIR /app

进入全屏模式 退出全屏模式

version: "3.8"
services:
    database:
        image: postgres:13
        container_name: test_weight_tracker_db_psql
        environment:
            - POSTGRES_PASSWORD=password
            - POSTGRES_USER=admin
            - POSTGRES_DB=test_weight_tracker_database
        ports:
            - "5436:5432" # we are mapping the port 5436 on the local machine
              # to the image running inside the container
        volumes:
            - test-pgdata:/var/lib/postgresql/data

    app:
        container_name: test_weight_tracker_app
        build:
            context: .
            dockerfile: Dockerfile.test
        ports:
            - "8080:8080"
        working_dir: /app
        volumes:
            - ./:/app
            - test-go-modules:/go
        environment:
            - DB_NAME=test_weight_tracker_database
            - DB_PASSWORD=${DB_PASSWORD}
            - DB_HOST=test_weight_tracker_db_psql
            - DB_PORT=${DB_PORT}
            - DB_USERNAME=${DB_USERNAME}
            - ENVIRONMENT=test
        depends_on:
            - database

volumes:
    test-pgdata:
    test-go-modules:

进入全屏模式 退出全屏模式

有了这些,我们现在可以轻松地运行我们的集成测试,这可以通过以下方式完成:

make run-integration-tests

进入全屏模式 退出全屏模式

方法 1:香草设置

旁注:如果您想查看与其他代码分开的代码,请查看分支vanilla-approach/running-integration-tests-using-std-library

Go 社区倾向于更多地倾向于标准库而不是引入外部库。并且有充分的理由,您可以仅使用标准库做很多事情。在这方面,我绝不是纯粹主义者,我们也在这里使用外部库进行路由、迁移等,但我认为从标准库开始,然后使用其他库,可以很好地理解正在发生的事情当你去时。

在前面的部分中,我们启动并运行了我们的基础架构,因此我们有一个活动的数据库,一个正在运行的应用程序,或者在这种情况下,可以触发针对我们的应用程序的测试运行。

为了最大限度地提高我们对代码的信心,我们希望我们的集成测试尽可能地模仿我们的生产环境。这意味着我们需要一些设置和拆卸功能来运行迁移,使用种子数据填充数据库并在测试后拆卸所有内容,以便我们每次都有一个干净的环境。最后一部分很重要,因为我们想要一个可靠的测试环境,所以我们不希望以前运行的测试影响当前运行的测试。

在这里值得注意的是,这也意味着我们的集成测试必须按顺序运行,而不是并行运行,因为我们很可能会让我们的单元测试执行,这意味着运行测试套件需要更长的时间。我不会

一开始就太担心这一点,而只是专注于进行一些覆盖范围不错的集成测试。一旦您开始达到无法忍受的长时间集成测试运行,那么就该开始研究其他设置/改进当前设置了。例如,我们可以为每个测试创建一个新数据库并使用它,但它会添加更复杂的设置。或者我们可以使用 SQLite 数据库进行集成测试,但同样,一旦你到达它,就可以跨越那座桥。

尝试更改此代码以尝试不同的策略来加速测试运行将是一个很好的练习。

迁移

我是 golang-migrate 库的忠实粉丝,所以我们将使用它来编写迁移。简而言之,它会生成 up/down 迁移对,并将每个迁移视为一个新版本,因此您可以回滚到最后一个工作

如果需要,版本。

我不会在这里讨论迁移策略,所以我们的测试将假设我们有一个包含所有最新迁移的数据库。为了在我们的测试中实现这一点,我们将在每次测试运行之前运行升级版本。为此,我们需要以下功能:

func RunUpMigrations(cfg config.Config) error {
    _, b, _, _ := runtime.Caller(0)
    basePath := filepath.Join(filepath.Dir(b), "../migrations")
    migrationDir := filepath.Join("file://" + basePath)
    db, err := sql.Open("postgres", cfg.GetDatabaseConnString())
    if err != nil {
        return errors.WithStack(err)
    }
    defer db.Close()
    driver, err := postgres.WithInstance(db, &postgres.Config{})
    if err != nil {
        return errors.WithStack(err)
    }
    defer driver.Close()

    m, err := migrate.NewWithDatabaseInstance(migrationDir, "postgres", driver)
    if err != nil {
        return errors.WithStack(err)
    }

    if err := m.Up(); err != nil {
        if errors.Is(err, ErrNoNewMigrations) {
            return errors.WithStack(err)
        }
    }
    m.Close()
    return nil
}

进入全屏模式 退出全屏模式

测试结束后,我们希望环境干净,所以我们还需要这个函数:

func RunDownMigrations(cfg config.Config) error {
    _, b, _, _ := runtime.Caller(0)
    basePath := filepath.Join(filepath.Dir(b), "../migrations")
    migrationDir := filepath.Join("file://" + basePath)
    db, err := sql.Open("postgres", cfg.GetDatabaseConnString())
    if err != nil {
        return errors.WithStack(err)
    }
    defer db.Close()
    driver, err := postgres.WithInstance(db, &postgres.Config{})
    if err != nil {
        return errors.WithStack(err)
    }
    defer driver.Close()

    m, err := migrate.NewWithDatabaseInstance(migrationDir, "postgres", driver)
    if err != nil {
        return errors.WithStack(err)
    }

    if err := m.Down(); err != nil {
        return errors.WithStack(err)
    }

    return nil
}

进入全屏模式 退出全屏模式

基本上,我们得到一个到数据库的新连接,创建一个新的迁移实例,我们将路径传递到迁移文件夹、我们使用的数据库和驱动程序。然后我们运行迁移并再次关闭连接,非常简单。

接下来,我们需要数据库中的一些数据来运行我们的测试。我非常喜欢将其保存在 SQL 文件中,然后让帮助函数针对数据库运行 SQL 脚本。为此,我们只需要一个类似于上述两个的函数:

func LoadFixtures(cfg config.Config) error {
    pathToFile := "/app/fixtures.sql"
    q, err := os.ReadFile(pathToFile)
    if err != nil {
        return errors.WithStack(err)
    }

    db, err := sql.Open("postgres", cfg.GetDatabaseConnString())
    if err != nil {
        return errors.WithStack(err)
    }

    _, err = db.Exec(string(q))
    if err != nil {
        return errors.WithStack(err)
    }
    err = db.Close()
    if err != nil {
        return errors.WithStack(err)
    }

    return nil
}

进入全屏模式 退出全屏模式

有了这个设置,我们就可以编写我们的第一个测试了。

我们的第一次测试

从技术上讲,我们可以通过在 database/SQL 包中提供方法的模拟版本来测试与数据库交互的代码。但这并没有真正给我们太多,因为模拟一种情况会很棘手,例如,您错过了 .Scan 方法中的变量或有一些语法问题。因此,我倾向于为我的所有数据库功能编写集成测试。让我们为 CreateUser 函数添加一个测试。我们需要以下内容:


// testing the happy path only - to improve upon these tests, we could consider
// using a table test
func TestIntegration_CreateUser(t *testing.T) {
    // create a NewStorage instance and run migrations
    cfg := config.NewConfig()
    storage := psql.NewStorage()

    err := psql.RunUpMigrations(*cfg)
    if err != nil {
        t.Errorf("test setup failed for: CreateUser, with err: %v", err)
        return
    }

    // run the test
    t.Run("should create a new user", func(t *testing.T) {
        newUser, err := entity.NewUser(
            "Jon Snow", "male", "90", "thewhitewolf@stark.com", 16, 182, 1)
        if err != nil {
            t.Errorf("failed to run CreateUser with error: %v", err)
            return
        }

        // to ensure consistency we could consider adding in a static date
        // i.e. time.Date(insert-fixed-date-here)
        // creationTime := time.Now()
        err = storage.CreateUser(*newUser)
        // assert there is no err
        if err != nil {
            t.Errorf("failed to create new user with err: %v", err)
            return
        }

        // now lets verify that the user is actually created using a
        // separate connection to the DB and pure sql
        db, err := sql.Open("postgres", cfg.GetDatabaseConnString())
        if err != nil {
            t.Errorf("failed to connect to database with err: %v", err)
            return
        }
        queryResult := entity.User{}
        err = db.QueryRow("SELECT id, name, email FROM users WHERE email=$1",
            "thewhitewolf@stark.com").Scan(
            &queryResult.ID, &queryResult.Name, &queryResult.Email,
        )
        if err != nil {
            t.Errorf("this was query err: %v", err)
            return
        }

        if queryResult.Name != newUser.Name {
            t.Error(`failed 'should create a new user' wanted name did not match 
                returned value`)
            return
        }
        if queryResult.Email != newUser.Email {
            t.Error(`failed 'should create a new user' wanted email did not match 
                returned value`)
            return
        }
        if int64(queryResult.ID) != int64(1) {
            t.Error(`failed 'should create a new user' wanted id did not match 
                returned value`)
            return
        }

    })

    // // run some clean up, i.e. clean the database so we have a clean env
    // // when we run the next test
    t.Cleanup(func() {
        err := psql.RunDownMigrations(*cfg)
        if err != nil {
            if errors.Is(err, migrate.ErrNoChange) {
                return
            }
            t.Errorf("test cleanup failed for: CreateUser, with err: %v", err)
        }
    })
}

进入全屏模式 退出全屏模式

我们首先创建一个新的配置和存储实例(就像我们在运行整个应用程序时在 main.go 中所做的那样),然后运行向上迁移函数。如果没有任何问题,我们应该有一些类似于我们在生产中的东西。

然后,我们使用我们刚刚设置的存储实例来创建一个新用户,打开一个新连接来查询我们刚刚创建的用户,并验证该用户是使用我们期望的值创建的。之后,使用 testing 包提供的 Cleanup 函数调用向下迁移。这基本上清除了数据库。

您可能会注意到的另一件事是我们有一个 psql_test.go 文件。打开它,你会发现如下功能:


// TestMain gets run before running any other _test.go files in each package
// here, we use it to make sure we start from a clean slate
func TestMain(m *testing.M) {
    cfg := config.NewConfig()
    // make sure we start from a clean slate
    err := psql.DropEverythingInDatabase(*cfg)
    if err != nil {
        panic(err)
    }

    os.Exit(m.Run())
}

进入全屏模式 退出全屏模式

TestMain 是一个特殊函数,它在它所在的包中的所有其他测试之前被调用。在这里,我们(我会说是有道理的)偏执并调用一个删除数据库中所有内容的函数,所以我们确定我们正在开始从一张白纸开始。如果您想仔细查看,可以在 repository/psql.go 中找到该函数。

这基本上就是针对我们的数据库功能运行集成测试。我们可以在这里使用表驱动测试并且可能应该使用,但这只是为了说明目的。如果您不了解表驱动测试,请参阅此处了解表驱动测试的说明。接下来,让我们进行“适当的”集成测试并确保我们的端点按预期工作!

测试我们的端点

现在我们进入这些东西的实质。我们已经达到(或至少更接近)在 ol' google book 中找到的定义。我们正在测试我们的代码的多个部分是否协同工作,主要是在基础设施层,协同工作

以我们期望的方式。在这里,我们希望确保每当我们的 API 收到履行我们作为开发人员提出的合同的请求时,它都会执行我们想要的操作。也就是说,我们要测试快乐路径。理想情况下,我们还想测试悲伤的路径(不确定这是否是它的词,但这是我的文章,所以现在是)但是集成测试更“昂贵”,所以它是一个微妙的平衡。您可以选择模拟数据库响应并以更单元测试的方式测试悲伤的路径,或者您可以添加集成测试,直到运行测试套件所需的时间变得难以忍受。我可能会错误地向许多人添加 1 个集成测试,并在它变得太大时处理“成本”。

好了,啰嗦得够多了。让我们开始吧。这里有一个旁注;我正在使用受 Node Web 框架 express 启发的 gofiber。我设置 POST 请求的方式取决于 gofiber 的处理方式。我之所以这么说,是因为从 Go 发送 post 请求时的基础是使用 Marshaling。当我们谈到它时,我会指出它,但请注意,如果你喜欢大猩猩或杜松子酒,你可能需要在谷歌上搜索一下。

路由器设置的快速概述

我们不会在这方面花费太多时间,因为您可以在 repo 中找到代码。基本上,我们有这个:

type serverConfig interface {
    GetServerReadTimeOut() time.Duration
    GetServerWriteTimeOut() time.Duration
    GetServerPort() int64
}

type Http struct {
    router        *fiber.App
    serverPort    int64
    userHandler   userHandler
    weightHandler weightHandler
}

func NewHttp(
    cfg serverConfig, userHandler userHandler, weightHandler weightHandler) *Http {
    r := fiber.New(fiber.Config{
        ReadTimeout:  cfg.GetServerReadTimeOut(),
        WriteTimeout: cfg.GetServerWriteTimeOut(),
        AppName:      "Weight Tracking App",
    })
    return &Http{
        router:        r,
        serverPort:    cfg.GetServerPort(),
        userHandler:   userHandler,
        weightHandler: weightHandler,
    }
}

进入全屏模式 退出全屏模式

我们设置了一个 HTTP 结构,该结构具有一些依赖项,以使我们的服务器启动并与路由器一起运行。在该结构上,我们定义了一些特定于服务器的方法。这很简单。

测试我们的端点以创建一个新用户

我们的端点非常简单。没有中间件和身份验证,每个人都可以通过请求向我们的服务器发送垃圾邮件并创建大量用户。这并不理想,但也不是我们现在真正关心的。我们只是想确保我们的 API 做它应该做的事情。


func TestIntegration_UserHandler_New(t *testing.T) {
    cfg := config.NewConfig()
    storage := psql.NewStorage()

    err := psql.RunUpMigrations(*cfg)
    if err != nil {
        t.Errorf("test setup failed for: CreateUser, with err: %v", err)
        return
    }

    userService := service.NewUser(storage)
    weightService := service.NewWeight(storage)

    userHandler := http.NewUserHandler(userService)
    weightHandler := http.NewWeightHandler(weightService)

    srv := http.NewHttp(cfg, *userHandler, *weightHandler)

    srv.SetupRoutes()
    r := srv.GetRouter()

    req := http.NewUserRequest{
        Name:          "Test user",
        Sex:           "male",
        WeightGoal:    "80",
        Email:         "test@gmail.com",
        Age:           99,
        Height:        185,
        ActivityLevel: 1,
    }
    var buf bytes.Buffer
    err = json.NewEncoder(&buf).Encode(req)
    if err != nil {
        log.Fatal(err)
    }
    rq, err := h.NewRequest(h.MethodPost, "/api/user", &buf)
    if err != nil {
        t.Error(err)
    }
    rq.Header.Add("Content-Type", "application/json")

    res, err := r.Test(rq, -1)
    if err != nil {
        t.Error(err)
    }

    if res.StatusCode != 200 {
        t.Error(errors.New("create user endpoint did not return 200"))
    }

    // query the database to verify that a user was created based on the request
    // we sent
    newUser, err := storage.GetUserFromEmail(req.Email)
    if err != nil {
        t.Error(err)
    }

    if newUser.Height != req.Height {
        t.Error(errors.New("create user endpoint did not create user with correct details"))
    }

    t.Cleanup(func() {
        err := psql.RunDownMigrations(*cfg)
        if err != nil {
            if errors.Is(err, migrate.ErrNoChange) {
                return
            }
            t.Errorf("test cleanup failed for: CreateUser endpoint, with err: %v", err)
        }
    })
}

进入全屏模式 退出全屏模式

其中大部分看起来与我们在存储库测试中的相似。我们设置了数据库、服务,最后设置了服务器。我们创建一个请求,对其进行编码,将其发送到我们的端点并检查响应。这里需要注意的重要一点是,我们并不真正知道这头野兽背后发生了什么。我们只知道我们发送了一个包含一些数据的请求,然后返回 OK 并在数据库中创建一个包含预期数据的用户。这也称为黑盒测试。我们不关心这是如何完成的,我们关心的是预期的行为是否发生。

关于上述代码的一件事是,我们如何设置测试并在每次运行后将它们拆除有相当多的重复性。如果我们不必复制粘贴所有这些内容并在每次测试运行后洗一个长时间的热水澡,那就太好了,因为我们违反了 DRY。我们当然可以自己做,或者我们可以使用_Approach 2 - using test suites with Testify_。

方法 2 - 使用 Testify 运行我们的集成测试

为此,我们将使用我已经使用了一段时间的 testify 包。这对我们做的主要事情是节省一些配置行并确保我们的测试套件的一致性。当它只有这个大小时,将整个代码库放在你的脑海中是很容易的,但是随着它的增长,在一个地方完成设置和配置会使事情变得容易得多。让我们看看如何为我们的处理程序集成测试完成设置:

type HttpTestSuite struct {
    suite.Suite
    TestStorage *psql.Storage
    TestDb      *sql.DB
    TestRouter  *fiber.App
    Cfg         *config.Config
}

func (s *HttpTestSuite) SetupSuite() {
    log.SetFlags(log.LstdFlags | log.Lshortfile)
    cfg := config.NewConfig()

    db, err := sql.Open("postgres", cfg.GetDatabaseConnString())
    if err != nil {
        panic(errors.WithStack(err))
    }

    err = db.Ping()
    if err != nil {
        panic(errors.WithStack(err))
    }
    storage := psql.NewStorage()

    userService := service.NewUser(storage)
    weightService := service.NewWeight(storage)

    userHandler := http.NewUserHandler(userService)
    weightHandler := http.NewWeightHandler(weightService)

    srv := http.NewHttp(cfg, *userHandler, *weightHandler)

    srv.SetupRoutes()
    r := srv.GetRouter()

    s.Cfg = cfg
    s.TestDb = db
    s.TestStorage = storage
    s.TestRouter = r
}

进入全屏模式 退出全屏模式

我们基本上采取了整个设置步骤并为每个测试套件自动化。如果我们查看 SetupSuite 方法的文档,我们会发现它基本上是在运行套件中的测试之前运行的方法。因此,我们使用标准库进行的整个设置如下所示:


func TestIntegration_UserHandler_CreateUser(t *testing.T) {
    cfg := config.NewConfig()
    storage := psql.NewStorage()

    ..... irrelevant code removed

    userService := service.NewUser(storage)
    weightService := service.NewWeight(storage)

    userHandler := http.NewUserHandler(userService)
    weightHandler := http.NewWeightHandler(weightService)

    srv := http.NewHttp(cfg, *userHandler, *weightHandler)

    srv.SetupRoutes()
    r := srv.GetRouter()

    ..... irrelevant code removed

}

进入全屏模式 退出全屏模式

对我们来说是自动化的,很好!现在,我们也确实有其他一些要求,即每次测试运行我们都有一个“新鲜”的环境。这意味着我们需要运行向上/向下迁移以确保我们的数据库是干净的。这是在每个测试之前的设置和拆卸部分完成的,但是使用 testify,我们可以只定义 beforeTest 和 afterTest ,我们可以在其中运行与以前相同的方法,而不必为每个测试复制粘贴它们。

如果您查看 repo,您会注意到的一件事是,我们在存储库中的代码几乎与我们在这里所做的相同。仅结构中的 TestRouter 除外。我真的不介意这里的重复,因为我对端点测试的需求将来可能会发生变化,并且尽可能少地保持我的依赖关系是可取的。因此,如果您愿意,您可以制作一个大型集成测试套件。我只是更喜欢把事情分开,每个人都有自己的。

总结

以上步骤会不会让你避免我们在文章开头所经历的灾难?也许,这取决于(每个高级开发人员最喜欢的回复)。但是,它肯定会增加你可以拥有的信心

在你的代码中。有多少集成测试总是一个平衡,因为它们确实需要更长的时间来运行,但是有一些方法可以解决这个问题,所以在事情变得难以忍受的缓慢之前测试快乐的路径是一个很好的经验法则。当你到达它时,穿过那座桥。

正如引言中提到的,这并不是试图添加一些新东西并彻底改变我们在 Go 社区中进行集成测试的方式。但是给你另一个视角和一些样板代码来开始你的集成测试冒险。如果你想继续学习

人们比我聪明(你绝对应该),查看资源部分。

资源

Learn go with tests - 基本上读完了整篇文章。 Chris 出色地向您展示了如何开始使用 Go 和测试驱动开发。绝对值得一读。

HTML 表单、数据库、集成测试 - 虽然不是在 Go 中,但在 Rust 中,Luca 很好地解释了集成测试。始终尝试寻找超越编程语言的概念和不超越的概念。有一个细致入微的观点总是有益的。

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐