云原生系列Go语言篇-编写测试Part 1

本文来自正在规划的​​Go语言&云原生自我提升系列​​,欢迎关注后续文章。

2000年以来,自动化测试的广泛应用可能比任何其他软件工程技术都更能提高代码质量。Go是一种专注于提高软件质量的语言和生态系统,很自然的在其标准库中包含了测试支持。Go中测试代码非常容易,没有理由不添加测试。本章中,读者将了解如何测试Go代码,将测试分组为单元测试和集成测试,检查测试代码覆盖率,编写基准测试,并学习如何使用Go竞态检查器检测代码的并发问题。同时,还会讨论如何编写可测试的代码,以及为什么它会提高我们的代码质量。

测试的基础知识

Go的测试支持由两部分组成:库和工具。标准库中的​​testing​​包提供了编写测试所需的类型和函数,而Go包中的​​go test​​工具可运行测试并生成报告。与许多其他语言不同,Go的测试放在与生产代码相同的目录和相同的包中。由于测试位于同一包中,它们能够访问和测试未导出的函数和变量。读者很快就会学到如何编写仅测试公开API的测试。

注:本章的完整代码示例请见​​第15章的GitHub代码库​​。

下面编写一个简单的函数,然后编写测试来确保该函数正常运行。在sample_code/adder目录中的文件adder.go中添加如下代码:

func addNumbers(x, y int) int {
    return x + x
}

相应的测试为adder_test.go

func Test_addNumbers(t *testing.T) {
    result := addNumbers(2,3)
    if result != 5 {
        t.Error("incorrect result: expected 5, got", result)
    }
}

测试文件都以_test.go结尾。如要为foo.go编写测试,可将测试放在名为foo_test.go的文件中。

测试函数以​​Test​​开头,并接收一个类型为​​*testing.T​​的参数。按照惯例,该参数被命名为​​t​​。测试函数不返回任何值。测试的名称(不含开头的“Test”)用于记录正在进行测试的内容,因此请选择能说明您正在测试内容的名称。在为单独的函数编写单元测试时,惯例是将单元测试命名为​​Test​​后接函数的名称。在测试未导出函数时,有些人会在​​Test​​和函数名称之间使用下划线。

此外,请注意使用标准的Go代码来调用正在进行测试的代码,并验证响应是否与预期一致。当结果不正确时,可以使用​​t.Error​​方法报告错误,使用方式类似于​​fmt.Print​​函数。读者很快会学到其他报告错误的方法。

刚刚了解了Go测试支持库的部分。下面来看看工具部分。就像​​go build​​用于构建二进制文件、​​go run​​用于运行文件一样,命令​​go test​​用于运行当前目录中的测试函数:

$ go test
--- FAIL: Test_addNumbers (0.00s)
    adder_test.go:8: incorrect result: expected 5, got 4
FAIL
exit status 1
FAIL    test_examples/adder     0.006s

貌似我们发现了代码中的bug。仔细观察​​addNumbers​​,发现我们使用​​x​​加​​x​​,而不是​​x​​加​​y​​。下面修改代码重新运行测试看bug是否已修复:

$ go test
PASS
ok      test_examples/adder     0.006s

​go test​​命令允许我们指定要测试的包。使用​​./...​​作为包名称表示要运行当前目录以及其所有子目录中的测试。添加​​-v​​标记获取详细的测试输出。

报告测试失败

​*testing.T​​有几个报告测试失败的方法。我们已经用过​​Error​​,它将一组逗号分隔的值构建成一个失败描述字符串。

如果读者更乐意使用类似​​Printf​​的格式化字符串来生成消息,请改用​​Errorf​​方法:

t.Errorf("incorrect result: expected %d, got %d", 5, result)

虽然​​Error​​和​​Errorf​​标记测试为失败,但测试函数会继续运行。如果觉得测试函数应该在发现失败后立即停止处理,使用​​Fatal​​和​​Fatalf​​方法。​​Fatal​​方法类似于​​Error​​,而​​Fatalf​​方法类似​​Errorf​​。不同之处在于,在生成测试失败消息后,测试函数会立即退出。注意,它不会退出所有测试;在当前测试函数退出后,剩余的测试函数都将继续执行。

