首页 > 其他分享 >Go - Web Application 10

Go - Web Application 10

时间:2024-09-07 16:37:37浏览次数:14  
标签:Web http TestHumanDate 10 0.00 PASS test time Go

Creating a unit test

In Go, it’s standard practice to write your tests in *_test.go files which live directly alongside the code that you’re testing. So, in this case, the first thing that we’re going to do is create a new cmd/web/template_test.go file to hold the test.

And then we can create a new unit test for the humanDate function like so:

package main

import (
    "testing"
    "time"
)

func TestHumanDate(t *testing.T) {
    // Initialize a new time.Time object and pass it to the humanDate function.
    tm := time.Date(2024, 3, 17, 10, 15, 0, 0, time.UTC)
    hd := humanDate(tm)

    // Check that the output from the humanDate function is in the format we expect. If it isn't 
    // what we expect, use the t.Errorf() function to indicate that the test has failed and log 
    // the expected and actual values.
    if hd != "17 Mar 2024 at 10:15" {
        t.Errorf("got %q; want %q", hd, "17 Mar 2024 at 10:15")
    }
}

 

zzh@ZZHPC:/zdata/Github/snippetbox$ go test -v ./cmd/web
=== RUN   TestHumanDate
--- PASS: TestHumanDate (0.00s)
PASS
ok      snippetbox/cmd/web      0.002s

 

Table-driven tests

Let’s now expand our TestHumanDate() function to cover some additional test cases. Specifically, we’re going to update it to also check that:

1. If the input to humanDate() is the zero time, then it returns the empty string "" .
2. The output from the humanDate() function always uses the UTC time zone.

In Go, an idiomatic way to run multiple test cases is to use table-driven tests.

Essentially, the idea behind table-driven tests is to create a ‘table’ of test cases containing the inputs and expected outputs, and to then loop over these, running each test case in a sub-test. There are a few ways you could set this up, but a common approach is to define your test cases in an slice of anonymous structs.

package main

import (
    "testing"
    "time"
)

func TestHumanDate(t *testing.T) {
    // Create a slice of anonymous structs containing the test case name, input to our humanDate()
    // function, and expected output.
    tests := []struct{
        name   string
        input  time.Time
        expected string
    }{
        {
            name: "UTC",
            input: time.Date(2024, 3, 17, 10, 15, 0, 0, time.UTC),
            expected: "17 Mar 2024 at 10:15",
        },
        {
            name: "Empty",
            input: time.Time{},
            expected: "",
        },
        {
            name: "CET",
            input: time.Date(2024, 3, 17, 10, 15, 0, 0, time.FixedZone("CET", 1*60*60)),
            expected: "17 Mar 2024 at 09:15",
        },
    }

    // Loop over the test cases.
    for _, tc := range tests {
        // Use the t.Run() function to run a sub-test for each test case. The first parameter to 
        // this is the name of the test (which is used to identify the sub-test in any log output) 
        // and the second parameter is an anonymous function containing the actual test for each 
        // case.
        t.Run(tc.name, func(t *testing.T) {
            hd := humanDate(tc.input)

            if hd != tc.expect {
                t.Errorf("got %q; expect %q", hd, tc.expected)
            }
        })
    }
}

 

zzh@ZZHPC:/zdata/Github/snippetbox$ go test -v ./cmd/web
=== RUN   TestHumanDate
=== RUN   TestHumanDate/UTC
=== RUN   TestHumanDate/Empty
    template_test.go:43: got "01 Jan 0001 at 00:00"; expect ""
=== RUN   TestHumanDate/CET
    template_test.go:43: got "17 Mar 2024 at 10:15"; expect "17 Mar 2024 at 09:15"
--- FAIL: TestHumanDate (0.00s)
    --- PASS: TestHumanDate/UTC (0.00s)
    --- FAIL: TestHumanDate/Empty (0.00s)
    --- FAIL: TestHumanDate/CET (0.00s)
FAIL
FAIL    snippetbox/cmd/web      0.002s
FAIL

So here we can see the individual output for each of our sub-tests. As you might have guessed, our first test case passed but the Empty and CET tests both failed. Notice how — for the failed test cases — we get the relevant failure message and filename and line number in the output?

Let’s head back to our humanDate() function and update it to fix these two problems:

func humanDate(t time.Time) string {
    if t.IsZero() {
        return ""
    }

    // Convert the time to UTC before formatting it.
    return t.UTC().Format("02 Jan 2006 at 15:04")
}

 

