测试外部API调用有时可能是一个挑战。但是,重要的是要确保您围绕外部依赖性的逻辑如预期的。
在这篇文章中,我将使用我的prejoin attendance list demo在GO中介绍一种对Daily’s REST API进行测试的方法,这是一种流行的后端服务语言。
这篇文章假设读者已经有一些事先经验,因此我不会在这里介绍基础知识。但是,我将提供链接,以帮助您在需要的情况下完成任何相关概念。
演示的概述
如果您还没有阅读my tutorial on implementing a prejoin attendance list with Daily,请现在介绍该应用程序的基础知识。如果您已经熟悉演示,请随时跳过本节。
前加入出勤列表演示使用户可以创建一个新的每日视频通话室,然后加入其预电话大厅。当加入这个大厅时,右侧显示了已经在呼叫中的参与者列表。这使用户能够偷看谁已经到了电话。
要在本地运行演示,请参阅GitHub存储库中的the README。
出于本文的目的,我们主要关心server-side components,这些server-side components被写成GO中的Netlify无状态功能。我有一个creating a room的Netlify终点,另一个是retrieving video call presence。我已经为这些实施了一些basic tests。这些测试是我在这篇文章中关注的内容。
使用该概述,让我们进入测试方法。
我如何在GO中测试外部API调用
测试外部API调用时,我的目标是测试我对返回数据的处理, 不是以测试外部API本身。我想确保我自己的代码处理每日返回的数据。
这些天,我首选测试外部呼叫的方法是使用koude0软件包,这是GO标准库的一部分。还有其他方法,例如将外部请求逻辑与interface一起包装,然后嘲笑上述接口,但是我发现每个测试用例的小测试服务器都在大多数时候是一种更直观的方法。
这是我用于测试我的每日REST API在我的前加入列表演示中进行调用的方法。房间的创建和存在检索测试都遵循相同的方法。在这篇文章中,我将以存在的检索测试为例。
表驱动测试
我在测试时选择使用table-driven tests。表驱动的测试本质上是一个测试,该测试定义了测试本身中的许多输入和预期输出,并重复使用该数据来运行测试的核心逻辑。您通常会在测试功能中看到一个新结构。该结构决定了测试输入和输出的结构。然后,您将根据此结构有一个变量指定测试用例。
我的存在测试看起来像这样:
func TestGetPresence(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
retCode int
retBody string
wantErr error
wantParticipants []Participant
}{
// Multiple test cases here
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// Shared test logic here
})
}
}
此测试将专门用于测试我的端点koude1功能,这是每日呼叫的呼叫api的地方。
上面的测试用例结构定义了一些关键信息:
-
name
是测试案例的名称。这应该是您可以在日志中轻松理解的东西。 -
retCode
是返回代码,我将从每天进行模拟。 -
retBody
是返回主体,每天都在模拟。 -
wantErr
是我期望我的逻辑的错误,可以在处理每日模拟返回数据后返回。 -
wantParticipants
是参与者的切片,我期望我的逻辑在处理每日返回数据后返回。
定义了测试表和测试用例后,我拥有每个测试用例将通过的共享逻辑。
- i在每个测试用例上迭代,并遮蔽
tc
变量。t.Run()
在单独的goroutine中运行其字面的函数。在GO中,函数文字保留了外部范围中变量的引用。这意味着作为tc
变量集作为for
循环声明的一部分,随着循环移动到其下一次迭代时,可以更改运行的测试用例。通过阴影tc
,我确保每次迭代的测试案例数据是我期望的。 - 然后,我致电koude8并将其传递为测试的核心逻辑,我们将通过下一步。
定义测试的核心逻辑
让我们从覆盖测试的共享组件开始:每个测试案例都会通过的内容。这是每个测试用例的测试服务器将被旋转并销毁的地方。
func TestGetPresence(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
retCode int
retBody string
wantErr error
wantParticipants []Participant
}{
// Multiple test cases here
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(tc.retCode)
_, err := w.Write([]byte(tc.retBody))
require.NoError(t, err)
}))
defer testServer.Close()
gotParticipants, gotErr := getPresence("name", "key", testServer.URL)
require.ErrorIs(t, gotErr, tc.wantErr)
if tc.wantErr == nil {
require.EqualValues(t, tc.wantParticipants, gotParticipants)
}
})
}
}
上面发生了三个主要事情:
- 我正在配置每日REST API响应的模拟。
- 我正在调用我的功能进行测试。
- 我要检查该功能的返回值是否与我期望的内容相匹配。
模拟每日的REST API响应
上面的第一步是主要的:使用koude13创建测试服务器。这里的构造函数采用koude14的实例,该实例是定义一个函数的接口:ServeHTTP(ResponseWriter, *Request).
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(tc.retCode)
_, err := w.Write([]byte(tc.retBody))
require.NoError(t, err)
}))
defer testServer.Close()
在我的测试HTTP处理程序内部,我写了要模拟的返回标头(在这种情况下,我的测试案例中定义的返回代码)和 body 我希望我的假日返回。
我然后验证使用koude17工具包用require.NoError()
编写身体没有问题。如果此时出现问题,这将确保测试失败。
创建testServer
后,我添加了一个koude19语句以关闭服务器。该语句将作为测试用例的最后一个调用。
总结了主要设置:配置我的测试服务器以返回我想要测试功能处理的每日数据。
调用getPresence()
函数
完成设置后,我可以测试我的实际功能。我这样做是通过调用getPresence()
并将其返回值分配给两个got
变量:
gotParticipants, gotErr := getPresence("name", "key", testServer.URL)
get
和want
变量前缀格式使任何阅读测试的人都清楚了哪些数据i got 我是我的测试的内容以及哪些数据i 希望从我的测试中获取,然后比较它们。
上面传递给getPresence
的前两个参数只是虚拟值:它们不会影响我的测试服务器响应的行为。最后一个,testServer.URL
非常重要。
When calling koude1 from my live handler code,最后一个参数是每日REST API的URL。但是在这种情况下,我们希望通过我的 fake 服务器重新汇总请求:我上面创建的测试服务器。这就是为什么我通过testServer.URL
传递。
这可能是您有时需要构建非测试代码的一个很好的例子。
编写可测试代码
完全不使用URL参数来编写getPresence()
非常容易,只需从函数内部检索我的DAILY_API_UR
l环境变量即可。它甚至可能很诱人:为什么当数据在那里时,为什么用另一个参数将功能签名混乱?但这本来可以使这里的测试方法在这里不可能,因为我无法将测试服务器注入过程中。
测试返回值
致电getPresence()
并消耗返回值后,最后要做的事情是确认返回的数据是我所期望的。 I like using testify’s koude32 package for this:
require.ErrorIs(t, gotErr, tc.wantErr)
if tc.wantErr == nil {
require.EqualValues(t, tc.wantParticipants, gotParticipants)
}
首先,我检查我的错误是否是我的期望。如果此require.ErrorIs()
检查失败,则测试将立即失败,并且不进行下一个检查。
如果成功,我检查我想要的错误是否真的是nil
:即根本没有错误。在这种情况下,我使用require.EqualValues()
比较了我说我想与实际获得的参与者的价值观的值进行比较。如果这些不匹配,则测试也将失败。
这是我们测试逻辑的主要大部分!剩下的只是快速查看一些测试案例数据的示例,我将进食此测试。
定义测试案例数据
在为外部API编写测试用例时,我的第一站始终是文档。在这种情况下,这将是每天的koude36 endpoint docs。我特别对API响应的示例感兴趣,您可以在该页面的底部看到。
该示例通常定义我的第一个测试用例,我将弹出到上面定义的test cases slice:
func TestGetPresence(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
retCode int
retBody string
wantErr error
wantParticipants []Participant
}{
{
name: "one-participant",
retCode: 200,
// This response is copied directly from the presence endpoint docs example:
// https://docs.daily.co/reference/rest-api/rooms/get-room-presence#example-request
retBody: `
{
"total_count": 1,
"data": [
{
"room": "w2pp2cf4kltgFACPKXmX",
"id": "d61cd7b2-a273-42b4-89bd-be763fd562c1",
"userId": "pbZ+ismP7dk=",
"userName": "Moishe",
"joinTime": "2023-01-01T20:53:19.000Z",
"duration": 2312
}
]
}
`,
wantParticipants: []Participant{
{
ID: "d61cd7b2-a273-42b4-89bd-be763fd562c1",
Name: "Moishe",
},
},
},
}
上面的retCode
和retBod
y值直接从每日文档中获取。
然后,我根据我的逻辑运行后,根据我期望将数据变成的wantParticipants
值。
在这种情况下,我希望getPresence()
(正在测试的功能)返回一个参与者切片,其中包括一个元素:具有"d61cd7b2-a273-42b4-89bd-be763fd562c1"
ID的参与者和"Moishe"
的名称。
我通常要做的接下来是测试失败响应。我的测试案例是每日返回的内部服务器错误,如下所示:
{
name: "failure",
retCode: 500,
wantErr: util.ErrFailedDailyAPICall,
},
在这里,我没有供日常测试服务器返回的车身,只返回500
状态代码。在这种情况下,我希望getPresence()
返回参与者的 no 切片(即nil
值)和预定的koude46错误。
这些只是两个基本示例。然后,您可以用更多的测试用例充实测试,例如:
- 测试更多来自Daily返回的参与者
- 测试您的功能不知道如何解析的意外数据
- 测试意外的响应时间
凭借测试的核心逻辑保持不变,测试功能中的更多变化和代码路径就成为简单地将另一个测试用例引入您的表的问题。
结论
在这篇文章中,我介绍了一种测试每日REST API呼叫的方法。如果您对测试我们视频API的任何其他部分有任何疑问,无论是在GO还是其他语言中,请不要犹豫不决。我很想知道每天都会听到其他开发人员进行测试的方法。如果您想分享,请前往我们的Webrtc社区peerConnection。