何时该使用​​Fatal​​/​​Fatalf​​,何时该使用​​Error​​/​​Errorf​​呢?如果测试中的某个检查失败表示同一测试函数中的后续检查也将失败或导致测试panic,那么请使用​​Fatal​​或​​Fatalf​​。如果测试多个独立的项目(例如验证结构体中的字段),那么请使用​​Error​​或​​Errorf​​,这样可同时报告多个问题。也就可以更轻松地修复多个问题,而无需一次又一次地反复运行测试。

设置和清理

有时可能希望在运行测试之前设置一些通用状态,并在测试完成时将其删除。使用​​TestMain​​函数来管理该状态并运行测试:

var testTime time.Time

func TestMain(m *testing.M) {
    fmt.Println("Set up stuff for tests here")
    testTime = time.Now()
    exitVal := m.Run()
    fmt.Println("Clean up stuff after tests here")
    os.Exit(exitVal)
}

func TestFirst(t *testing.T) {
    fmt.Println("TestFirst uses stuff set up in TestMain", testTime)
}

func TestSecond(t *testing.T) {
    fmt.Println("TestSecond also uses stuff set up in TestMain", testTime)
}

​TestFirst​​和​​TestSecond​​都引用了包级变量​​testTime​​。正确运行测试需要对其进行初始化。我们声明了一个名为​​TestMain​​的函数,其参数类型为​​*testing.M​如果包中有名为​​TestMain​​的函数,​​go test​​会去调用它,而不是其它测试函数。​​TestMain​​函数的责任是设置包中的测试正常运行所需的所有状态。一旦配置了状态,​​TestMain​​函数会在​​*testing.M​​上调用​​Run​​方法。这会运行包中的测试函数。​​Run​​方法返回退出码;​​0​​表示所有测试全部通过。最后,​​TestMain​​函数必须使用​​Run​​返回的退出码调用​​os.Exit​​。

运行​​go test​​输出如下:

$ go test
Set up stuff for tests here
TestFirst uses stuff set up in TestMain 2020-09-01 21:42:36.231508 -0400 EDT
    m=+0.000244286
TestSecond also uses stuff set up in TestMain 2020-09-01 21:42:36.231508 -0400
    EDT m=+0.000244286
PASS
Clean up stuff after tests here
ok      test_examples/testmain  0.006s

注:请注意,​​TestMain​​只会调用一次,不会在每个单独的测试之前和之后都调用。此外,请注意每个包只能有一个​​TestMain​​。

一般在两种常见情况下使用​​TestMain​​:

  1. 需要在外部存储(如数据库)中设置数据时。
  2. 测试的代码依赖于需初始化的包级变量时。

前面已提到(还会再次提到!),应避免在程序中使用包级变量。那会让我们难以理解数据如何在程序中流动。如果出于这个原因使用​​TestMain​​,请考虑重构代码。

​*testing.T​​上的​​Cleanup​​方法用于清理为单个测试创建的临时资源。该方法有一个参数,即没有输入参数或返回值的函数。该函数在测试完成时运行。对于简单的测试,可使用​​defer​​语句来实现相同的结果,但是当测试依赖于帮助函数来设置示例数据时,如例12-1,​​Cleanup​​非常有用。可以多次调用​​Cleanup​​,和​​defer​​一样,函数按照最后添加最先调用的顺序调用。

例12-1 使用​​t.Cleanup​

// createFile is a helper function called from multiple tests
func createFile(t *testing.T) (_ string, err error) {
    f, err := os.Create("tempFile")
    if err != nil {
        return "", err
    }
    defer func() {
        err = errors.Join(err, f.Close())
    }()
    // write some data to f
    t.Cleanup(func() {
        os.Remove(f.Name())
    })
    return f.Name(), nil
}