zzh@ZZHPC:/zdata/Github/snippetbox$ go test -v ./cmd/web
=== RUN   TestHumanDate
=== RUN   TestHumanDate/UTC
=== RUN   TestHumanDate/Empty
=== RUN   TestHumanDate/CET
--- PASS: TestHumanDate (0.00s)
    --- PASS: TestHumanDate/UTC (0.00s)
    --- PASS: TestHumanDate/Empty (0.00s)
    --- PASS: TestHumanDate/CET (0.00s)
PASS
ok      snippetbox/cmd/web      0.002s

 

Helpers for test assertions

As I mentioned briefly earlier, over the next few chapters we’ll be writing a lot of test assertions that are a variation of this pattern:

if actualValue != expectedValue {
    t.Errorf("got %v; want %v", actualValue, expectedValue)
}

Let’s quickly abstract this code into a helper function.

If you’re following along, go ahead and create a new internal/assert package.

Create a new file named assert.go. And then add the following code:

package assert

import "testing"

func Equal[T comparable](t *testing.T, actual, expected T) {
    t.Helper()

    if actual != expected {
        t.Errorf("got: %v; expect: %v", actual, expected)
    }
}

Note: The t.Helper() function that we’re using in the code above indicates to the Go test runner that our Equal() function is a test helper. This means that when t.Errorf() is called from our Equal() function, the Go test runner will report the filename and line number of the code which called our Equal() function in the output.

With that in place, we can simplify our TestHumanDate() test like so:

package main

import (
    "snippetbox/internal/assert"
    "testing"
    "time"
)

func TestHumanDate(t *testing.T) {
    tests := []struct {
        name     string
        input    time.Time
        expected string
    }{
        {
            name:     "UTC",
            input:    time.Date(2024, 3, 17, 10, 15, 0, 0, time.UTC),
            expected: "17 Mar 2024 at 10:15",
        },
        {
            name:     "Empty",
            input:    time.Time{},
            expected: "",
        },
        {
            name:     "CET",
            input:    time.Date(2024, 3, 17, 10, 15, 0, 0, time.FixedZone("CET", 1*60*60)),
            expected: "17 Mar 2024 at 09:15",
        },
    }

    for _, tc := range tests {
        t.Run(tc.name, func(t *testing.T) {
            hd := humanDate(tc.input)

            // Use the new assert.Equal() helper to compare the actual and expected values.
            assert.Equal(t, hd, tc.expected)
        })
    }
}

 

Sub-tests without a table of test cases

It’s important to point out that you don’t need to use sub-tests in conjunction with table-driven tests (like we have done so far). It’s perfectly valid to execute sub-tests by calling t.Run() consecutively in your test functions, similar to this:

func TestExample(t *testing.T) {
    t.Run("Example sub-test 1", func(t *testing.T) {
        // Do a test.
    })

    t.Run("Example sub-test 2", func(t *testing.T) {
        // Do another test.
    })

    t.Run("Example sub-test 3", func(t *testing.T) {
        // And another...
    })
}

 

Testing HTTP handlers and middleware

Let’s move on and discuss some specific techniques for unit testing your HTTP handlers.

All the handlers that we’ve written for this project so far are a bit complex to test, and to introduce things I’d prefer to start off with something more simple.

So, if you’re following along, head over to your handlers.go file and create a new ping handler function which returns a 200 OK status code and an "OK" response body. It’s the type of handler that you might want to implement for status-checking or uptime monitoring of your server.

func ping(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("OK"))
}

 

Recording responses

Go provides a bunch of useful tools in the net/http/httptest package for helping to test your HTTP handlers.

One of these tools is the httptest.ResponseRecorder type. This is essentially an implementation of http.ResponseWriter which records the response status code, headers and body instead of actually writing them to a HTTP connection.

So an easy way to unit test your handlers is to create a new httptest.ResponseRecorder , pass it to the handler function, and then examine it again after the handler returns.

First, follow the Go conventions and create a new handlers_test.go file to hold the test.

package main

import (
    "bytes"
    "io"
    "net/http"
    "net/http/httptest"
    "snippetbox/internal/assert"
    "testing"
)

