type
status
date
slug
summary
tags
category
icon
password
Sub-item
Last edited time
Mar 24, 2024 03:12 AM
Parent item
领域
您是否听说过一个项目,其中对您不喜欢的客户或无法盈利的国家/地区进行了测试?或者更糟糕的是——你从事过这样的项目吗?
仅仅说它不公平、不专业还不够。开发任何新东西也很困难,因为你害怕对代码库进行任何更改。
为了轻松、自信地开发应用程序,您需要进行一组多个级别的测试。在本章中,我将通过实际示例介绍如何实现高质量的数据库集成测试。我还将介绍基本的 Go 测试技术,例如测试表、断言函数、并行执行和黑盒测试。
测试质量高实际上意味着什么?
高质量测试的 4 个原则
我准备了 4 条规则,我们需要通过这些规则来表明我们的集成测试质量很高。
- 快速
好的测试需要快速。这里没有妥协。
每个人都讨厌长时间运行的测试。让我们想想你的队友在等待测试结果时的时间和心理健康状况。无论是在 CI 还是本地。太可怕了。
当你等待很长时间时,你可能会同时开始做其他事情。 CI 通过后(希望如此),您将需要切换回此任务。上下文切换是最大的生产力杀手之一。这对我们的大脑来说非常疲惫。我们不是机器人。
我知道还有一些公司可以24小时进行测试。我们不想遵循这种方法。您应该能够在 1 分钟内(最好是 10 秒以内)在本地运行测试。我知道有时可能需要一些时间投入。这是一项具有出色投资回报率(ROI)的投资!在这种情况下,您可以真正快速地检查您的更改。此外,部署时间等也大大缩短。
根据我的经验,尝试找到可以最大程度地减少测试执行的快速胜利总是值得的。帕累托原则(80/20 规则)在这里完美发挥作用!
- 在各个级别上测试足够的场景
- 测试需要稳健且具有确定性
- 您应该能够在本地执行大部分测试
- TDD - 让我们从测试开始,检查事务是否正常工作。
- 先从实现开始,稍后再添加测试。
- 当它不能正常工作时,它会在测试中产生难以调试的问题,
- 它会使测试变慢,
- 它会增加开发的开销(您需要记住更新清理功能)
- 它可能会使并行运行测试变得更加困难。
- 分页 – 如果您迭代页面,其他测试可以在页面之间放置一些内容并移动页面中的“项目”。
- 全局计数器——与分页一样,其他测试可能会以意想不到的方式影响计数器。
我希望您已经知道 100% 的测试覆盖率并不是最好的想法(只要它不是一个简单/关键的库)。
问自己“它有多容易坏?”这个问题总是一个好主意。如果您觉得您正在实现的测试开始看起来与您测试的函数完全相同,那么更值得问这个问题。最后我们不编写测试,因为测试很好,但它们应该拯救我们!
根据我的经验,在 Go 中,70-80% 的覆盖率是一个相当不错的结果。
用组件或端到端测试覆盖所有内容也不是最好的主意。首先,您将无法执行此操作,因为无法模拟某些错误场景,例如存储库上的回滚。其次——它将打破第一条规则,这些测试会很慢。
领域 - 单元测试
repo,ports - 集成测试
app,adpters - 组件测试
多个层次上的测试也应该重叠,这样我们就知道集成是否正确完成。您可能认为解决方案很简单:测试金字塔!有时确实如此……。
但是,例如,对于聚合来自多个其他服务的数据并通过 API 公开数据的应用程序呢?它没有复杂的保存数据的逻辑。可能大部分代码都与数据库操作相关。在这种情况下,我们应该使用反向测试金字塔(它实际上看起来更像一棵圣诞树)。当我们的应用程序的大部分连接到某些基础设施(例如:数据库)时,很难通过单元测试来覆盖很多功能。
您知道当您进行一些紧急修复时,测试在本地通过,您将更改推送到存储库,然后……20 分钟后它们在 CI 中失败的感觉吗?这太令人沮丧了。它还阻止我们添加新的测试。这也降低了我们对他们的信任。您应该尽快解决该问题。在这种情况下,破窗理论确实有效。
您在本地运行的测试应该让您有足够的信心,确信您开发或重构的功能仍然有效。 E2E 测试应该只是仔细检查所有内容是否正确集成。当服务之间的契约由于使用 gRPC、protobuf 或 OpenAPI 而变得稳健时,您也会更有信心。这是我们在较低级别(从最低级别开始)尽可能多地进行覆盖的一个很好的理由:单元、集成和组件测试。
实施
让我们看一些可以在项目中实施的实际示例,从我在上一章中描述的存储库模式开始。我们与数据库交互的方式是由
hour.Repository
接口定义的。它假设我们的存储库实现是愚蠢的。所有复杂的逻辑都由我们应用程序的领域部分处理。它应该只保存数据而不进行任何验证等。该方法的显着优点之一是简化存储库和测试实现。在上一章中,我准备了三种不同的数据库实现:MySQL、Firebase 和内存。现在对他们进行测试,所有这些都是完全兼容的,因此我们可以只有一个测试套件。我们编写的所有测试都是黑盒测试。换句话说——我们只会通过测试来覆盖公共功能。为了确保这一点,我们所有的测试包都有 _test 后缀。这迫使我们只能使用包的公共接口。这些测试不受任何内部变化的影响。如果您无法编写良好的黑盒测试,则应该考虑您的公共 API 是否设计良好。
我们所有的存储库测试都是并行执行的。因此,它们花费的时间不到 200 毫秒。添加多个测试用例后,这个时间不应显着增加。
当我们进行多个测试时,我们传递相同的输入并检查相同的输出,最好使用称为测试表的技术。这个想法很简单:您应该定义测试的输入和预期输出的一部分,并对其进行迭代以执行测试。
您可以看到我们使用了非常流行的 github.com/stretchr/testify库。通过为断言提供多个帮助器,它显着减少了测试中的样板。
require.NoError() :当
assert.NoError
断言失败时,测试执行不会中断。
require
包中的断言会在测试失败时停止执行测试,因此通常使用 require
来检查错误,因为在许多情况下,如果某些操作失败,则稍后检查任何内容都是没有意义的。
当我们断言多个值时,assert
是更好的选择,因为您将收到更多上下文。如果我们有更具体的数据需要断言,添加一些辅助函数总是一个好主意。它消除了很多重复,并大大提高了测试的可读性!
测试事务
错误告诉我,在实现复杂代码时不应该相信自己。有时我们可能无法理解文档或者只是引入一些愚蠢的错误。您可以通过两种方式获得信心:
如果不使用 TDD ,我会纠结测试实施是否有效。为了更加自信,我使用了一种我称之为测试破坏的技术。该方法非常简单 - 让我们破坏正在测试的实现,看看是否有任何失败。
如果你的测试在这样的改变之后通过了,这是个坏消息...…
测试数据库竞争条件
我们的应用程序并不是凭空运行的。总是有这样的情况:两个多个客户端可能尝试执行相同的操作,并且只有一个可以获胜!
在我们的案例中,典型的场景是两个客户尝试同时安排培训。我们一小时内只能安排一次训练。此约束是通过乐观锁(在存储库模式中描述)和领域约束(在域驱动设计精简版中描述)来实现的。
让我们验证一下是否可以多次安排一小时。这个想法很简单:让我们创建 20 个 goroutine,我们将立即释放它们并尝试安排训练。我们期望只有一名工人能够成功。
这也是一个很好的例子,一些用例在集成测试中更容易测试,而不是在验收或 E2E 级别。像 E2E 这样的测试将非常重,将需要更多的工作人员来确保他们同时执行事务。
加快测试速度
如果您的测试无法并行执行,那么即使在最好的机器上测试速度也会很慢。
放置
t.Parallel()
就足够了吗?好吧,我们需要确保我们的测试是独立的。在我们的例子中,如果两个测试尝试编辑同一个Hour,它们可能会随机失败。这是一种非常不希望出现的情况。为了实现这一目标,我创建了
newValidHourTime()
函数,该函数提供当前测试运行中唯一的随机Hour。在大多数应用程序中,为您的实体生成唯一的 UUID 可能就足够了。使我们的测试独立的另一个好处是不需要数据清理。根据我的经验,进行数据清理总是很混乱,因为:
我们也可能无法并行运行测试。两个常见的示例是:
在这种情况下,值得让这些测试尽可能短。
请不要在测试中使用睡眠!
最后一个导致测试不稳定且缓慢的技巧是在其中添加睡眠功能。拜托,不要!最好对测试使用
channel
或sync.WaitGroup{}
同步。这样它们会更快、更稳定。如果你确实需要等待某些事情,最好使用
assert.Eventually
而不是sleep
。Eventually
断言在 waitFor
时间内将满足给定条件condition
,并在每个时钟周期tick
定期检查目标函数。运行
现在,当我们的测试完成后,就可以运行它们了!在此之前,我们需要使用
docker-compose up
启动包含 Firebase 和 MySQL 的容器。我准备了 make test
命令,以一致的方式运行测试(例如,-race 标志)。它也可以在 CI 中使用。运行一个测试并传递自定义参数
如果您想传递一些额外的参数,以获得详细输出(
-v
)或执行精确测试(-run
),您可以在 make test --
之后传递它。如果您对它的实现方式感兴趣,我建议您查看我的 Makefile magic 。
调试
有时我们的测试会以一种不明确的方式失败。在这种情况下,能够轻松检查数据库中的数据非常有用。
对于 SQL 数据库,我的首选是 MySQL的
mycli
和 PostgreSQL的 pgcli
。我已将 make mycli
命令添加到 Makefile,因此您无需始终传递凭据。对于 Firestore,模拟器通过 localhost:4000/firestore 访问 UI。
拥有经过充分测试的应用程序的第一步
我们目前最大的差距是缺乏组件和E2E级别的测试。此外,应用程序的很大一部分根本没有经过测试。我们将在接下来的章节中解决这个问题。我们还将讨论这次我们跳过的一些主题。
但在此之前,我们需要先讨论一个主题——整洁/六边形架构!这种方法将帮助我们稍微组织我们的应用程序,并使未来的重构和功能更容易实现。需要提醒的是,Wild Workouts 的完整源代码可以在 GitHub上找到。您可以在本地运行它并通过一个命令部署到 Google Cloud。