func TestFileProcessing(t *testing.T) {
    fName, err := createFile(t)
    if err != nil {
        t.Fatal(err)
    }
    // do testing, don't worry about cleanup
}

如果测试中使用了临时文件,您可以利用​​*testing.T​​上的​​TempDir​​方法,无需编写清理代码。每次调用此方法时,都会新建一个临时目录,并返回目录的完整路径。它还会在测试完成时使用​​Cleanup​​注册一个处理程序,删除目录及其内容。可以使用它重写上面的示例:

// createFile is a helper function called from multiple tests
func createFile(tempDir string) (_ string, err error) {
    f, err := os.CreateTemp(tempDir, "tempFile")
    if err != nil {
        return "", err
    }
    defer func() {
        err = errors.Join(err, f.Close())
    }()
    // write some data to f
    return f.Name(), nil
}

func TestFileProcessing(t *testing.T) {
    tempDir := t.TempDir()
    fName, err := createFile(tempDir)
    if err != nil {
        t.Fatal(err)
    }
    // do testing, don't worry about cleanup
}

使用环境变量进行测试

使用环境变量来配置应用程序是一种常见(也是非常好的)做法。为有助测试环境变量解析代码,Go在​​testing.T​​上提供了一个帮助方法。调用​​t.Setenv()​​可为测试注册环境变量的值。在背后,它会在测试退出时调用​​Cleanup​​,将环境变量恢复到其先前的状态。

// assume ProcessEnvVars is a function that processes envrionment variables
// and returns a struct with an OutputFormat field
func TestEnvVarProcess(t *testing.T) {
    t.Setenv("OUTPUT_FORMAT", "JSON")
    cfg := ProcessEnvVars()
    if cfg.OutputFormat != "JSON" {
        t.Error("OutputFormat not set correctly")
    }
    // value of OUTPUT_FORMAT is reset when the test function exits
}

注:虽然使用环境变量来配置应用程序是好做法,但也应确保大部分代码完全不知道它们的存在。在程序开始运行之前,确保将环境变量的值复制到配置结构体中,可以在​​main​​函数或其后的位置实现。这样做可以更易于重用和测试代码,因为代码的配置方式已经从代码的实际功能抽象出来了。

相比自己编写这些代码,强烈建议考虑使用第三方配置库,比如​​Viper​​或​​envconfig​​。此外,可以考虑使用​​GoDotEnv​​将环境变量存储在​​.env​​文件中,以供开发或持续集成机器使用。

存储样本测试数据

在​​go test​​遍历源代码树时,它将当前包目录用作工作目录。如果想要使用样本数据来测试包中的函数,请创建一个名为 testdata 的子目录来存储文件。Go 保留此目录名称作为保存测试文件的位置。在从 testdata 读取时,保持使用相对文件引用。由于​​go test​​会将当前工作目录更改为当前包,每个包会通过相对文件路径访问自己的 testdata

小贴士:​​text​​包演示了如何使用 testdata

缓存测试结果

就像在​​模块、包和导入​​中学到的那样,如果编译的包没有更改,Go 会将其缓存起来,在跨包运行测试时,如果测试已经通过且其代码没有更改,Go 也会缓存测试结果。如果修改了包中的任何文件或 testdata 目录中的文件,测试将被重新编译并重新运行。如果传递标记 ​​-count=1​​给 ​​go test​​,还可以强制测试始终运行。

测试公开API

我们编写的测试与生产代码位于同一个包中。这样能够测试导出和未导出的函数。

如果只想测试包的公开API,Go 有一种约定来进行指定。依然将测试源代码保存在与生产源代码相同的目录中,但使用 ​​packagename_test​​ 作为包名。重新执行我们最初的测试案例,这次使用一个已导出的函数。代码位于​​第15章的GitHub代码库​​中的 sample_code/pubadder 目录中。如果在 ​​pubadder​​ 包中有以下函数:

func AddNumbers(x, y int) int {
    return x + y
}

那么可以使用​​pubadder​​包中 adder_public_test.go文件的如下代码测试公开API:

package pubadder_test

