单元测试-httptest

项目demo地址:go-test

本文主要针对单元测试工具,其他工具请看专栏内其它博客。

httptest

**介绍:**Go 内置标准库net/http/httptest,核心用途用于测试net/http构建的HTTP服务(如API接口、Web服务等),它可以模拟HTTP请求发送和HTTP响应的接收,无需启动真实的HTTP服务器即可完成接口测试,极大提升了测试的便捷性和执行效率

优点:

  • 无需启动真实服务器:无需调用 http.ListenAndServe 启动端口监听,直接测试 HTTP 处理器(Handler/HandlerFunc),测试执行更高效。
  • 脱离网络依赖:模拟 HTTP 请求与响应的完整生命周期,不受网络波动、端口占用等外部因素影响,测试结果稳定可复现。
  • 精准捕获响应细节:可完整获取响应状态码、响应头、响应体等所有信息,便于精准断言验证。
  • 支持两种核心测试场景:
    • 测试 HTTP 处理器(直接调用 ServeHTTP,最常用)
    • 启动临时测试服务器(模拟真实服务端,用于客户端测试或集成测试)

1.安装

内置工具可以直接使用

2.使用示例

相关代码在gitee代码仓库的示例代码中,仓库地址请看博客开头

blog.go

go 复制代码
package httptest_demo

import (
	"errors"
	"fmt"
	"io"
	"net/http"
)

func SearchHttp(targetURL string) (interface{}, error) {
	resp, err := http.Get(targetURL)
	if err != nil {
		errMsg := fmt.Sprintf("发送 GET 请求失败:%v", err)
		fmt.Println(errMsg)
		return nil, errors.New(errMsg)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		errMsg := fmt.Sprintf("请求失败,状态码:%d,状态信息:%s", resp.StatusCode, resp.Status)
		fmt.Println(errMsg)
		return nil, errors.New(errMsg)
	}

	bodyBytes, _ := io.ReadAll(resp.Body)
	return string(bodyBytes), nil
}

blog_test.go

go 复制代码
package httptest_demo

import (
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
)

