104. Go单测系列4---编写可测试的代码

文章目录

  • 一、剔除干扰因素
  • 二、接口抽象进行解耦
  • 三、依赖注入代替隐式依赖
  • 四、SOLID原则

本文是Go单测系列的最后一篇,在这一篇中我们不再介绍编写单元测试的工具而是专注于如何编写可测试的代码。

编写可测试的代码可能比编写单元测试本身更加重要,可测试的代码简单来说就是指我们可以很容易的为其编写单元测试代码。编写单元测试的过程也是一个不断思考的过程,思考我们的代码是否正确的被设计和实现。

接下来,我们将通过几个简单示例来介绍如何编写可测试的代码。

一、剔除干扰因素

假设我们现在有一个根据时间判断报警信息发送速率的模块,白天工作时间允许大量发送报警信息,而晚上则减小发送速率,凌晨不允许发送报警短信。

// judgeRate 报警速率决策函数
func judgeRate() int {
	now := time.Now()
	switch hour := now.Hour(); {
	case hour >= 8 && hour < 20:
		return 10
	case hour >= 20 && hour <= 23:
		return 1
	}
	return -1
}

这个函数内部使用了time.Now()来获取系统的当前时间作为判断的依据,看起来很合理。

但是这个函数现在隐式包含了一个不确定因素——时间。在不同的时刻我们调用这个函数都可能会得到不一样的结果。想象一下,我们该如何为这个函数编写单元测试呢?

如果不修改系统时间,那么我们就无法为这个函数编写单元测试,这个函数成了“不可测试的代码”(当然可以使用打桩工具对time.Now进行打桩,但那不是本文要强调的重点)。

接下来我们该如何改造它?

我们通过为函数传参数的方式传入需要判断的时刻,具体实现如下。

// judgeRateByTime 报警速率决策函数
func judgeRateByTime(now time.Time) int {
	switch hour := now.Hour(); {
	case hour >= 8 && hour < 20:
		return 10
	case hour >= 20 && hour <= 23:
		return 1
	}
	return -1
}

这样我们不仅解决了函数与系统时间的紧耦合,而且还扩展了函数的功能,现在我们可以根据需要获取任意时刻的速率值。为改造后的judgeRateByTime编写单元测试也更方便了。

func Test_judgeRateByTime(t *testing.T) {
	tests := []struct {
		name string
		arg  time.Time
		want int
	}{
		{
			name: "工作时间",
			arg:  time.Date(2022, 2, 18, 11, 22, 33, 0, time.UTC),
			want: 10,
		},
		{
			name: "晚上",
			arg:  time.Date(2022, 2, 18, 22, 22, 33, 0, time.UTC),
			want: 1,
		},
		{
			name: "凌晨",
			arg:  time.Date(2022, 2, 18, 2, 22, 33, 0, time.UTC),
			want: -1,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := judgeRateByTime(tt.arg); got != tt.want {
				t.Errorf("judgeRateByTime() = %v, want %v", got, tt.want)
			}
		})
	}
}

二、接口抽象进行解耦

同样是函数中隐式依赖的问题,假设我们实现了一个获取店铺客单价的需求,它完成的功能就像下面的示例函数。

// GetAveragePricePerStore 每家店的人均价
func GetAveragePricePerStore(storeName string) (int64, error) {
	res, err := http.Get("https://lym.com/api/orders?storeName=" + storeName)
	if err != nil {
		return 0, err
	}
	defer res.Body.Close()

	var orders []Order
	if err := json.NewDecoder(res.Body).Decode(&orders); err != nil {
		return 0, err
	}

	if len(orders) == 0 {
		return 0, nil
	}

	var (
		p int64
		n int64
	)

	for _, order := range orders {
		p += order.Price
		n += order.Num
	}

	return p / n, nil
}

在之前的章节中我们介绍了如何为上面的代码编写单元测试,但是我们如何避免每次单元测试时都发起真实的HTTP请求呢?亦或者后续我们改变了获取数据的方式(直接读取缓存或改为RPC调用)这个函数该怎么兼容呢?

我们将函数中获取数据的部分抽象为接口类型来优化我们的程序,使其支持模块化的数据源配置。

// OrderInfoGetter 订单信息提供者
type OrderInfoGetter interface {
	GetOrders(string) ([]Order, error)
}