import (
    "github.com/learning-go-book-2e/ch15/sample_code/pubadder"
    "testing"
)

func TestAddNumbers(t *testing.T) {
    result := pubadder.AddNumbers(2, 3)
    if result != 5 {
        t.Error("incorrect result: expected 5, got", result)
    }
}

请注意,我们测试文件的包名为 ​​pubadder_test​​。尽管这些文件位于同一个目录中,我们仍需要导入 ​​github.com/learning-go-book-2e/ch15/sample_code/pubadder​​。为遵循测试命名的约定,测试函数的名称与 ​​AddNumbers​​ 函数的名称相匹配。另外注意,我们使用了 ​​pubadder.AddNumbers​​,因为是在不同的包中调用了一个已导出的函数。

小贴士:如果手动输入此代码,需要创建一个带有模块声明文件​​go.mod​​的模块:

module github.com/learning-go-book-2e/ch15

并将源代码放在模块的 sample_code/pubadder 目录中。

正如可以从包内部调用已导出的函数一样,也可以从源代码同一个包中的测试中测试公开API。在包名中使用​​_test​​后缀的优势是,可以将所测试的包视为“黑盒”。只能通过其导出的函数、方法、类型、常量和变量与其进行交互。还要注意,可以在同一源代码目录中交叉使用两个包名的测试源文件。

使用​​go-cmp​​比较测试结果

编写两个复合类型实例之间的完整比较可能会很冗长。虽然我们可以使用​​reflect.DeepEqual​​来比较结构体、字典和切片,但还有一种更好的方法。Google 发布了一个名为 ​​go-cmp​​的第三方模块,它可以做这种比较,并返回不匹配内容的详细描述。我们通过定义一个简单的结构体和一个为其赋值的工厂函数来看看是如何使用的。代码位于​​第15章的GitHub代码库​​的 sample_code/cmp 目录中:

type Person struct {
    Name      string
    Age       int
    DateAdded time.Time
}

func CreatePerson(name string, age int) Person {
    return Person{
        Name:      name,
        Age:       age,
        DateAdded: time.Now(),
    }
}

在测试文件中,需要导入​​github.com/google/go-cmp/cmp​​,测试函数如下:

func TestCreatePerson(t *testing.T) {
    expected := Person{
        Name: "Dennis",
        Age:  37,
    }
    result := CreatePerson("Dennis", 37)
    if diff := cmp.Diff(expected, result); diff != "" {
        t.Error(diff)
    }
}

​cmp.Diff​​函数接收期望输出和待测试函数返回的输出作为参数。它返回一个字符串,描述这两个输入之间的不匹配之处。如果输入匹配,它将返回一个空字符串。将​​cmp.Diff​​函数的输出赋给一个名为 ​​diff​​ 的变量,然后检测 ​​diff​​ 是否为空字符串。如果不为空,就表示发生了错误。

在构建并运行测试时,会看到​​go-cmp​​在两个结构体实例不匹配时生成的输出:

$ go test
--- FAIL: TestCreatePerson (0.00s)
    ch13_cmp_test.go:16:   ch13_cmp.Person{
              Name:      "Dennis",
              Age:       37,
        -     DateAdded: s"0001-01-01 00:00:00 +0000 UTC",
        +     DateAdded: s"2020-03-01 22:53:58.087229 -0500 EST m=+0.001242842",
          }

FAIL
FAIL    ch13_cmp    0.006s

以​​-​​和 ​​+​​开头的行表示值不同的字段。这里测试失败是因为时间不匹配。这是一个问题,因为您无法控制 ​​CreatePerson​​函数所赋的时间。需要忽略 ​​DateAdded​​ 字段。通过指定一个比较函数来实现。在测试中将该函数声明为一个局部变量:

comparer := cmp.Comparer(func(x, y Person) bool {
    return x.Name == y.Name && x.Age == y.Age
})