func TestPing(t *testing.T) {
    // Initialize a new httptest.ResponseRecorder.
    rr := httptest.NewRecorder()

    // Initialize a new dummy http.Request.
    r, err := http.NewRequest(http.MethodGet, "/", nil)
    if err != nil {
        t.Fatal(err)
    }

    // Call the ping handler function, passing in the httptest.ResponseRecorder and http.Request.
    ping(rr, r)

    // Call the Result() method on the http.ResponseRecorder to get the http.Response generated 
    // by the ping handler.
    res := rr.Result()

    // Check that the status code written by the ping handler was 200.
    assert.Equal(t, res.StatusCode, http.StatusOK)

    // And we can check that the response body written by the ping handler equals "OK".
    defer res.Body.Close()
    body, err := io.ReadAll(res.Body)
    if err != nil {
        t.Fatal(err)
    }
    body = bytes.TrimSpace(body)

    assert.Equal(t, string(body), "OK")
}

Note: In the code above we use the t.Fatal() function in a couple of places to handle situations where there is an unexpected error in our test code. When called, t.Fatal() will mark the test as failed, log the error, and then completely stop execution of the current test (or sub-test). Typically you should call t.Fatal() in situations where it doesn’t make sense to continue the current test — such as an error during a setup step, or where an unexpected error from a Go standard library function means you can’t proceed with the test.

 

zzh@ZZHPC:/zdata/Github/snippetbox$ go test -v ./cmd/web
=== RUN   TestPing
--- PASS: TestPing (0.00s)
=== RUN   TestHumanDate
=== RUN   TestHumanDate/UTC
=== RUN   TestHumanDate/Empty
=== RUN   TestHumanDate/CET
--- PASS: TestHumanDate (0.00s)
    --- PASS: TestHumanDate/UTC (0.00s)
    --- PASS: TestHumanDate/Empty (0.00s)
    --- PASS: TestHumanDate/CET (0.00s)
PASS
ok      snippetbox/cmd/web      0.002s

 

Testing middleware

First you’ll need to create a cmd/web/middleware_test.go file to hold the test.

package main

import (
    "bytes"
    "io"
    "net/http"
    "net/http/httptest"
    "snippetbox/internal/assert"
    "testing"
)

func TestCommonHeaders(t *testing.T) {
    rr := httptest.NewRecorder()

    r, err := http.NewRequest(http.MethodGet, "/", nil)
    if err != nil {
        t.Fatal(err)
    }

    // Create a mock HTTP handler that we can pass to our commonHeaders middleware, chich writes
    // a 200 status code and "OK" response body.
    next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("OK"))
    })

    // Pass the mock HTTP handler to our commonHeaders middleware. Because commonHeaders *returns*
    // an http.Handler we can call its ServeHTTP() method, passing in the http.ResponseRecorder
    // and dummy http.Request to execute it.
    commonHeaders(next).ServeHTTP(rr, r)

    // Call the Result() method on the http.ResponseRecorder to get the results of the test.
    res := rr.Result()

    // Check that the middleware has correctly set the Content-Security-Policy header on the
    // response.
    expectedValue := "default-src 'self'; style-src 'self' fonts.googleapis.com; font-src fonts.gstatic.com"
    assert.Equal(t, res.Header.Get("Content-Security-Policy"), expectedValue)

    // Check that the middleware has correctly set the Referrer-Policy header on the response.
    expectedValue = "origin-when-cross-origin"
    assert.Equal(t, res.Header.Get("Referrer-Policy"), expectedValue)

    // Check that the middleware has correctly set the X-Content-Type-Options header on the 
    // response.
    expectedValue = "nosniff"
    assert.Equal(t, res.Header.Get("X-Content-Type-Options"), expectedValue)

    // Check that the middleware has correctly set the X-Frame-Options header on the response.
    expectedValue = "deny"
    assert.Equal(t, res.Header.Get("X-Frame-Options"), expectedValue)

    // Check that the middleware has correctly set the X-XSS-Protection header on the response.
    expectedValue = "0"
    assert.Equal(t, res.Header.Get("X-XSS-Protection"), expectedValue)

    // Check that the middleware has correctly set the Server header on the response.
    expectedValue = "Go"
    assert.Equal(t, res.Header.Get("Server"), expectedValue)

    // Check that the middleware has correctly called the next handler in line and the response 
    // status code and body are expected.
    assert.Equal(t, res.StatusCode, http.StatusOK)

    defer res.Body.Close()
    body, err := io.ReadAll(res.Body)
    if err != nil {
        t.Fatal(err)
    }
    body = bytes.TrimSpace(body)

    assert.Equal(t, string(body), "OK")
}

 