然后定义一个API类型,它拥有一个通过HTTP请求获取订单数据的GetOrders方法,正好实现OrderInfoGetter接口。

// HttpApi HTTP API类型
type HttpApi struct{}

// GetOrders 通过HTTP请求获取订单数据的方法
func (a HttpApi) GetOrders(storeName string) ([]Order, error) {
	res, err := http.Get("https://lym.com/api/orders?storeName=" + storeName)
	if err != nil {
		return nil, err
	}
	defer res.Body.Close()

	var orders []Order
	if err := json.NewDecoder(res.Body).Decode(&orders); err != nil {
		return nil, err
	}
	return orders, nil
}
将原来的 GetAveragePricePerStore 函数修改为以下实现。

// GetAveragePricePerStore 每家店的人均价
func GetAveragePricePerStore(getter OrderInfoGetter, storeName string) (int64, error) {
	orders, err := getter.GetOrders(storeName)
	if err != nil {
		return 0, err
	}

	if len(orders) == 0 {
		return 0, nil
	}

	var (
		p int64
		n int64
	)

	for _, order := range orders {
		p += order.Price
		n += order.Num
	}

	return p / n, nil
}

经过这番改动之后,我们的代码就能很容易地写出单元测试代码。例如,对于不方便直接请求的HTTP API, 我们就可以进行 mock 测试。

// Mock 一个mock类型
type Mock struct{}

// GetOrders mock获取订单数据的方法
func (m Mock) GetOrders(string) ([]Order, error) {
	return []Order{
		{
			Price: 20300,
			Num:   2,
		},
		{
			Price: 642,
			Num:   5,
		},
	}, nil
}

func TestGetAveragePricePerStore(t *testing.T) {
	type args struct {
		getter    OrderInfoGetter
		storeName string
	}
	tests := []struct {
		name    string
		args    args
		want    int64
		wantErr bool
	}{
		{
			name: "mock test",
			args: args{
				getter:    Mock{},
				storeName: "mock",
			},
			want:    12062,
			wantErr: false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := GetAveragePricePerStore(tt.args.getter, tt.args.storeName)
			if (err != nil) != tt.wantErr {
				t.Errorf("GetAveragePricePerStore() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if got != tt.want {
				t.Errorf("GetAveragePricePerStore() got = %v, want %v", got, tt.want)
			}
		})
	}
}

三、依赖注入代替隐式依赖

我们可能经常会看到类似下面的代码,在应用程序中使用全局变量的方式引入日志库或数据库连接实例等。

package main

import (
	"github.com/sirupsen/logrus"
)

var log = logrus.New()

type App struct{}

func (a *App) Start() {
	log.Info("app start ...")
}

func main() {
	app := &App{}
	app.Start()
}

上面的代码中 App 中通过引用全局变量的方式将依赖项硬编码到代码中,这种情况下我们在编写单元测试时如何 mock log 变量呢?

此外这样的代码还存在一个更严重的问题——它与具体的日志库程序强耦合。当我们后续因为某些原因需要更换另一个日志库时,我们该如何修改代码呢?

我们应该将依赖项解耦出来,并且将依赖注入到我们的App实例中,而不是在其内部隐式调用全局变量。

type App struct {
	Logger
}

func (a *App) Start() {
	a.Logger.Info("app start ...")
	// ...
}

// NewApp 构造函数,将依赖项注入
func NewApp(lg Logger) *App {
	return &App{
		Logger: lg, // 使用传入的依赖项完成初始化
	}
}

上面的代码就很容易 mock log实例,完成单元测试。

依赖注入就是指在创建组件(Go 中的 struct)的时候接收它的依赖项,而不是它的初始化代码中引用外部或自行创建依赖项。

// Config 配置项结构体
type Config struct {
	// ...
}

// LoadConfFromFile 从配置文件中加载配置
func LoadConfFromFile(filename string) *Config {
	return &Config{}
}

// Server server 程序
type Server struct {
	Config *Config
}

// NewServer Server 构造函数
func NewServer() *Server {
	return &Server{
    // 隐式创建依赖项
		Config: LoadConfFromFile("./config.toml"),
	}
}

上面的代码片段中就通过在构造函数中隐式创建依赖项,这样的代码强耦合、不易扩展,也不容易编写单元测试。我们完全可以通过使用依赖注入的方式,将构造函数中的依赖作为参数传递给构造函数。