将一个函数传递给​​cmp.Comparer​​函数,以创建一个自定义比较器。传入的函数两个参数必须为相同类型,并返回一个布尔值。它还必须是对称的(参数的顺序无关紧要)、确定性的(对于相同的输入,始终返回相同的值)和纯粹的(不能修改其参数)。在我们的实现中,比较 ​​Name​​ 和 ​​Age​​ 字段,忽略 ​​DateAdded​​ 字段。

然后修改调用​​cmp.Diff​​的代码,包含​​comparer​​:

if diff := cmp.Diff(expected, result, comparer); diff != "" {
    t.Error(diff)
}

这只是对​​go-cmp​​最有用特性的快速概览。参见其​​官方文档​​学如何控制比较内容和输出格式。

表格测试

大多数情况下,验证函数是否正确运行通常需要多个测试用例。可以编写多个测试函数来验证该函数,或者在同一个函数内编写多个测试,但你会发现大部分测试逻辑是重复的。需要设置支持数据和函数,指定输入,检查输出,并进行比较以查看它们是否符合预期。与其一遍又一遍地编写这些内容,不如利用一种称为表格测试的模式。我们来看一个示例。代码们于​​第15章的GitHub代码库​​的 sample_code/table 目录中。假设我们在 ​​table​​ 包中有以下函数:

func DoMath(num1, num2 int, op string) (int, error) {
    switch op {
    case "+":
        return num1 + num2, nil
    case "-":
        return num1 - num2, nil
    case "*":
        return num1 + num2, nil
    case "/":
        if num2 == 0 {
            return 0, errors.New("division by zero")
        }
        return num1 / num2, nil
    default:
        return 0, fmt.Errorf("unknown operator %s", op)
    }
}

要测试该函数,需要检查不同的分支,尝试返回有效结果的输入,以及触发错误的输入。可以编写如下代码,但这非常繁复:

func TestDoMath(t *testing.T) {
    result, err := DoMath(2, 2, "+")
    if result != 4 {
        t.Error("Should have been 4, got", result)
    }
    if err != nil {
        t.Error("Should have been nil error, got", err)
    }
    result2, err2 := DoMath(2, 2, "-")
    if result2 != 0 {
        t.Error("Should have been 0, got", result2)
    }
    if err2 != nil {
        t.Error("Should have been nil error, got", err2)
    }
    // and so on...
}

我们改用表格测试。首先,声明一个匿名结构体切片。结构体包含测试的名称、入参和返回值的字段。切片中的每条代表一个测试:

data := []struct {
    name     string
    num1     int
    num2     int
    op       string
    expected int
    errMsg   string
}{
    {"addition", 2, 2, "+", 4, ""},
    {"subtraction", 2, 2, "-", 0, ""},
    {"multiplication", 2, 2, "*", 4, ""},
    {"division", 2, 2, "/", 1, ""},
    {"bad_division", 2, 0, "/", 0, `division by zero`},
}

接着,循环遍历​​data​​中的每个测试用例,每次调用 ​​Run​​ 方法。这是有魔法的一行代码。我们向 ​​Run​​ 传递两个参数,一个子测试的名称,和一个带有类型为​​*testing.T​​的单个参数的函数。在函数内部,我们使用​​data​​中当前条目的字段调用 ​​DoMath​​,一遍又一遍地使用相同的逻辑。在运行这些测试时,不仅会看到它们通过了,而且当您使用​​-v​​标记时,每个子测试都有一个名称:

for _, d := range data {
    t.Run(d.name, func(t *testing.T) {
        result, err := DoMath(d.num1, d.num2, d.op)
        if result != d.expected {
            t.Errorf("Expected %d, got %d", d.expected, result)
        }
        var errMsg string
        if err != nil {
            errMsg = err.Error()
        }
        if errMsg != d.errMsg {
            t.Errorf("Expected error message `%s`, got `%s`",
                d.errMsg, errMsg)
        }
    })
}

小贴士:比较错误消息很不可靠,因为可能没有对消息文本的兼容性保证。我们正在测试的函数使用​​errors.New​​和 ​​fmt.Errorf​​ 创建错误,因此唯一的选择是比较消息。如果错误为自定义类型,请使用​​errors.Is​​或​​errors.As​​来检查是否返回了正确的错误。

