高效Go编程指南:2 测试入门

高效Go编程指南:2 测试入门

首页角色扮演里程碑式生成器更新时间:2024-07-30
本章涵盖

让我们回到过去。Google 的 Go 团队正忙于编写 Go 标准库。你,程序员,最近加入了团队。在谷歌,一个名为Wizards的团队中的其他开发人员正在开发重定向服务。

当他们的程序收到 Web 请求时,他们会检查 URL 以查看它是否有效,如果 URL 有效,他们希望更改它的某些部分并将 Web 请求重定向到新位置。但他们意识到 Go 标准库中没有 URL 解析器。

所以他们要求你将这样一个 URL 解析器包添加到 Go 标准库中。您知道基础知识,但还不知道如何在 Go 中编写和测试惯用代码。我将在本章中通过从头开始实现和单元测试一个名为 url 的新库包来帮助您编写惯用代码。

图2.1 解析网址

  1. 解析网址以检查它是否是有效的网址
  2. 将 URL 分成几个部分:方案、主机和路径
  3. 提供更改已解析 URL 各部分的功能

如图 2.1 所示,代码将 URL 字符串传递给 Parse 函数。然后,Parse 分析它,创建一个新的 URL 值,并返回指向该 URL 值的指针。它返回一个指针,以便您可以更改同一 URL 值的字段。

URL 结构类型包含 URL 部分,例如方案、主机和路径。成熟的解析器涉及更多内容,但让我们保持更易于管理,只解析方案、主机名和 URL 路径。

首先,您将了解 Go 如何进行测试、Go 中单元的定义以及单元测试。之后,您将学习如何编写第一个单元测试。创建基本测试后,您将学习如何与测试框架进行通信。您还将学习如何编写描述性失败消息。编写第一个测试后,您将开始编写并向 url 包添加更多测试。

您将看到当您没有编写正确的测试函数时会遇到什么样的错误。这样你就会看到你将要编写的每一行代码背后的原因。您不仅将学习如何测试,而且还将看到 Go 测试框架背后的一些代码并更深入地理解它。

好了,让我们开始吧!

2.1 Go 的测试方法

让我们开始讨论 Go 在开始编写 url 包之前如何进行测试。在 Go 中,几乎所有东西都是内置的。此方法也适用于测试。因此,您可以使用内置测试框架自动测试代码,而无需安装任何外部工具或库。与 Go 中的所有内容一样,测试表面上也很简单,但它隐藏了简单编程接口背后的复杂性。

尽管一切都是内置的,但除了测试运行器和一些测试包之外,本身并没有一个成熟的测试框架。但是,这并不意味着 Go 中的内置测试设施很弱。Go 标准库非常强大,提供了很多测试帮助器。

尽管如此,许多新的 Go 开发人员在开始用 Go 编程之前立即开始寻找额外的测试框架和包。我理解他们,因为我没有什么不同。来自 Node.js,我曾经有一些其他的第三方测试框架进行测试。现在,我主要使用内置的测试设施,并在关键时刻带上小帮手。很快,您会发现 Go 测试工具拥有几乎所有类型测试所需的一切。

对于 Go 中的测试,有两个主要参与者:

让我们看一下图 2.2 以了解测试框架的流程。

图 2.2 Go 测试框架机制

  1. 首先,测试工具找到测试函数,它们与测试包通信,例如报告它们是成功还是失败。
  2. 测试工具本身不是编译器,因此它使用 Go 编译器来编译测试函数及其依赖的包,并将它们打包在可执行的二进制文件中。
  3. 测试工具运行最终的测试二进制文件,测试包进行控制。
  4. 测试包在可执行二进制文件中运行测试函数,并将其结果显示到控制台。

在本章中,您将了解测试工具和测试包,我偶尔会将其称为 Go 测试框架。Go 标准库中还有其他帮助程序包和工具以及工具。我会在你读这本书的时候揭示它们。

2.1.1 go中的“单位”是什么?

许多新手经常感到困惑,尤其是在 Go 中定义“单位”一词时。因此,在本节中,我将解释 Go 语言中“单元”和“单元测试”的含义。但所有这些术语都是模糊的。人们在任何地方对它们的解释都大不相同。你可以问一群人什么是单元测试或单元测试,你会得到许多不同的答案。

在其他一些流行的编程语言中,类是主要参与者,开发人员针对类编写单元测试。但是围棋中没有类;相反,您可以围绕包组织代码,理想情况下,每个包都提供其他包独有的内容。

通常,包可能会变大,并且可能包含许多函数和类型。发生这种情况时,您可以将包代码分离到同一包中的多个文件,以便更直接地导航。例如,stdlib 的字符串包相对较大,将相关功能组合到不同的源代码和测试文件中,如表 2.1 所示。

表 2.1.字符串包

类型和函数

代码文件

测试文件

Fields(s string) []string

按空格拆分字符串值的函数。

HasPrefix(s, prefix string) bool

检查一个字符串是否以另一个字符串开头的函数。

字符串包中还有数十个其他函数。

strings/strings.go

此文件声明函数。

strings/strings_test.go

此文件测试已在 strings.go 文件中声明的所有函数。

Builder struct

可以有效地将字符串组合到缓冲区中的类型。

strings/builder.go

此文件声明生成器类型及其方法以及一些其他帮助程序函数。

strings/builder_test.go

此文件包含对 builder.go 中代码的测试。

Reader struct

可以从数据流读取的类型。

strings/reader.go

此文件声明读取器类型及其方法。

strings/reader_test.go

此文件包含 reader.go 中代码的测试。

Replacer struct

可以用替换一系列字符串的类型。

strings/replace.go

此文件声明了替换器类型以及替换器的一些其他帮助程序类型和函数。

strings/replace_test.go

此文件包含对 replace.go 中代码的测试。

正如您在表 2.1 中看到的,所有这些函数、类型和测试一起构成了一个称为字符串的包。将测试分离到多个文件中仍使这些测试成为字符串包的单元测试,因为它们只测试字符串包。