// NewServer Server 构造函数
func NewServer(conf *Config) *Server {
	return &Server{
		// 隐式创建依赖项
		Config: conf,
	}
}

不要隐式引用外部依赖(全局变量、隐式输入等),而是通过依赖注入的方式引入依赖。经过这样的修改之后,构造函数NewServer 的依赖项就很清晰,同时也方便我们编写 mock 测试代码。

使用依赖注入的方式能够让我们的代码看起来更清晰,但是过多的构造函数也会让主函数的代码迅速膨胀,好在Go 语言提供了一些依赖注入工具(例如 [wire](https://github.com/google/wire) ,可以帮助我们更好的管理依赖注入的代码。

四、SOLID原则

最后我们补充一个程序设计的SOLID原则,我们在程序设计时践行以下几个原则会帮助我们写出可测试的代码。

在这里插入图片描述

至此,Go 语言单元测试系列到此更新完。

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

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

相关文章

03-自媒体文章发布

自媒体文章发布 1)自媒体前后端搭建 1.1)后台搭建 ①&#xff1a;资料中找到heima-leadnews-wemedia.zip解压 拷贝到heima-leadnews-service工程下&#xff0c;并指定子模块 执行leadnews-wemedia.sql脚本 添加对应的nacos配置 spring:datasource:driver-class-name: com…

五、OpenAI实战之Assistants API

在8线小城的革委会办公室里&#xff0c;黑8和革委会主任的对话再次展开。 黑8&#xff1a;主任&#xff0c;您知道吗&#xff1f;除了OpenAI API&#xff0c;现在还有一项新的技术叫做Assistants API&#xff0c;它可以帮助我们更好地进行对话和沟通。 主任&#xff1a;Assis…

如何保证消息的可靠传输

数据的丢失问题&#xff0c;可能出现在生产者、MQ、消费者中 生产者丢失&#xff1a; 生产者将数据发送到 RabbitMQ 的时候&#xff0c;可能数据就在半路给搞丢了&#xff0c;因为网络问题啥的&#xff0c;都有可能。此时可以选择用 RabbitMQ 提供的事务功能&#xff0c;就是生…

从0到1快速搭建一个jeecg 企业级应用管理后台

一. 基本介绍 官网地址&#xff1a;https://jeecg.com/ JeecgBoot 是一款企业级的低代码平台&#xff01;前后端分离架构 SpringBoot2.x&#xff0c;SpringCloud&#xff0c;Ant Design&Vue3&#xff0c;Mybatis-plus&#xff0c;Shiro&#xff0c;JWT 支持微服务。强大的…

shell详解

系列文章目录 shell脚本基础知识3 shell脚本基础知识3 系列文章目录一、什么是shell二、shell脚本意义三、如何创建shell脚本&#xff08;幻数&#xff09;四、自动生成脚本头信息五、shell脚本运行方式5.1手动在环境中开启指定解释器&#xff0c;不会开启脚本指定的幻数5.2不…

基于 Win Server 2008 复现 IPC$ 漏洞

写在前面 本篇博客演示了使用 winXP&#xff08;配合部分 win10 的命令&#xff09;对 win server 2008 的 IPC$ 漏洞进行内网渗透&#xff0c;原本的实验是要求使用 win server 2003&#xff0c;使用 win server 2003 可以规避掉很多下面存在的问题&#xff0c;建议大家使用 …

【论文阅读】Generative Pretraining from Pixels

Generative Pretraining From Pixels 引用&#xff1a; Chen M, Radford A, Child R, et al. Generative pretraining from pixels[C]//International conference on machine learning. PMLR, 2020: 1691-1703. 论文链接&#xff1a; http://proceedings.mlr.press/v119/chen…

仿牛客网项目---消息队列的实现

本篇文章讲一讲我们的项目中用到的消息队列。 1.阻塞队列 2.kafka 我的项目为什么要用消息队列&#xff1f; 如果采用消息队列&#xff0c;那么评论、点赞、关注三类不同的事&#xff0c;可以定义三类不同的主题&#xff08;评论、点赞、关注&#xff09;&#xff0c;发生相应…

Makedown语法

这里写自定义目录标题 欢迎使用Markdown编辑器新的改变功能快捷键合理的创建标题&#xff0c;有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants 创建一个自定义列表如何创建一个…