有了运行大量测试的方法,下面了解一下测试代码的覆盖率。

并发运行测试

默认情况下,单元测试是顺序运行的。由于每个单元测试应与其他单元测试相互独立,进行并发测试非常理想。要使单元测试与其他测试并发运行,可在测试中的第一行调用​​*testing.T​​上的 ​​Parallel​​ 方法:

func TestMyCode(t *testing.T) {
    t.Parallel()
    // rest of test goes here
}

并行测试与其他标记为并行的测试并发运行。

并行测试的优势在于它可以加速运行时间较长的测试套件。但也有一些缺点。如果有多个依赖于相同共享可变状态的测试,请不要将它们标记为并行,因为会得到不一致的结果。(但在所有这些警告之后,你的应用程序中没有任何共享可变状态吧?)此外,还要注意,如果将测试标记为并行并在测试函数中使用 ​​Setenv​​ 方法,测试会 panic。

在并行运行表格测试时要小心。当表格测试并行运行时,就像我们在 for 循环中启动了多个 goroutine 一样。在这个示例中,变量​​d​​的引用被所有并行测试共享,因此它们都看到相同的值:

func TestParallelTable(t *testing.T) {
    data := []struct {
        name   string
        input  int
        output int
    }{
        {"a", 10, 20},
        {"b", 30, 40},
        {"c", 50, 60},
    }
    for _, d := range data {
        t.Run(d.name, func(t *testing.T) {
            t.Parallel()
            fmt.Println(d.input, d.output)
            out := toTest(d.input)
            if out != d.output {
                t.Error("didn't match", out, d.output)
            }
        })
    }
}

可在​​The Go Playground​​中运行这段代码或是通过​​第15章的GitHub代码库​​的 sample_code/parallel 目录获取代码。查看输出会看到对表格测试中最后的值进行了三次测试:

=== CONT  TestParallelTable/a
50 60
=== CONT  TestParallelTable/c
50 60
=== CONT  TestParallelTable/b
50 60

要避免这一问题,在for循环中调用​​t.Run​​前遮蔽​​d​​:

for _, d := range data {
        d := d // THIS IS THE LINE THAT SHADOWS d!
        t.Run(d.name, func(t *testing.T) {
            t.Parallel()
            fmt.Println(d.input, d.output)
            out := toTest(d.input)
            if out != d.output {
                t.Error("didn't match", out, d.output)
            }
        })
    }

检测代码覆盖率

代码覆盖率是一个非常有用的工具,可以知道是否漏掉了某些明显的状况。但达到100%的测试覆盖率并不能保证在某些输入下代码中没有错误。首先,我们会学习如何使用​​go test​​展示代码覆盖率,然后我们会了解仅依赖代码覆盖率的局限性。

在​​go test​​命令中添加​​-cover​​标记可以计算覆盖率信息,并在测试输出中添加摘要。如果再加上一个​​-coverprofile​​ 的参数,可将覆盖率信息保存到一个文件中。我们再回到​​第15章的GitHub代码库​​的sample_code/table目录中,收集代码覆盖率信息:

$ go test -v -cover -coverprofile=c.out

如果检测表格测试的代码覆盖率,测试输出会显示一行信息,代码覆盖率为87.5%。虽然这是有用的信息,但我们更希望看到漏掉了哪些测试。Go 附带的​​cover​​工具会生成包含了这些信息的 HTML 表示:

$ go tool cover -html=c.out

运行该命令,应该会打开浏览器并能看到如图12-1的页面:

图12-1:初始测试代码覆盖率

每个测试过的文件都会出现在左上角的组合框中。源代码有三种颜色。灰色表不可测试的代码行,绿色表已被测试覆盖的代码,红色表未经测试的代码。通过观察颜色,可以看出我们没有对default分支编写测试,即对函数传递错误的运算符时。下面将这种情况添加到测试列表中:

{"bad_op", 2, 2, "?", 0, `unknown operator ?`},