func TestSearchHttp(t *testing.T) {
	// --------------- 1. 定义所有测试场景的表驱动用例(循环遍历执行) ---------------
	testCases := []struct {
		name             string        // 用例名称
		prepareFunc      func() string // 前置准备:创建模拟服务器/构造URL,返回待请求的URL
		expectedErr      bool          // 是否预期返回错误
		errContains      string        // 预期错误信息包含的关键字(非空则验证)
		expectedNonEmpty bool          // 正常场景下,是否预期返回非空字符串
	}{
		// 场景1:正常请求(200 OK,响应体正常)
		{
			name: "正常请求-状态码200",
			prepareFunc: func() string {
				// 启动模拟HTTP服务器,返回200和测试响应体
				mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
					mockBody := "<!DOCTYPE html><html><title>百度一下</title></html>"
					w.WriteHeader(http.StatusOK)
					_, _ = w.Write([]byte(mockBody))
				}))
				// 关键:将mockServer放入测试上下文,确保后续能关闭(避免资源泄露)
				t.Cleanup(func() { mockServer.Close() })
				return mockServer.URL
			},
			expectedErr:      false,
			errContains:      "",
			expectedNonEmpty: true,
		},
		// 场景2:请求失败(无效URL,模拟网络异常)
		{
			name: "异常场景-无效URL请求失败",
			prepareFunc: func() string {
				// 返回一个无效的URL,触发http.Get请求失败
				return "http://invalid-xxx-url-12345/"
			},
			expectedErr:      true,
			errContains:      "发送 GET 请求失败",
			expectedNonEmpty: false,
		},
		// 场景3:状态码非200(模拟404 Not Found)
		{
			name: "异常场景-状态码404",
			prepareFunc: func() string {
				// 启动模拟HTTP服务器,返回404状态码
				mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
					w.WriteHeader(http.StatusNotFound)
					_, _ = w.Write([]byte("页面不存在"))
				}))
				t.Cleanup(func() { mockServer.Close() })
				return mockServer.URL
			},
			expectedErr:      true,
			errContains:      "请求失败,状态码:404",
			expectedNonEmpty: false,
		},
		// 场景4:状态码非200(模拟500服务器内部错误)
		{
			name: "异常场景-状态码500",
			prepareFunc: func() string {
				// 启动模拟HTTP服务器,返回500状态码
				mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
					w.WriteHeader(http.StatusInternalServerError)
					_, _ = w.Write([]byte("服务器内部错误"))
				}))
				t.Cleanup(func() { mockServer.Close() })
				return mockServer.URL
			},
			expectedErr:      true,
			errContains:      "请求失败,状态码:500",
			expectedNonEmpty: false,
		},
	}

	// --------------- 2. 循环遍历所有测试用例,统一执行验证 ---------------
	for _, tc := range testCases {
		// 循环内使用t.Run创建子用例(便于精准定位失败场景,不影响其他用例)
		t.Run(tc.name, func(t *testing.T) {
			// 步骤1:执行前置准备,获取待请求的URL
			targetURL := tc.prepareFunc()

			// 步骤2:调用被测函数
			result, err := SearchHttp(targetURL)

			// 步骤3:统一断言验证
			// 3.1 验证错误是否符合预期
			if (err != nil) != tc.expectedErr {
				t.Fatalf("错误预期不符:预期是否错误[%t],实际是否错误[%t],错误信息[%v]",
					tc.expectedErr, err != nil, err)
			}

			// 3.2 若预期错误,验证错误信息是否包含指定关键字
			if tc.expectedErr && tc.errContains != "" {
				if !strings.Contains(err.Error(), tc.errContains) {
					t.Errorf("错误信息不符:预期包含[%s],实际错误[%v]", tc.errContains, err)
				}
			}

			// 3.3 验证返回值是否符合预期
			if tc.expectedErr {
				// 异常场景:预期返回nil
				if result != nil {
					t.Errorf("异常场景预期返回nil,实际返回[%v],类型[%T]", result, result)
				}
			} else {
				// 正常场景:验证返回值是string类型,且非空(若预期非空)
				resultStr, ok := result.(string)
				if !ok {
					t.Fatalf("正常场景预期返回string类型,实际返回[%T]", result)
				}
				if tc.expectedNonEmpty && len(resultStr) == 0 {
					t.Error("正常场景预期返回非空字符串,实际返回空字符串")
				}
			}
		})
	}
}

命令行执行命令

go test -cover

结果:

powershell 复制代码
PS D:\wyl\workspace\go\tracer\logic\httptest_demo> go test -cover         
发送 GET 请求失败:Get "http://invalid-xxx-url-12345/": dial tcp: lookup invalid-xxx-url-12345: no such host
请求失败,状态码:404,状态信息:404 Not Found
请求失败,状态码:500,状态信息:500 Internal Server Error
PASS
coverage: 100.0% of statements
ok      tracer/logic/httptest_demo      1.683s
相关推荐
liangbm313 小时前
AI-ViewNote:把网课和会议视频自动卷成结构化笔记
ai·typescript·go·软件构建·开源软件·react·桌面软件
我叫黑大帅16 小时前
Gin 实战入门:从环境搭建到企业级常用特性全解析
后端·面试·go
我叫黑大帅16 小时前
Gin 日志体系详解
后端·面试·go
华科易迅16 小时前
Spring 单元测试
java·spring·单元测试
F1FJJ17 小时前
Shield CLI v0.3.3 新增 PostgreSQL 插件:浏览器里管理 PG 数据库
网络·网络协议·docker·postgresql·容器·go
小陈工17 小时前
Python测试实战:单元测试、集成测试与性能测试全解析
大数据·网络·数据库·人工智能·python·单元测试·集成测试
mCell1 天前
【万字长文】从 AI SDK 到 mini-opencode:一次很巧的 Go Agent 架构实践
架构·go·agent
jump_jump1 天前
深入理解 Go Context:从原理到实战(基于 Go 1.26)
go·源码
哈里谢顿1 天前
golang目前遇到的面试题
go
哈里谢顿1 天前
python与golang性能差异对比
go