考研C语言复习初阶(5)

目录 一.表达式求值 1.1隐式类型转换 1.2 算术转换 12.3 操作符的属性 二. 指针是什么&#xff1f; 三 指针和指针类型 3.1 指针-整数 3.2 指针的解引用 3.3 野指针 四.指针运算 4.1 指针-整数 4.2 指针-指针 4.3 指针的关系运算 5. 指针和数组 6. 二级指针 …

【学习笔记】VMware vSphere 6.7虚拟化入门

VMware vSphere 6.7虚拟化入门课程介绍 课程内容 1、VMware vSphere 6.7虚拟化入门课程介绍 2、ESXi6.7控制台设置 3、使用vSpkere Host client管理虚拟机 4、VMware EsXi基础操作 5、VMware Esxi存储管理 6、管理ESXi主机网络与虚拟机网络 7、安装配置vCenter Server Applia…

哈希表|1.两数之和

力扣题目链接 /*** Note: The returned array must be malloced, assume caller calls free().*/// leetcode 支持 ut_hash 函式庫typedef struct {int key;int value;UT_hash_handle hh; // make this structure hashable} map;map* hashMap NULL;void hashMapAdd(int key, i…

【JAVA类】利用接口的多继承实现———图书管理系统【附源码】

引言 在我们学习了一些java的基础语法之后&#xff0c;需要把这些知识点可以串起来&#xff0c;这里使用一个简单的小项目可以很好的帮助我们牢记这些知识点&#xff0c;今天就带大家学习一个有关java的小项目&#xff0c;很多学校也经常把这个项目作为他们的课程设计——经典的…

HTML5+CSS3小实例:按钮边框动效

实例:按钮边框动效 技术栈:HTML+CSS 效果: 源码: 【HTML】 <!DOCTYPE html> <html lang="zh-CN"> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.…

git入门到精通

第3章 Git常用命令 3.1 设置用户签名 3.2 初始化本地库 3.3 查看本地 状态 3.3.1 首次查看&#xff08;工作区没有任何文件&#xff09; 3.3.2 新增文件&#xff08;hello.txt&#xff09; 3.3.3 再次查者&#xff08;检測到末追踪的文件&#xff09; 3.4添加暫存区 3…

System Verilog学习笔记(二十)——TCL基础

TCL简介 TCL&#xff08;Tool Command Language&#xff09;是一种解释执行的脚本语言。它提供了通用的编程能力&#xff1a;支持变量、过程和控制结构&#xff1b;同时TCL还拥有功能强大的固有的核心命令集 由于TCL的解释器是用C/C语言的过程库实现的&#xff0c;因此可以把T…

Unity笔记:C#基础(1)

杂项 虚函数 CSDN - C虚函数详解 cnblog - C#中的虚函数virtual 常量池与new 在C#中&#xff0c;string是不可变的&#xff0c;这意味着对string对象的操作通常会返回一个新的string对象&#xff0c;而不会修改原始的string对象。因此&#xff0c;几乎所有涉及更改string内…

现在可以在Mac桌面上快速打开C知道进行AI提问

看&#xff0c;我的Mac桌面有个C知道组件&#xff0c;点击即可快速打开C知道页面进行提问使用&#xff0c;再也不需要先打开浏览器&#xff0c;再输入csdn.net访问网站&#xff0c;然后点击页面上的C知道入口。 这是如何实现的呢&#xff1f; 首先&#xff0c;我们来做一些准…

微信聊天助手

最近我在负责公司的招聘工作时&#xff0c;真是深感繁琐与困扰。每当新添加求职者的微信&#xff0c;为了了解他们的基本情况以及求职意向&#xff0c;不得不反复地提出一系列固定的问题。比如询问他们的教育背景、工作经验、擅长技能、期望职位等等&#xff0c;这些必不可少却…

东京工业大学最新!一种具有多周期特征描述的精确ORB提取器

作者&#xff1a;小柠檬 | 来源&#xff1a;3DCV 在公众号「3DCV」后台&#xff0c;回复「原论文」可获取论文pdf 添加微信&#xff1a;dddvision&#xff0c;备注&#xff1a;3D高斯&#xff0c;拉你入群。文末附行业细分群 详细内容请关注3DCV 3D视觉精品课程&#xff1a;…
最新文章