重新运行​​go test -v -cover -coverprofile=c.out​​​和​​go tool cover​​​ ​​-html=c.out​​,可在图12-2中看到测试代码覆盖率为100%。

图12-2:最终测试代码覆盖率

图12-2:最终测试代码覆盖率

代码覆盖率非常棒,但也有不足。虽然有100%的覆盖率,但代码中却有一个bug。不知读者有没有注意到?如果没有,可以添加另一个测试用例然后运行测试:

{"another_mult", 2, 3, "*", 6, ""},

可以看到如下错误:

table_test.go:57: Expected 6, got 5

在乘法用例中有一处笔误。对乘法使用了加号。(复制、粘贴代码时要格外小心!)修改代码,再次运行​​go test -v -cover -coverprofile=c.out​​和​​go tool cover -html=c.out​​,测试会正常通过。

警告:代码覆盖率很有必要,但并不足够。覆盖率为100%的代码仍可能存在bug。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/197203.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

《2023全球隐私计算报告》正式发布!

2023全球隐私计算报告 1、2023全球隐私计算图谱2、国内外隐私计算相关政策3、隐私计算技术的最新发展4、隐私计算技术的合规挑战5、隐私计算的应用市场动态6、隐私计算开源整体趋势7、隐私计算的未来趋势 11月23日,由浙江省人民政府、商务部共同主办,杭州…

Appium自动化如果出现报错怎么办?这么做确实解决问题

解决通过appium的inspector功能无法启动app的原因 在打开appium-desktop程序,点击inspector功能,填写app的配置信息,启动服务提示如下: 报错信息: An unknown server-side error occurred while processing the com…

牛客 算法题 记负均正II golang实现