zzh@ZZHPC:/zdata/Github/snippetbox$ go test -v ./cmd/web
=== RUN   TestPing
--- PASS: TestPing (0.00s)
=== RUN   TestCommonHeaders
--- PASS: TestCommonHeaders (0.00s)
=== RUN   TestHumanDate
=== RUN   TestHumanDate/UTC
=== RUN   TestHumanDate/Empty
=== RUN   TestHumanDate/CET
--- PASS: TestHumanDate (0.00s)
    --- PASS: TestHumanDate/UTC (0.00s)
    --- PASS: TestHumanDate/Empty (0.00s)
    --- PASS: TestHumanDate/CET (0.00s)
PASS
ok      snippetbox/cmd/web      0.002s

So, in summary, a quick and easy way to unit test your HTTP handlers and middleware is to simply call them using the httptest.ResponseRecorder type. You can then examine the status code, headers and response body of the recorded response to make sure that they are working as expected.

 

标签:Web,http,TestHumanDate,10,0.00,PASS,test,time,Go
From: https://www.cnblogs.com/zhangzhihui/p/18401846

相关文章

  • 编程技术开发105本经典书籍推荐分享
    最近整理了好多的技术书籍,对于提高自己能力来说还是很有用的,当然要有选择的看,不然估计退休了都不一定看得完,分享给需要的同学。编程技术开发105本经典书籍推荐:https://zhangfeidezhu.com/?p=753分享截图......
  • 分享10个免费的Python代码仓库,轻松实现办公自动化!
    为了帮助大家更好地利用Python实现自动化办公,我们精心挑选了10个免费的Python代码仓库。这些仓库不仅包含了实用的脚本和示例,还涵盖了从基础到进阶的各种自动化任务解决方案。无论你是Python编程的初学者,还是希望提升工作效率的职场人士,都能在这些仓库中找到适合自己的资......
  • 基于django+vue智能会议管理系统【开题报告+程序+论文】-计算机毕设
    本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表开题报告内容研究背景随着企业规模的不断扩大和远程办公模式的兴起,会议管理成为了企业日常运营中不可或缺的一环。传统的手工或基于简单电子表格的会议管理方式......
  • 基于django+vue智能充电桩【开题报告+程序+论文】-计算机毕设
    本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表开题报告内容研究背景随着新能源汽车产业的迅猛发展,电动汽车作为绿色出行的重要载体,其普及率日益提升。然而,电动汽车的广泛应用面临着一个关键挑战——充电基础......
  • 基于django+vue智慧阅读平台【开题报告+程序+论文】-计算机毕设
    本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表开题报告内容研究背景在信息爆炸的时代,阅读已成为获取知识、提升自我能力的重要途径。然而,面对海量的阅读资源和碎片化的信息环境,如何高效地选择与阅读成为了一......
  • Python毕业设计基于Django的动漫漫画手办周边商城
    文末获取资源,收藏关注不迷路文章目录一、项目介绍二、主要使用技术三、研究内容四、核心代码五、文章目录一、项目介绍动漫周边商城分为二个模块,分别是管理员功能模块和用户功能模块。管理员功能模块包括:文章资讯、文章类型、动漫活动、动漫商品功能,用户功能模块......
  • AI跟踪报道第55期-新加坡内哥谈技术-本周AI新闻: GPT NEXT (x100倍)即将在2024推出
      每周跟踪AI热点新闻动向和震撼发展想要探索生成式人工智能的前沿进展吗?订阅我们的简报,深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同,从行业内部的深度分析和实用指南中受益。不要错过这个机会,成为AI领域的领跑者。点击订阅,与未来同行!订阅:https://......
  • 软件测试中的web自动化测试
    在日志系统中,通常包含几个核心组件,这些组件之间通过特定的接口和机制相互协作,以实现日志的收集、处理、格式化和输出。以下是日志系统中常见的几个组件及其之间的联系:日志器(Logger):日志器是日志系统的入口点,用于接收应用程序或系统生成的日志消息。它通常提供不同级别的日......
  • STM32系列修改外部晶振以及代码的修改(f103、f105为例)
    此文章为引用正点原子详细讲解刚刚接触STM32的时候,用的都是8M晶振。比如你想更换到为外部晶振为12M,但是主频仍想用72M的。该如何设置?或者想倍频到更高的主频该怎么修改?例子就直接直接拿<正点原子>的例子吧!属性原来现在外部晶振8M12M倍频96主频72M72M想从原来的8......
  • Go-函数的那些事儿
    Go-函数的那些事儿定义函数是结构化编程的最小模块单元。它将复杂的算法过程分解为若干较小任务,隐藏相关细节,使得程序结构更加清晰,易于维护。函数被设计成相对独立,通过接收输入参数完成一段算法指令,输出或存储相关结果。因此,函数还是代码复用和测试的基本单元。Go函数借......