虽然字符串包在 Go 术语中是一个单元,但类型也是单元,或者可能是子单元。例如,字符串。生成器类型也是一个单元,并具有自己的单元测试。

如您所见,定义什么是单位并不总是那么容易。我相信定义取决于您的团队。对我来说,Go 中的单元是一个包,单元测试的描述是验证单个行为的测试

2.1.2 什么是单元测试?

例如,单个单元测试可以验证函数是否正常工作。假设有一个简单的函数,它将两个给定的数字相加并返回结果:

func sum(a, b int) int { return a b }

然后,您可以编写一个单元测试来验证 sum 函数是否正常工作:

func main() { if n := sum(2, 3); n != 5 { fmt.Printf("TEST FAILED") } }注意

此示例演示了一个单元测试,但不是 Go 中单元测试的惯用方法。本章将向您展示如何使用 Go 标准库的测试包编写惯用的单元测试。

单元测试是自动化测试的基本部分,如果制作正确,它们可以快速运行并帮助您设计可测试的代码。对我来说,适当的单元测试通常具有以下特征:

我认为最后两个特征非常简单,但是第一个特征:“孤立”呢?所以真正的问题是:代码的一小部分是什么?“小”是什么意思?单元测试的范围在很大程度上取决于开发团队。只要开发人员同意测试是他们自己的问题空间中的单元测试,该术语就无关紧要。如果你问我,单元测试通常是验证单个包的单个或多个功能的测试,由开发人员编写。

2.1.3 总结

在进入下一节之前,让我们总结一下到目前为止您学到的内容:

2.2 编写第一个单元测试

正如我在章节条目中所解释的,您将向 Go 标准库添加新的 URL 解析器包。您可以考虑先编写代码。但您可能从其他语言的经验中知道,测试可以帮助您编写正确的代码。测试还可以帮助您了解有关 Go 测试框架的更多信息,而您以前可能从未有机会使用过这些框架。

在本节中,您将编写第一个单元测试。在本节结束时,你将学习测试的基础知识、编写的惯用测试和用于解析 URL 的代码。url 包不会是 Go 标准库的 url 包的精确副本。不过,它还是会告诉你以一种有趣的方式编写惯用的单元测试。

2.2.1 创建网址包

您可能还记得本章介绍中,威世智团队希望使用您的 url 包解析 URL。为此,他们需要导入包。你可能已经知道,在 Go 中,每个目录对应于一个 Go 包。因此,您将开始在新目录中编写 url 包。

让我们通过在命令行中输入以下命令来制作并切换到一个名为 url 的新目录:

mkdir url cd url

您为包创建了一个目录,但尚未定义它。您将在下一节中执行此操作,您的软件包将愉快地存在于此目录中。

警告

关于 Go 模块

如果你已经从这本书的github存储库下载了代码,那么你已经在ch02/url目录中有了url包的代码。

如果你想和书一起编写代码并从头开始编写代码,你可能希望首先创建一个新目录并使用以下命令初始化一个新的 Go 模块:

mkdir project_name cd project_name go mod init github.com/your_username/project_name

在本节中创建 url 包后,可以像这样导入:

import github.com/your_username/project_name/url关于包的命名

请注意,您没有调用包url_parser或解析或解析。包名称应描述它提供的内容,而不是它的作用。这可能不仅仅是解析 URL。

url 包提供了使用 URL 的方法。将来,您可以向 url 包添加更多功能,并且无需更改包的名称。

然而,那里没有一个真相。你可以做任何最适合你自己情况的事情。

2.2.2 创建测试文件

Go 有一个特殊的命名约定,使源代码文件成为测试文件。如图 2.3 所示,每个测试文件都应具有 _test.go 后缀,以便测试工具可以将该文件视为测试文件并自动运行它。

图2.3 测试文件的命名

由于您要为 url 包编写测试,因此您可以创建一个名为 url_test.go 的新空测试文件。如果需要,可以通过键入以下命令在命令行创建文件:

$ touch url_test.go注意

Windows 没有触摸命令。相反,您可以在自己喜欢的编辑器中创建一个空文件。

让我们尝试使用 go 测试工具运行您的第一个测试文件。在 url 目录中,您可以简单地使用测试工具运行测试,如下所示:

$ go test expected 'package', found 'EOF'

您会看到此错误,因为测试文件只是另一个 Go 源代码文件,并且它还需要属于包。最好保持简单,尤其是在开始时。您可能还不知道要编写什么代码、要导出什么以及不从 url 包中导出什么代码。因此,您可以将代码和测试放在同一个包中,而不是创建新的测试包(清单 2.1)。

示例 2.1: 第一个测试文件 (url_test.go)

package url

您现在可以从测试中访问 url 包的所有功能,这称为内部测试。稍后您将了解外部内部测试。

运行测试工具时,您将看到另一个错误,如下所示:

$ go test testing: warning: no tests to run

此消息来自测试工具。它在包中找不到任何测试函数。您有一个测试文件,但没有测试函数,因此还没有要运行的内容。不用担心,接下来您将创建第一个测试函数。

排除检查

如果您想知道编译程序时测试会发生什么,请继续阅读。只有测试工具在编译的二进制文件中包含测试。使用 go build 命令生成代码时,编译器会忽略以 _test.go 后缀结尾的文件。因此,测试最终不会使二进制文件膨胀。

2.2.3 编写测试函数

在 Go 中,您可以使用简单的函数进行测试。如图 2.4 所示,您编写了一个以 Test 前缀开头并采用 *test 的函数。T 参数。

图2.4 测试函数命名

  1. 测试函数应以测试前缀开头。
  2. 它还应该进行*测试。T 参数。

假设您决定编写一个名为 parse 的函数,该函数可以解析给定的 URL。但您希望为威世智团队提供解析功能。为此,您需要通过将函数重命名为 解析 .这样,威世智团队就可以在导入包时调用该函数。所以你计划最后有一个 Parse 函数。

提示

即使你想测试一个未导出的(私有)函数,你仍然会编写TestParse而不是Testparse。如果您担心会有两个同名的测试,那么您可以将另一个测试设为 TestParseInternal 。