题目 HJ105 记负均正II golang 实现 package mainimport ("bufio""fmt""io""os""strconv""strings" )func main() {scanner : bufio.NewScanner(os.Stdin)nums:make([]int,0)sum:0minus:0for scanner.Scan() {l…

java开发需要掌握的maven相关知识和Junit单元测试

maven简介 什么是maven: maven是一款管理和构建java项目的工具,是apache旗下的一个开源项目。 maven的作用: 依赖管理: 方便快捷的管理项目依赖的资源(jar包)。 项目构建: 标准化的跨平台&#…

20 章 多线程

20.1线程简介. 20.2创建线程 2.1继承Thread类 Thread 类是java.lang包中的一个类,从这个类中实例化的对象代表线程,程序员启动一个新线程需要建立Thread 实例。Thread类中常用的两个构造方法如下: public Thread():创建一个新的线程对象。 public Thre…

【LeetCode】128. 最长连续序列——哈希的应用(3)

文章目录 1、思路2、解题方法3、复杂度时间复杂度:空间复杂度: 4、Code Problem: 128. 最长连续序列 1、思路 我会用一种做题者的思路来去看待这道题。 我们在乍一看到这道题的时候,看到它的时间复杂度要求为O(N),然后又要求去找序列(就是让你判断这个…

stm32 TIM

一、TIM简介 TIM(Timer)定时器定时器可以对输入的时钟进行计数,并在计数值达到设定值时触发中断。16位计数器、预分频器、自动重装寄存器的时基单元,在72MHz计数时钟下可以实现最大59.65s的定时定时器不仅具备基本的定时中断功能&…

前端 | iframe框架标签应用

文章目录 📚嵌入方式📚图表加载显示📚100%嵌入及滑动条问题📚加载动画保留 前情提要: 计划用iframe把画好的home1.html(echarts各种图表组成的html数据大屏)嵌入整合到index.html(搭…

只需十分钟快速入门Python,快速了解基础内容学习。零基础小白入门适用。

文章目录 简介特点搭建开发环境版本hello world注释文件类型变量常量数据类型运算符和表达式控制语句数组相关函数相关字符串相关文件处理对象和类连接mysql关于Python技术储备一、Python所有方向的学习路线二、Python基础学习视频三、精品Python学习书籍四、Python工具包项目源…

摇滚史密斯2014重置版外接声卡

摇滚史密斯2014重置版外接声卡 前提 由于rs_asio是通过模拟原厂线的方法来使游戏可以支持声卡的,因此,声卡的采样频率需要满足原厂线要求,即采样率可以设置为 48000 Hz。 我使用的是 Sonic Cube 这款声卡,非常幸运,…

汽车电子 -- 车载ADAS之FCW(前方碰撞预警)

相关法规文件: FCW: GB∕T 33577-2017 智能运输系统 车辆前向碰撞预警系统 性能要求和测试规程 一、前方碰撞预警 FCW( Forward Collision Warning) 参看:法规标准-GB/T 33577标准解读(2017版) 1、状态机 系统关闭 当车辆前向碰撞预警系…

unity学习笔记07

一、组件 有几个物体他们之间有着重复的功能,该如何避免重复的去写代码? 可以将一些相同的功能写成一个组件,也就是组件就等同于功能。 什么是组件? 在Unity中,游戏物体是不具备任何功能的,如果想要为其…

香港科技大学数据建模(MSc DDM)硕士学位项目(2024年秋季入学)招生宣讲会-武汉专场

时间:2023 年12 月 8 日(周五) 15:00 地点:华中科技大学大学生活动中心B座303 嘉宾教授:张锐 教授 项目旨在培养科学或工程背景的学员从数据中提取信息的数据建模能力,训练其拥有优秀的解难和逻辑思考与分…

3.OpenResty系列之Nginx反向代理

1. Nginx简介 Nginx (engine x) 是一款轻量级的 Web 服务器 、反向代理服务器及电子邮件(IMAP/POP3)代理服务器 什么是反向代理? 反向代理(Reverse Proxy)方式是指以代理服务器来接受 internet 上的连接请求&#x…

金山办公前端二面

1. react 和 vue的区别 还有jquery? (1) jquery 和 vue、react 的区别: vue 和 react : 数据和视图分离 以数据驱动视图,只关心数据变化 dom 操作被封装(数据驱动) jquery:依靠 do…

python环境的搭建+pytharm安装教程

一、Anaconda安装 1、去官网下载anaconda >百度搜索anaconda按回车键 >找到官网地址进去(注意看网址) >下载位置 2、安装anaconda 具体就安装步骤就不演示了(写文章时已经安装好了) 二、pycharm安装 1、去官网下载py…

Redis 基本命令—— 超详细操作演示!!!

内存数据库 Redis7—— Redis 基本命令 三、Redis 基本命令(下)3.8 benchmark 测试工具3.9 简单动态字符串SDS3.10 集合的底层实现原理3.11 BitMap 操作命令3.12 HyperLogLog 操作命令3.13 Geospatial 操作命令3.14 发布/订阅命令3.15 Redis 事务 四、Re…

【C语言加油站】函数栈帧的创建与销毁 #保姆级讲解

函数栈帧的创建与销毁 导言一、计算机硬件1.冯•诺依曼机基本思想2.冯•诺依曼机的特点:3.存储器3.1 分类3.2 内存的工作方式3.3 内存的组成 4.寄存器4.1 基本含义4.2 寄存器的功能4.3 工作原理4.4 分类4.4.1 通用寄存器组AX(AH、AL):累加器BX(BH、BL)&a…

第七节HarmonyOS UIAbility生命周期以及启动模式

一、UIAbility生命周期 为了实现多设备形态上的裁剪和多窗口的可扩展性,系统对组件管理和窗口管理进行了解耦。UIAbility的生命周期包括Create、Foreground、Background、Destroy四个状态,WindowStageCreate和WindowStageDestroy为窗口管理器&#xff08…

堆结构的应用:随时取得数据流中的中位数

大根堆和小根堆配合 实现 第一个数字直接入大根堆 对于后面的数字&#xff0c; 如果数字 < 大根堆的堆顶&#xff0c;这个数字入大根堆 否则入小根堆 在数字入堆的同时&#xff0c;进行大根堆与小根堆的大小的比较&#xff0c;一旦它们两个的大小之差 2&#xff0c;较大…