在这篇文章中,我将分享我对围棋程序应该如何以一种干净而灵活的方式进行测试的看法。
日志记录、度量集合以及与某些代码的主要功能无关的任何内容都不得出现在该代码中。取而代之的是可以由用户检测的代码的定义跟踪点。
让我们假设我们有一个名为lib的包和一些lib.clientstructure,它们在每次执行somquest时都会ping其底层连接。
包库类型客户端结构{conn net.。conn}函数(c*客户端)请求(CTX上下文。上下文)错误{if err:=c..。ping(Ctx);err!=nil{return err}//这里有一些逻辑。}func(c*client)ping(CTX上下文。上下文)错误{返回掺杂(CTX,c.。CONN)}。
如果我们需要在ping发生之前和之后写入一些日志,该怎么办?一种方法是将一些记录器(或记录器接口)注入客户端:
包库类型客户端结构{Logger Logger Conn Net.。conn}函数(c*客户端)ping(CTX上下文。上下文)(错误){c..。伐木者。信息(";ping开始";)错误=掺杂(CTX,c.。conn)c.。伐木者。信息(";ping已完成(错误为%v)";,错误)返回}。
包库类型客户端结构{Logger Logger Metrics Registry Conn Net.。conn}函数(c*客户端)ping(CTX上下文。上下文)(错误){开始:=时间。现在()c.。伐木者。信息(";ping开始";)错误=掺杂(CTX,c.。conn)c.。伐木者。信息(";ping已完成(错误为%v)";,err)度量:=c..。指标。获取(";PING_LATHERY";)指标。发送(时间。自(开始))返回错误}。
如果我们继续向客户端添加检测方法,我们将意识到它的大部分代码都与检测相关,而不是与客户端的主要功能相关(这只是一行带有掺杂()调用的代码)。
不连贯的(与我们客户的主要目的无关)代码行数只是该方法的第一个问题。
如果在程序的操作过程中,您意识到度量名称(例如,指标名称)是错误的,并且您应该将其重命名,该怎么办呢?或者您必须使用一些不同的库来记录日志?
使用上面的方法,您将需要转到客户端的实现(以及其他类似的组件)并对其进行更改。
这意味着每当与组件的主要功能无关的内容发生更改时,您都要更改代码。换句话说,这样的设计确实违反了SRP原则。
如果您在多个程序之间共享代码,该怎么办?如果您甚至根本无法控制代码的使用者(老实说,我建议将每个包视为被未知数量的程序重用的包,即使实际上只有一个包是您的)。
在我看来,正确的方法是定义跟踪点(又名钩子),然后代码用户可以在运行时使用某个函数(又名探测)初始化这些跟踪点。
当然,这仍然会增加一些额外的代码行,但也会给用户带来灵活性,让他们可以使用任何适当的方法来测量我们组件的运行时。
让我们提供几乎相同的机制,但有一个变化。我们不提供OnPingStart()和OnPingDone()挂钩,而是引入单个OnPing()挂钩,该挂钩将在ping之前被调用,并将返回一个回调,后者将在ping之后立即被调用。这样,我们可以将一些变量存储在闭包中,以便在ping完成时访问它们(例如,计算ping延迟)。
包库类型客户端结构{OnPing func()func(Error)conn net.。conn}函数(c*客户端)ping(CTX上下文。上下文)(错误){完成:=c..。OnPing()Err=掺杂(CTX,c.。conn)完成(错误)返回}。
看起来很整洁,但除非我们意识到OnPing挂钩和回调它的返回可能都为零:
函数(c*客户端)ping(CTX上下文。上下文)(错误){如果fn:=c,则变量完成函数(错误)。OnPing;Fn!=nil{Done=Fn()}Err=掺杂(CTX,c.。conn)如果完成!=nil{Done(Err)}return}。
现在它是正确的,仍然擅长灵活性和SRP原则,但不太擅长代码简单性。
在简化代码之前,让我们先讨论一下当前钩子实现还存在的另一个问题。
用户将如何设置多个探测器?因此,前面提到的httptrace包具有ClientTrace.compose()方法,该方法将两个跟踪结构合并到第三个跟踪结构中。因此,result tingtrace中的每个探测函数都将调用先前跟踪中的相应探测(如果设置了它们的话)。
因此,让我们首先尝试手动执行相同的操作(并且不使用反射)。为此,我们将OnPing挂钩从客户端移至单独的结构ClientTrace:
包库类型客户端结构{Trace ClientTrace conn net.。conn}type ClientTrace struct{OnPing func()func(Error)}
func(A ClientTrace)Compose(B ClientTrace)(C ClientTrace){Switch{case a.。OnPing==nil:c.。OnPing=b.。安平案件b。OnPing==nil:c.。Onping=a.。OnPing默认值:c.。onPing=func()func(错误){doona:=a.。On Ping()Done B:=b.。onPing()开关{case dona==nil:return donB case donA B==nil:return dona默认值:return func(Err Error){dona(Err)donB(Err)}return c}。
单钩的代码很多,对吧?现在让我们继续前进,稍后再回来讨论这个问题。
包主导入(";log";";ome/path/to/lib";)func main(){var trace lib.。ClientTrace//日志挂钩。TRACE=跟踪。作曲(唱词)。ClientTrace{OnPing:func()函数(错误){日志。Println(";ping start";)返回函数(错误){日志。Println(";ping Done";,Err)}},})//一些指标挂钩。TRACE=跟踪。作曲(唱词)。ClientTrace{OnPing:func()func(错误){start:=time.。NOW()返回函数(错误){METRY:=STATS。获取(";PING_LATHERY";)指标。发送(时间。由于(Start))}},})c:=lib。客户端{跟踪:跟踪,}}。
我们还可以为用户提供的一件事是基于上下文的跟踪。也就是说,它与httptrace包中的完全相同-能够将挂钩与传递给Client.Request()的context.Context相关联。
包库类型clientTraceContextKey struct{}func ClientTrace(CTX上下文。上下文)ClientTrace{t,_:=ctx.。value(clientTraceContextKey{})return t}func WithClientTrace(CTX上下文。上下文,t ClientTrace)上下文。context{prev:=ContextClientTrace(Ctx)返回上下文。WithValue(CTX,clientTraceContextKey{},Prev.。作曲(T),)}。
哈。看起来现在差不多完成了,我们已经准备好为我们组件的用户带来所有最好的跟踪工具。
但是,为我们想要检测的每个结构编写所有代码不是真的很乏味吗?当然,您可以为此编写一些vim的宏(实际上我以前也是这样做的),但是让我们看看替代方法。
好消息是合并钩子和检查nil,以及特定于上下文的函数都是模式化的,所以我们可以在没有宏或反射的情况下为它们生成GO代码。
gtrace是一个命令行工具,它为上面讨论的tracingmethod生成GO代码。它建议您使用带有钩子字段的结构(用//gtrace:gen标记),并围绕它们生成帮助器函数,这样您就可以合并这样的结构并调用钩子,而无需任何检查。它还可以生成上下文感知帮助器来调用与上下文相关联的挂钩。
Package lib//go:生成gtrace//gtrace:gen//gtrace:设置上下文类型ClientTrace struct{OnPing func()func(Error)}type client struct{Trace ClientTrace conn net。conn}函数(c*客户端)ping(CTX上下文。上下文)(错误){完成:=c..。痕迹。onPing(CTX)ERR=掺杂(CTX,c.。conn)完成(错误)返回}。
运行Go Generate之后,我们将能够使用在ClientTrace中定义的生成的非导出版本的跟踪挂钩。
就这样!。gtrace负责样板代码,并允许您将注意力集中在您希望能够由用户检测的跟踪点上。
克里斯韦隆的极简主义C库。我很久以前读过这篇文章,关于图书馆如何组织的清晰想法给了我灵感。