正如我之前所说,在编写 Parse 函数之前,您将首先编写一个测试。您可以在清单 2.2 中看到您的第一个测试函数。

示例 2.2: 第一个测试用例 (url_test.go)

package url func TestParse() { #A // Nothing is here yet. }

您将测试函数命名为 TestParse,并将其放入 url 包中。运行测试工具时,会收到一条错误消息:

$ go test wrong signature for TestParse, must be: func TestParse(t *testing.T)

测试工具告诉您测试函数的签名不正确。签名包括函数的名称、它采用和返回的参数。您可以通过添加 *test 来使测试函数正确。T 参数。您可以在清单 2.3 中看到更新的测试代码。

示例 2.3: 一个有效的测试用例 (url_test.go)

package url import "testing" #A func TestParse(t *testing.T) { #B // Nothing is here yet. }

您将测试函数命名为 TestParse,让它进行 *测试。T 参数,并将测试函数放入 url 包中。您还需要导入测试包才能使用 *testing。T 型。重新运行测试时,将获得以下输出:

PASS

祝贺!测试工具告诉您测试成功,您会感觉良好。但是你不应该有这种感觉,因为测试应该失败了,对吧?为什么会成功?这是因为测试包不知道您的测试应该失败。为此,您需要了解*测试。T 型。

注意

清单 2.3 中只有一个测试函数,但您可以在同一个测试文件或其他测试文件中编写任意数量的测试函数。测试工具将自动查找并运行它们。

2.2.4 信号测试

您终于有一个有效的测试用例。但它不是一个有用的测试用例,因为它还没有验证任何东西!在本节中:

如果你能写一个失败的测试,这将是重要的第一步。因此,让我们更深入地了解*测试。键入并了解它提供的方法。

假设测试包正在运行 TestParse 测试函数。测试函数如何向测试包发出失败或成功的信号?需要有一种与测试包通信的方法。为此,测试包提供了几种信令机制。其中之一是*测试。T 型。如图 2.5 所示,您可以通过与测试包通信来控制测试流程。

图2.5 测试包与测试功能的通信

测试包通过*测试。测试函数的 T 值,以便测试函数可以与测试包通信。

  • 如果测试函数调用 t.Log 方法,测试包将记录一条消息。测试包将消息记录在测试输出中测试的右侧下方。因此,您可以看到消息来自该测试。
  • 如果测试函数调用 t.Fail 方法,则测试包会将该测试函数标记为失败。稍后,它将在测试输出中将函数报告为失败。
  • 在引擎盖下,*测试。T 类型是 Go 标准库测试中的结构类型:

    // src/testing/testing.go package testing type T struct { ... }

    在运行测试函数之前,测试包会通过 *测试。T 值。*T 类型提供了几种信令方法,您可以在表 2.2 中看到其中的一些方法。在整个章节中,您还将了解其他信令类型。现在,您将只使用 日志 、 日志 和 失败 方法。

    编写失败的测试

    您希望解析给定的 URL,并在解析失败时收到错误。否则,使用该函数的威世智团队将永远不会知道解析是否失败。一开始,解析将始终失败,因为稍后您将实现解析逻辑。如果立即实现了正确的逻辑,则可能无法确定测试代码是否正常工作。因此,让我们调用 Parse 函数,从中获取错误,如果出现错误,则测试失败。

    在清单 2.4 中,测试函数调用 url 包的 Parse 函数。然后,如果它从 Parse 函数收到错误,它将使用 Fail 方法向测试包发送信号。因此,测试包将测试标记为失败。

    示例 2.4: 编写失败的测试 (url_test.go)

    func TestParse(t *testing.T) { if err := Parse("broken url"); err != nil { #A t.Fail() #B } }

    您可能确定这次编写了正确的测试函数。但是当您要执行测试时,将收到如下错误:

    $ go test undefined: Parse FAIL

    错误消息显然告诉您测试失败。接下来是时候实现 Parse 函数了。

    这里发生了什么?

    测试工具首先尝试使用 Go 编译器编译代码,但失败了,因为您尚未声明 Parse 函数。这就是为什么由于编译错误,测试工具从未到达测试包的原因。想想吧。测试工具使用测试包编译测试。那么,如果构建失败,测试包将如何工作?

    编写代码

    您可能已经注意到,您还没有编写 Parse 函数。这样,您就可以看到在没有它的情况下运行测试时会发生什么。因此,让我们开始它,首先在同一目录中创建一个新的空文件。您现在已经准备好实现清单 2.5 中的 Parse 函数了。

    示例 2.5: 编写 Parse 函数 (url.go)

    package url import "errors" // Parse parses rawurl into a URL structure. #A func Parse(rawurl string) error { #B return errors.New("malformed url") #C }提示

    您可能始终希望记录您的函数,主要是在从包中导出函数时。顺便说一下,请注意,由于测试和代码在同一个包中,因此无需导出函数来测试它。

    运行测试时,它将再次失败:

    $ go test --- FAIL: TestParse

    祝贺!现在你感觉很好,因为你终于写了一个失败的测试。虽然这听起来像个笑话,但这个测试是一个很好的里程碑,让我可以解释测试包的更多方面。

    将代码和测试放在同一个文件中

    您可以将解析器代码 ( url.go ) 放入同一个测试文件 ( url_test.go ),但最好将它们分开。否则,您可能会无意中将它们混合在一起,这很容易导致可维护性的噩梦。有时,尤其是在原型设计时,可以将代码和测试放在同一个文件中。

    编写描述性失败消息

    当您阅读前面的输出时,您看不到测试失败的原因。然后你心想:“我对这个错误消息一无所知!我很高兴地说,有一个 Log 方法可以打印出一条消息。因此,如果测试函数失败(清单 2.6),您可以从 Parse 函数打印错误。

    示例 2.6: 记录错误消息 (url_test.go)

    func TestParse(t *testing.T) { if err := Parse("broken url"); err != nil { t.Log(err) #A t.Fail() #B } }

    现在,您有一个失败并显示错误消息的测试函数。当您运行它时,这一次,它将失败,并显示您从 Parse 函数收到的错误消息:

    $ go test --- FAIL: TestParse malformed url

    虽然当前的输出看起来比前一个更好,但它仍然不够有用(我知道;我是一个挑剔的人。如果你有很多测试功能,你就不会知道为什么会出现这个错误。因此,让我们在清单 2.7 中使用另一个称为 Logf 的方法制作一个更好的错误消息。

    示例 2.7: 使用 Logf 方法 (url_test.go) 的更好错误消息

    func TestParse(t *testing.T) { const rawurl = "broken url" if err := Parse(rawurl); err != nil { t.Logf("Parse(%q) err = %q, want nil", rawurl, err) #A t.Fail() } }

    正如清单 2.7 中看到的,Logf 方法的作用类似于 fmt。打印函数。首先将格式说明符作为字符串传递给它,然后传递任何类型的许多可变参数。在清单 2.7 中,格式说明符包括:

    运行测试时,会收到更具描述性的错误消息:

    $ go test --- FAIL: TestParse Parse("broken url") err = "malformed url", want nil

    现在,该消息会告诉您传递给 Parse 函数的 URL、从中收到的错误以及您想要的内容。使用此错误消息,您可以轻松查看测试失败的原因。有帮助,嗯?不?

    通过测试

    到目前为止,您已经检查了如果 Parse 函数失败,测试函数是否捕获错误。如果解析函数成功解析 URL,则还应检查测试是否成功。因此,您可以确定测试有效。您可以在清单 2.8 中看到代码。

    示例 2.8: 将 Parse 函数更改为成功 (url.go)

    func Parse(rawurl string) error { return nil }2.2.5 总结

    您创建了一个名为 url 的新包,并成功编写了失败并通过测试!干得好!您了解了可以使用 Fail 方法使测试失败。以及如何使用 Log 和 Logf 方法编写描述性失败消息。让我们总结一下到目前为止您学到的东西:

    2.3 编写网址解析器

    现在您已经了解了测试的基础知识,是时候开始编写解析器代码了!威世智团队要求您提供一个包来解析 URL,如果您还记得章节介绍的话。

    让我们看一下一个简单的 URL 并讨论它的各个部分:

    https://twitter.com/inancgumus

    抽象地看如下:

    scheme://hostname/path

    如图 2.6 所示,您将 URL 字符串传递给 Parse 函数。然后,它分析 URL 字符串,并返回 URL 结构的指针值。URL 结构类型包含 URL 部分,例如方案、主机和路径。

    图2.6 解析网址

    1. 首先将原始 URL 作为字符串值传递给 Parse 函数。
    2. 然后,Parse 函数分析原始 URL 并返回 URL 结构的指针值。
    3. 如果 Parse 函数可以分析原始 URL,它将返回 nil 错误。零错误意味着一切正常。否则,它将返回一条特定的错误消息,说明为什么无法解析原始 URL。
    4. *URL 值包含原始 URL 的各个部分。例如,假设您将“https://foo.com/go”传递给 Parse 函数。然后该函数将返回一个 *URL 值,其中包含方案字段“https”、主机字段“foo.com”和路径字段“go”。
    关于解析器

    一个成熟的解析器涉及更多的事情,但你会让事情更易于管理。因此,您只会解析 URL 的方案、主机名和路径。没有必要让事情变得不必要的复杂,让你咬指甲。

    2.3.1 解析方案

    在本节中,您将首先解析 URL 的方案。首先,您将向上一个测试函数添加新的测试用例。测试失败后,您将编写必要的分析器代码以通过测试。在表 2.3 中,您可以看到要解析的内容。

    表 2.3.网址方案

    输入:原始网址

    输出:方案

    https://foo.com

    https

    http://foo.com

    http

    如表 2.3 所示,如果 Parse 函数收到包含 https://foo.com 作为原始 URL 的字符串值,它将使用 https 填充 URL 类型的方案字段。或者,如果它收到 http://foo.com,它将用http填充“方案”字段。

    编写测试用例

    在你最喜欢的其他语言中,你可能分三个阶段编写测试用例:排列行动断言。不用担心,在围棋中没有什么不同。最好使用以下阶段编写测试:

    注意

    清单 2.9 中的测试是故意破坏的(变量 u 丢失)。

    让我们在实际操作中使用此实践(清单 2.9),假设您要解析 https://foo.com 。

    示例 2.9: 测试 URL 方案 (url_test.go)

    func TestParse(t *testing.T) { const rawurl = "https://foo.com" #A if err := Parse(rawurl); err != nil { t.Logf("Parse(%q) err = %q, want nil", rawurl, err) t.Fail() } want := "https" #B got := u.Scheme #C if got != want { #D t.Logf("Parse(%q).Scheme = %q; want %q", rawurl, got, want) #E t.Fail() #F } }

    正如您在清单 2.9 中看到的,如果收到错误,则会记录一条描述性失败消息。但这一次,你添加了.解析(%q)旁边的方案。这样,您可以快速看到您正在从 Parse 函数返回的内容中获取 Scheme 字段。

    想要的命名约定

    根据这个习语,测试想要从它想要验证的代码中得到一些东西。然后它检查它是否得到了想要的东西。通常,我更喜欢使用此命名约定,但您根本不需要使用此约定。您可以选择任何您喜欢的约定。例如,您可以使用“exp”(预期的缩写)而不是“want”。

    声明网址类型

    由于您没有从 Parse 函数获取解析的 URL,因此在运行测试时将收到错误:未定义:u 。因此,您将创建一个名为 URL 的新类型,并将其作为指针从 Parse 函数返回:

    1. 目前,您只需要 URL 类型中的方案字段
    2. 您将在 url.go 文件中声明类型,添加字段,并从 Parse 函数返回一个新的 *URL 值
    3. 然后,您将更改 url_test.go 文件并从解析函数中检索解析的 *URL

    让我们首先从 url.go 文件开始,并在其中添加 URL 类型,如清单 2.10 所示。

    示例 2.10: 创建URL类型 (url.go)

    // A URL represents a parsed URL. #A type URL struct { #B // https://foo.com #C Scheme string // https #D }

    现在,是时候从 Parse 函数返回方案了。因此,起初,您不会解析方案,而是会故意返回错误的方案。你可以在清单 2.11 中看到你将要写的内容。

    示例 2.11: 返回一个带有方案的 URL (url.go)

    func Parse(rawurl string) (*URL, error) { #A return &URL{"fake"}, nil #B }

    现在,Parse 函数返回两个值:

    1. *URL:指向已解析的 URL 值的指针。在示例 2.11 中,你返回了一个带有假方案的 *URL。
    2. error : 在清单 2.11 中,您返回一个 nil 错误值,因为您不希望 Parse 函数在测试函数的第一部分失败。

    由于 Parse 函数再返回一个值,因此您还需要更改测试函数。您将得到解析后的 URL,并在清单 2.12 中放入一个名为 u 的变量。

    示例 2.12: 获取解析后的 URL (url_test.go)

    func TestParse(t *testing.T) { const rawurl = "https://foo.com" u, err := Parse(rawurl) #A if err != nil { ... } ... got := u.Scheme ... }

    清单 2.12 中的测试函数看起来不错。它解析原始网址并获取解析的 URL。然后,它从解析的 URL 中获取方案字段。现在,是时候执行您的新测试了。运行测试时,输出将类似于以下内容:

    $ go test --- FAIL: TestParse Parse("https://foo.com").Scheme = "fake"; want "https"

    该消息告诉您有关测试失败原因的一些事项:

    祝贺!您已验证测试是否有效,并且失败消息看起来是描述性的。智人程序员还想要什么?

    编写解析器代码

    您可能确定新测试有效。因此,现在您要从 URL 解析方案,而不是返回假方案。您将编写简单的解析器代码,该代码将按此值“://”拆分原始 URL。您将首先更改清单 2.13 中的 url.go,然后执行测试以确保它正常工作。

    示例 2.13: 解析方案 (url.go)

    ... import ( "strings" ... ) ... func Parse(rawurl string) (*URL, error) { i := strings.Index(rawurl, "://") #A scheme := rawurl[:i] #B return &URL{scheme}, nil #C }注意

    请不要忘记导入字符串包。

    运行测试时将通过。美妙!现在,您已完成一项任务,并且越来越接近实现 URL 解析器。令人兴奋,不是吗?

    修复解析器代码

    假设您正在弹出刚刚编写的测试,并在 Parse 函数中意识到一个致命问题。带着孩子般的兴奋,你想知道如果你在没有方案的情况下将格式错误的 url 传递给 Parse 函数会发生什么。现在玩这个怎么样?因此,让我们按如下方式更改原始网址:

    func TestParse(t *testing.T) { const rawurl = "foo.com" ... }

    测试时,输出类似于以下内容:

    $ go test --- FAIL: TestParse (0.00s) panic: runtime error: slice bounds out of range [:-1] [recovered] goroutine 6 [running]: testing.tRunner.func1.2(0x1134620, 0xc00001c1b0) /usr/local/go/src/testing/testing.go:1144 0x332 ...

    测试惊慌失措。但是问题出在哪里呢?让我们看看示例 2.13,其中您使用了一个名为 strings 的函数。指数。

    您可以非常轻松地解决问题。如果字符串,您可以返回错误。索引函数在原始 URL 中找不到方案模式。您可以在清单 2.14 中看到修复。

    示例 2.14: 修复解析器 (url.go)

    func Parse(rawurl string) (*URL, error) { i := strings.Index(rawurl, "://") if i < 0 { #A return nil, errors.New("missing scheme") #A } #A scheme := rawurl[:i] ... }

    恐慌测试功能

    恐慌总是属于单个 Goroutine,这就是为什么 Go 运行时也会打印恐慌的 Go 例程及其堆栈跟踪。如果您正在编码,则在分析计算机上的堆栈跟踪时,可以找到错误的根源。

    失败方法

    但是当你运行测试时,你遇到了另一个问题:

    $ go test --- FAIL: TestParse Parse("foo.com") err = "missing scheme", want nil panic: runtime error: invalid memory address...

    您看到测试捕获了解析错误并显示了缺少的方案错误:

    Parse("foo.com") err = "missing scheme", want nil

    但下一个错误消息更有趣:

    panic: runtime error: invalid memory address...

    测试再次惊慌失措!但是为什么?请记住,如果原始 URL 不包含方案,则返回 nil *URL 值(清单 2.14)。让我们看一下图 2.7 中的测试函数。

    图 2.7 Fail 方法不停止测试函数

  • 您可以看到测试函数尝试从 nil *URL 值中获取方案字段,从而导致测试函数中出现崩溃。出现此问题的原因是 Fail 方法不会停止测试函数。因此,即使使用零 *URL,测试函数也能继续工作!
  • 如果当测试函数从解析函数收到错误时可以阻止它运行,则可以解决此问题。解决方案就像听起来一样简单。

    失败现在方法

    您需要使用另一个来自 *T 类型的名为 FailNow 的方法,而不是调用 Fail 方法。让我们了解一下 FailNow 方法的实现,以更好地了解它的内部工作原理。正如清单 2.15 中看到的,FailNow 方法使测试函数失败并立即停止执行。

    示例 2.15: *T 类型的 FailNow 方法

    // src/testing/testing.go package testing // FailNow marks the function as having failed and stops its execution // by calling runtime.Goexit (which then runs all deferred calls in the // current goroutine). func (c *T) FailNow() { #A c.Fail() #B ... runtime.Goexit() #C }

    提示

    您可以在以下链接中找到 Go 标准库的源代码:https://github.com/golang/go/tree/master

    测试包在单独的 Go例程中运行每个测试函数。即使测试函数调用 FailNow 并终止,其他测试函数也会继续运行(测试函数可以位于同一个测试文件或其他文件中)。测试包还将捕获测试函数何时结束,并在测试摘要中报告。

    因此,让我们在测试函数中使用 FailNow 方法而不是 Fail 方法。您可以在清单 2.16 中看到代码。

    示例 2.16: 使用 FailNow 方法 (url_test.go)

    func TestParse(t *testing.T) { ... u, err := Parse(rawurl) if err != nil { ... t.FailNow() #A } ... #B }

    运行测试时,输出应如下所示:

    $ go test --- FAIL: TestParse Parse("foo.com") err = "missing scheme", want nil

    如您所见,现在测试失败,并且您没有收到紧急错误。调用 FailNow 方法后,测试函数停止运行。这样,您就可以避免代码在解析的 URL 为 nil 时尝试获取 Scheme 字段。您通常希望在发生致命错误时停止测试函数,或者无需继续运行测试函数。

    致命和致命方法

    假设您还在处理另一个项目并编写大量测试函数。但是你已经厌倦了每次想要测试某些东西时调用日志记录和错误函数。我有个好消息要告诉你!如表 2.4 所示,有更好的方法。测试包提供了用于同时记录和结束测试的 Fatal 和 Fatalf 方法。

    表 2.4.致命的方法

    方法

    目的

    t.Fatal

    它等效于调用日志和失败现在方法

    t.Fatalf

    它等效于调用 Logf 和 FailNow 方法

    当测试函数调用 Fatal 方法时,测试包将记录一条消息,并通过调用 FailNow 方法立即停止测试函数。就像 Log 方法一样,Fatal 方法采用可变参数( ...任何 )。因此,您可以使用任何类型和数量的值来调用它。它将在调用方法时记录这些值。

    日志和致命方法之间的唯一区别是 Fatal 方法也会停止运行测试函数。

    Fatalf 方法类似于 Fatal 方法,但它也允许您使用 format 参数传递格式说明符。因此,您可以像使用Logf方法一样使用它。

    现在您已经了解了 Fatal 和 Fatalf 方法的作用,您可以将测试函数中的 Logf 和 FailNow 方法替换为清单 2.17 中的单个 Fatalf 方法。

    示例 2.17: 使用 Fatalf 方法 (url_test.go)

    func TestParse(t *testing.T) { ... if err := Parse(rawurl); err != nil { t.Fatalf("Parse(%q) err = %q, want nil", rawurl, err) #A // t.Fatalf is the same as calling the following methods: // t.Logf("Parse(%q) err = %q, want nil", rawurl, err) #B // t.FailNow() #B } ... #C }

    运行测试时,将具有与以前相同的输出:

    $ go test --- FAIL: TestParse Parse("foo.com") err = "missing scheme", want nil

    在清单 2.17 中,您没有使用 FailNow 方法,因为您想将描述性错误消息打印为友好的 gopher。如果只想使测试函数失败而不记录消息,则可以使用 FailNow 方法。最后,多亏了 Fatalf 方法,您将两个方法调用替换为一个方法调用。这样做使代码更简洁且更具可读性。

    空接口类型和省略号

    在了解空接口类型之前,让我们看一下 Fatal 和 Fatalf 方法的实现,因为它们使用空接口类型:

    // Fatal is equivalent to Log followed by FailNow. func (c *T) Fatal(args ...any) { c.log(fmt.Sprintln(args...)) c.FailNow() }

    它采用可变参数,记录失败消息,并结束调用方测试函数。Fatalf 方法类似于 Fatal 方法,但它也为失败消息添加格式化程序参数:

    // Fatalf is equivalent to Logf followed by FailNow. func (c *T) Fatalf(format string, args ...any) { c.log(fmt.Sprintf(format, args...)) c.FailNow() }

    any 类型可以表示任何类型,因此得名。

    它实际上是一种接口类型。Go 1.18 将空接口类型(接口{})重命名为 any 类型。它们是相同的类型!

    interface{} 表示没有任何方法的接口类型,因此它实际上是空的,但很有用!这意味着 Go 中的任何类型都可以满足这个空接口类型。您可以将其视为来自Java(或Javascript)的对象类型或来自Python的对象类型。

    例如:

    var anything interface{} // Or (same as above): // var anything any anything = 3 anything = "three" anything = []string{"let", "there", "be", "light"}

    另一方面,省略号 ( ... 使函数接受可变数量的参数。所以一个函数有一个...任何参数都可以接受任意数量的值的任何类型的值。

    错误和错误方法

    你已使用更有用的 Fatalf 方法重构了测试函数。同样,每次希望测试失败时调用日志记录和错误函数也很麻烦。正如您在表 2.5 中看到的,幸运的是,测试包提供了另外两种称为 Error 和 Errorf 的方法,用于同时记录和失败测试。

    表 2.5.错误方法

    方法

    目的

    t.Error

    它等效于调用日志和失败方法。

    t.Errorf

    它等效于调用 Logf 和 Fail 方法。

    在清单 2.18 中,您可以看到 Error 方法的实现。它需要可变参数(...any ),以便将任意类型和数量的值传递给该方法。与 Fatal 方法一样,Error 方法首先记录错误消息,然后测试函数失败。它们之间的区别在于 Error 方法仅将测试函数标记为失败,并继续运行测试函数。

    示例 2.18: Error 方法的实现

    // src/testing/testing.go package testing // Error is equivalent to Log followed by Fail. func (c *T) Error(args ...any) { #A c.log(fmt.Sprintln(args...)) #B c.Fail() #C }

    在清单 2.19 中,您可以看到 Errorf 方法的实现。它需要一个格式说明符和可变参数( ...任何 )。

    示例 2.19: Errorf 方法的实现

    // src/testing/testing.go package testing // Errorf is equivalent to Logf followed by Fail. func (c *T) Errorf(format string, args ...any) { #A c.log(fmt.Sprintf(format, args...)) #B c.Fail() #C }

    与 Error 方法类似,即使测试函数失败,Errorf 方法也会继续运行测试函数。它首先记录一条错误消息,然后将测试函数标记为失败。与 Error 方法的区别在于,Errorf 方法还采用格式说明符。因此,它允许您打印描述性失败消息。

    重构测试

    现在是时候重构你编写的测试了。让我们将测试函数中的 Logf 和 Fail 方法替换为单个 Errorf 方法(示例 2.20)。如您所知,在后台,Errorf 方法将为您调用 Logf 和 Fail 方法。由于你想要格式化的输出,所以你没有使用 Error 方法,而是使用了 Errorf(清单 2.20)。您使用单个 Errorf 方法调用摆脱了两个方法调用,并实现了相同的结果。

    示例 2.20: 调用 Errorf 方法 (url_test.go)

    func TestParse(t *testing.T) { ... if got != want { t.Errorf("Parse(%q).Scheme = %q; want %q", rawurl, got, want) #A // t.Errorf is the same as calling the following methods: #B // t.Logf("Parse(%q).Scheme = %q; want %q", rawurl, got, want) #B // t.Fail() #B } // ... #C }

    让我们看一下清单 2.21 中的最终测试函数。您是否注意到 get 变量位于 if 语句旁边?这样做是一种很好的做法,因为测试函数仅在 if 语句中使用该变量,从而使代码简洁!您可以在清单 2.21 中看到最终的测试函数。

    示例 2.21: 调用 Errorf 方法 (url_test.go)

    func TestParse(t *testing.T) { const rawurl = "https://foo.com" u, err := Parse(rawurl) if err != nil { t.Fatalf("Parse(%q) err = %q, want nil", rawurl, err) } want := "https" if got := u.Scheme; got != want { #A t.Errorf("Parse(%q).Scheme = %q; want %q", rawurl, got, want) #A } #A }

    当您运行测试时,它会通过,并且不会惊慌失措。如果提供的 URL 没有方案,则测试函数将使用 Fatalf 方法停止运行。因此,测试函数不会尝试从 nil *URL 值中获取方案。仅当 Parse 函数可以分析 URL 时,测试函数才会继续运行。如果成功但无法解析方案,则测试输出会将测试标记为失败,并使用 Errorf 函数报告错误。

    *好!您现在有一个带有惯用测试的 URL 解析器。当然,你还没有完成。在下一节中,您将解析原始 URL 的主机名。但在进入下一节之前,让我们讨论一下到目前为止您学到了什么。

    您编写了包含安排、行动和断言阶段的测试用例。您在安排阶段设置您的期望,在操作阶段运行受测代码,最后,如果您在断言阶段得到意外结果,则将测试标记为失败:

    want := "https" // arrange got := u.Scheme // act if got != want {} // assert

    您还了解了 got-want 命名约定,其中您将测试期望放在一个名为 want 的变量中,然后运行代码并将结果放在名为 get 的变量中。正如我之前所说,这只是一种命名约定,只要您和其他开发人员习惯使用它,您就可以发明自己的方式。

    您还了解了何时使用某些测试方法。您可以使用 fatal 方法结束测试函数,以便剩余的测试函数将停止运行。但是,如果您希望测试函数继续运行,请使用错误方法。您可以在表 2.6 中找到您学到的所有测试方法的摘要。

    表 2.6.*测试。到目前为止使用的 T 方法

    方法

    目的

    t.Log(args ...any)

    将消息记录到测试输出

    t.Logf(format string, args ...any)

    将格式化的日志消息记录到测试输出

    t.Fail()

    将测试标记为失败测试并继续运行测试函数

    t.FailNow()

    将测试标记为失败的测试并停止运行测试函数

    t.Error(args ...any)

    它等效于调用日志和失败方法

    t.Errorf(format string, args ...any)

    它等效于调用 Logf 和 Fail 方法

    t.Fatal(args ...any)

    它等效于调用日志和失败现在方法

    t.Fatalf(format string, args ...any)

    它等效于调用 Logf 和 FailNow 方法

    您还稍微拉开了帷幕,看看测试包如何运行测试功能。您了解到测试包在单独的 Go例程中运行每个测试函数。因此,即使其中一个测试函数出现恐慌,测试包仍然可以捕获并报告错误。

    2.3.2 解析主机名

    在前面的部分中,您从 URL 分析了方案。威世智团队也想分析URL的主机。这样,当请求来自一组特定的域时,他们可以重定向 Web 请求。在本节中,您将解析 URL 的主机名。因此,当您将“https://foo.com”传递给 Parse 函数时,它将返回一个 *URL 值,其中包含方案字段“https”和主机字段“foo.com”。您可以在表 2.7 中看到您将解析的内容。

    表 2.7.网址主机名

    原始网址

    方案

    主机

    https://foo.com

    https

    foo.com

    编写测试用例

    让我们开始向现有测试函数添加新的测试用例。正如您在示例 2.22 中看到的,新的测试用例与前一个测试用例类似(示例 2.21)。首先,您获取主机字段(尚不存在),然后将其与主机名进行比较。如果预期的主机名与解析的主机名不匹配,则将测试标记为失败。

    示例 2.22: 测试主机名 (url_test.go)

    func TestParse(t *testing.T) { const rawurl = "https://foo.com" ... if got, want := u.Host, "foo.com"; got != want { t.Errorf("Parse(%q).Host = %q; want %q", rawurl, got, want) } }提示

    '如果变量 := 值;条件“称为短 if 声明。您可以使用它在 if 语句的作用域中声明变量。在较短的作用域中声明变量是一种很好的做法,有助于避免不必要的变量污染作用域命名空间。

    运行测试时,您将收到一条错误消息,告诉您 Host 字段尚不存在:

    $ go test u.Host undefined (type *URL has no field or method Host)

    此错误验证编译器是否可以编译测试,唯一的错误是缺少 Host 字段。

    编写解析器代码

    首先将主机字段添加到 URL 类型,然后在 Parse 函数中编写分析逻辑。您可以在清单 2.23 中看到更新的 URL 类型。

    示例 2.23: 将 Host 字段添加到 URL (url.go)

    type URL struct { // https://foo.com Scheme string // https Host string // foo.com }

    现在您已经有了必要的字段,让我们开始编写 Host 字段的分析逻辑。您可以在清单 2.24 中看到更新的代码。

    示例 2.24: 解析函数 (url.go)

    func Parse(rawurl string) (*URL, error) { i := strings.Index(rawurl, "://") ... // scheme := rawurl[:i] #A scheme, host := rawurl[:i], rawurl[i 3:] #B return &URL{scheme, host}, nil #C }

    在当前版本的 Parse 函数中,您可以在原始 URL 中找到方案的起始索引。因此,其余部分包含您要查找的主机名。您可以通过超出方案的起始位置加三来轻松获取主机名。这是因为您希望方案以“://”模式结尾。如果运行测试函数,则可以看到它通过。

    伟大!威世智团队可以获取URL的主机名,以将请求重定向到新域或停止接受来自该特定域的流量。

    2.3.3 解析路径

    您已将 url 包添加到 Go 标准库,并让威世智团队知道此新更改。他们开始使用包,但他们也想分析 URL 的路径。以便他们可以接受或拒绝对路径的访问。或者,他们可以更改 URL 的路径并将 Web 请求重定向到新位置。在本节中,您将分析 URL 的路径。让我们看一下您将在表 2.8 中解析的内容。

    表 2.8.网址路径

    原始网址

    方案

    主机

    路径

    https://foo.com

    https

    foo.com

    ""

    https://foo.com/go

    https

    foo.com

    提示

    “” 在 Go 中称为空字符串。它不同于null或未定义,不像其他一些语言。它是一个有效的字符串值,其长度为零(len(“”) 等于 0)。字符串变量可以具有空字符串值,您仍然可以获取其内存地址。例如:e := “” 。然后: p := &e .变量 p 将指向 e 的内存地址。

    如表 2.8 所示,当原始 URL 没有路径时,将返回一个空字符串。否则,您将分析原始 URL 中的路径,并将其放入 URL 类型中名为 Path 的新字段中。像往常一样,您将从一个新的测试用例开始。然后,您将向 URL 类型添加路径字段并相应地修改解析器。

    重新访问解析器代码

    之前在清单 2.24 中,您只从原始 URL 解析了方案和主机名。现在,您还需要一个在主机名之后开始的路径。例如,在 “https://foo.com/go” 中,路径为 “go”。但是当前解析器无法单独解析主机名和路径。让我们尝试解析器如何通过更改 url_test.go 中的 rawurl 来做出反应,如下所示:

    const rawurl = "https://foo.com/go"

    运行测试时,您将看到以下错误:

    $ go test Parse("https://foo.com/go").Host = "foo.com/go"; want "foo.com"

    从失败消息中可以看到,当前的解析逻辑是将主机名与路径一起解析。它将路径放入“主机”字段中。因此,您需要找到一种方法来将路径与主机名分开解析。为此,您需要将原始URL的其余部分存储在一个名为 rest 的新变量中。然后,您将解析该变量中的主机名和路径。

    现在,让我们首先解析主机名(如果原始 URL 中有路径)。您可以在清单 2.25 中看到更新的代码。通过此更改,如果运行测试,测试将通过。

    示例 2.25: 解析路径 (url.go)

    func Parse(rawurl string) (*URL, error) { ... scheme, rest := rawurl[:i], rawurl[i 3:] #A host := rest #B if i := strings.Index(rest, "/"); i >= 0 { #C host = rest[:i] #D } #C return &URL{scheme, host}, nil }编写测试用例

    现在,您的新解析器可以解析主机名,无论原始 URL 中是否存在路径。所以最后,是时候解析路径了。让我们首先向测试函数添加一个新的测试用例,如示例 2.26 所示。

    示例 2.26: 测试路径 (url_test.go)

    func TestParse(t *testing.T) { const rawurl = "https://foo.com/go" #A ... if got, want := u.Path, "go"; got != want { #B t.Errorf("Parse(%q).Path = %q; want %q", rawurl, got, want) } }编写解析器代码

    如果运行清单 2.26 中的测试,它将失败,因为 URL 类型中还没有 Path 字段。清单 2.27 将 Path 字段添加到 URL 类型中。

    示例 2.27: 添加路径字段 (url.go)

    type URL struct { // https://foo.com/go ... Path string // go }

    现在,您在URL类型中具有“路径”字段;是时候解析原始 URL 路径了。您可以在示例 2.27 中看到更新的代码。

    示例 2.28: 解析路径 (url.go)

    func Parse(rawurl string) (*URL, error) { ... host, path := rest, "" #A if i := strings.Index(rest, "/"); i >= 0 { host, path = rest[:i], rest[i 1:] #B } return &URL{scheme, host, path}, nil #C }

    通过此更改,新的解析器可以使用或不使用路径。而且,仅当原始 URL 中有路径时,您才解析路径。*好!如果运行测试,测试将通过。

    关于多个作业

    在 Go 中,您可以一次分配多个变量。例如,如果要将 rest 变量分配给主机变量,将空字符串分配给 path 变量,则可以执行以下操作:

    host, path := rest, ""

    上面的代码等效于以下内容:

    host := rest path := ""

    有关有关多个作业的所有规则的更多信息,请参阅我的帖子链接:https://stackoverflow.com/questions/17891226/difference-between-and-operators-in-go/45654233#45654233 和 https://blog.learngoprogramming.com/learn-go-lang-variables-visual-tutorial-and-ebook-9a061d29babe

    2.3.4 总结

    让我们总结一下到目前为止您在本节中学到的内容:

    2.4 小结

    哇!那是一次冒险!你学到了很多关于编写惯用代码和用 Go 进行测试的东西。*好!我认为最好的学习方法是实践。我希望你在阅读本章时跟着编码。随着测试的增长,它们将变得复杂。管理它们将很困难。在下一章中,您还将学习如何驯服复杂性怪物。

    查看全文
    大家还看了
    也许喜欢
    更多游戏

    Copyright © 2024 妖气游戏网 www.17u1u.com All Rights Reserved