一个 UT Failed 引出的思考

开发 项目管理
前几天某个服务 ut 失败,导致别人无法构建。查看下源代码以及 ut case, 发现槽点蛮多,分享下如何修复,写单测要注意的一些点,由此引出设计模式中的概念依赖反转、依赖注入、控制反转。

[[420832]]

本文转载自微信公众号「董泽润的技术笔记」,作者董泽润。转载本文请联系董泽润的技术笔记公众号。

前几天某个服务 ut 失败,导致别人无法构建。查看下源代码以及 ut case, 发现槽点蛮多,分享下如何修复,写单测要注意的一些点,由此引出设计模式中的概念依赖反转、依赖注入、控制反转

失败 case

  1. func toSeconds(in int64) int64 { 
  2.  if in > time.Now().Unix() { 
  3.   nanosecondSource := time.Unix(0, in
  4.   if dateIsSane(nanosecondSource) { 
  5.    return nanosecondSource.Unix() 
  6.   } 
  7.  
  8.   millisecondSource := time.Unix(0, in*int64(time.Millisecond)) 
  9.   if dateIsSane(millisecondSource) { 
  10.    return millisecondSource.Unix() 
  11.   } 
  12.  
  13.   // default to now rather than sending something stupid 
  14.   return time.Now().Unix() 
  15.  } 
  16.  return in 
  17.  
  18. func dateIsSane(in time.Time) bool { 
  19.  return (in.Year() >= (time.Now().Year()-1) && 
  20.   in.Year() <= (time.Now().Year()+1)) 

函数 toSeconds 接收一个时间参数,可能是秒、毫秒和其它时间,经过判断后返回秒值

  1. ...... 
  2.   { 
  3.  desc:   "less than now"
  4.  args:   1459101327, 
  5.  expect: 1459101327, 
  6. }, 
  7.  desc:   "great than year"
  8.  args:   now.UnixNano()/6000*6000 + 7.55424e+17, 
  9.  expect: now.Unix(), 
  10. }, 
  11. ...... 

上面是 test case table, 最后报错 great than year 断言失败了。简单的看下实现逻辑就能发现,函数是想修正到秒值,但假如刚好 go gc STW 100ms, 就会导致 expect 与实际结果不符

如何从根本上修复问题呢?要么修改函数签名,外层传入 time.Now()

  1. func toSeconds(in int64, now time.Time) int64 { 
  2.   ...... 

要么将 time.Now 函数定义成当前包内变量,写单测时修改 now 变量

  1. var now = time.Now 
  2.  
  3. func toSeconds(in int64) int64 { 
  4.  if in > now().Unix() { 
  5.   ...... 

以上两种方式都比较常见,本质在于单测 ut 不应该依赖于当前系统环境,比如 mysql, redis, 时间等等,应该仅依赖于输入参数,同时函数执行多次结果应该一致。去年遇到过 CI 机器换了,新机器没有 redis/mysql, 导致一堆 ut failed, 这就是不合格的写法

如果依赖环境的资源,那么就变成了集成测试。如果进一步再依赖业务的状态机,那么就变成了回归测试,可以说是层层递进的关系。只有做好代码的单测,才能进一步确保其它测试正常。同时也不要神话单测,过份追求 100% 覆盖

依赖注入

刚才我们非常自然的引入了设计模式中,非常重要的 依赖注入 Dependenccy injection 概念

  1. func toSeconds(in int64, now time.Time) int64  

简单的讲,toSeconds 函数调用系统时间 time.Now, 我们把依赖以参数的形式传给 toSeconds 就是注入依赖,定义就这么简单

关注 DI, 设计模式中抽像出来四个角色:

  • service 我们所被依赖的对像
  • client 依赖 service 的角色
  • interface 定义 client 如何使用 service 的接口
  • injector 注入器角色,用于构造 service, 并将之传给 client

我们来看一下面像对像的例子,Hero 需要有武器,NewHero 是英雄的构造方法

  1. type Hero struct { 
  2.  name   string 
  3.  weapon Weapon 
  4.  
  5. func NewHero(name string) *Hero { 
  6.  return &sHero{ 
  7.   name:   name
  8.   weapon: NewGun(), 
  9.  } 

这里面问题很多,比如换个武器 AK 可不可以呢?当然行。但是 NewHero 构造时依赖了 NewGun, 我们需要把武器在外层初始化好,然后传入

  1. type Hero struct { 
  2.  name   string 
  3.  weapon Weapon 
  4.  
  5. func NewHero(name string, wea Weapon) *Hero { 
  6.  return &Hero{ 
  7.   name:   name
  8.   weapon: wea, 
  9.  } 
  10.  
  11. func main(){ 
  12.  wea:= NewGun(); 
  13.  myhero = NewHero("killer47", wea) 

在这个 case 里面,Hero 就是上面提到的 client 角色,Weapon 就是 service 角色,injector 是谁呢?是 main 函数,其实也是码农

这个例子还有问题,原因在于武器不应该是具体实例,而应该是接口,即上面提到的 interface 角色

  1. type Weapon interface { 
  2.  Attack(damage int

也就是说我们的武器要设计成接口 Weapon, 方法只有一个 Attack 攻击并附带伤害。但是到现在还不是理想的,比如说我没有武器的时候,就不能攻击人了嘛?当然能,还有双手啊,所以有时我们要用 Option 实现默认依赖

  1. type Weapon interface { 
  2.  Attack(damage int
  3.  
  4. type Hero struct { 
  5.  name   string 
  6.  weapon Weapon 
  7.  
  8. func NewHero(name string, opts ...Option) *Hero { 
  9.  h := &Hero{ 
  10.   namename
  11.  } 
  12.  
  13.  for _, option := range options { 
  14.   option(i) 
  15.  } 
  16.  
  17.  if h.weapon == nil { 
  18.   h.weapon = NewFist() 
  19.  } 
  20.  return h 
  21.  
  22. type Option func(*Hero) 
  23.  
  24. func WithWeapon(w Weapon) Option { 
  25.  return func(i *Hero) { 
  26.   i.weapon = w 
  27.  } 
  28.  
  29. func main() { 
  30.  wea := NewGun() 
  31.  myhero = NewHero("killer47", WithWeapon(wea)) 

上面就是一个生产环境中,比较理想的方案,看不明白的可以运行代码试着理解下

第三方框架

刚才提到的例子比较简单,injector 由码农自己搞就行了。但是很多时候,依赖的对像不只一个,可能很多,还有交叉依赖,这时候就需要第三方框架来支持了

  1.  <?xml version="1.0" encoding="UTF-8"?> 
  2.  <beans xmlns="http://www.springframework.org/schema/beans" 
  3.   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
  4.   xsi:schemaLocation="http://www.springframework.org/schema/beans 
  5.   http://www.springframework.org/schema/beans/spring-beans-3.0.xsd"> 
  6.  
  7.     <bean id="service" class="ExampleService"
  8.     </bean> 
  9.  
  10.     <bean id="client" class="Client"
  11.         <constructor-arg value="service" />         
  12.     </bean> 
  13. </beans> 

 

 

 

Java 党写配置文件,用注解来实现。对于 go 来讲,可以使用 wire, https://github.com/google/wire

  1. // +build wireinject 
  2.  
  3. package main 
  4.  
  5. import ( 
  6.     "github.com/google/wire" 
  7.     "wire-example2/internal/config" 
  8.     "wire-example2/internal/db" 
  9.  
  10. func InitApp() (*App, error) { 
  11.     panic(wire.Build(config.Provider, db.Provider, NewApp)) // 调用wire.Build方法传入所有的依赖对象以及构建最终对象的函数得到目标对象 

类似上面一样,定义 wire.go 文件,然后写上 +build wireinject 注释,调用 wire 后会自动生成 injector 代码

  1. //go:generate go run github.com/google/wire/cmd/wire 
  2. //+build !wireinject 
  3.  
  4. package main 
  5.  
  6. import ( 
  7.     "wire-example2/internal/config" 
  8.     "wire-example2/internal/db" 
  9.  
  10. // Injectors from wire.go: 
  11.  
  12. func InitApp() (*App, error) { 
  13.     configConfig, err := config.New() 
  14.     if err != nil { 
  15.         return nil, err 
  16.     } 
  17.     sqlDB, err := db.New(configConfig) 
  18.     if err != nil { 
  19.         return nil, err 
  20.     } 
  21.     app := NewApp(sqlDB) 
  22.     return app, nil 

我司有项目在用,感兴趣的可以看看官方文档,对于构建大型项目很有帮助

依赖反转 DIP 原则

我们还经常听说一个概念,就是依赖反转 dependency inversion principle, 他有两个最重要的原则:

  • High-level modules should not depend on low-level modules. Both should depend on abstractions (e.g., interfaces).
  • Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.

高层模块不应该依赖低层模块,需要用接口进行抽像。抽像不应该依赖于具体实现,具体实现应该依赖于抽像,结合上面的 Hero&Weapon 案例应该很清楚了

那我们学习 DI、DIP 这些设计模式目的是什么呢?使我们程序各个模块之间变得松耦合,底层实现改动不影响顶层模块代码实现,提高模块化程度,增加括展性

但是也要有个度,服务每个都做个 interface 抽像一个模块是否可行呢?当然不,基于这么多年的工程实践,我这里面有个准则分享给大家:易变的模块需要做出抽像、跨 rpc 调用的需要做出抽像

控制反转 IOC 思想

本质上依赖注入是控制反转 IOC 的具体一个实现。在传统编程中,表达程序目的的代码调用库来处理通用任务,但在控制反转中,是框架调用了自定义或特定任务的代码,Java 党玩的比较多

推荐大家看一下 coolshell 分享的 undo 例子。比如我们有一个 set 想实现 undo 撤回功能

  1. type IntSet struct { 
  2.     data map[int]bool 
  3. func NewIntSet() IntSet { 
  4.     return IntSet{make(map[int]bool)} 
  5. func (set *IntSet) Add(x int) { 
  6.     set.data[x] = true 
  7. func (set *IntSet) Delete(x int) { 
  8.     delete(set.data, x) 
  9. func (set *IntSet) Contains(x int) bool { 
  10.     return set.data[x] 

这是一个 IntSet 集合,拥有三个函数 Add, Delete, Contains, 现在需要添加 undo 功能

  1. type UndoableIntSet struct { // Poor style 
  2.     IntSet    // Embedding (delegation) 
  3.     functions []func() 
  4.   
  5. func NewUndoableIntSet() UndoableIntSet { 
  6.     return UndoableIntSet{NewIntSet(), nil} 
  7.   
  8. func (set *UndoableIntSet) Add(x int) { // Override 
  9.     if !set.Contains(x) { 
  10.         set.data[x] = true 
  11.         set.functions = append(set.functions, func() { set.Delete(x) }) 
  12.     } else { 
  13.         set.functions = append(set.functions, nil) 
  14.     } 
  15. func (set *UndoableIntSet) Delete(x int) { // Override 
  16.     if set.Contains(x) { 
  17.         delete(set.data, x) 
  18.         set.functions = append(set.functions, func() { set.Add(x) }) 
  19.     } else { 
  20.         set.functions = append(set.functions, nil) 
  21.     } 
  22. func (set *UndoableIntSet) Undo() error { 
  23.     if len(set.functions) == 0 { 
  24.         return errors.New("No functions to undo"
  25.     } 
  26.     index := len(set.functions) - 1 
  27.     if function := set.functions[index]; function != nil { 
  28.         function() 
  29.         set.functions[index] = nil // For garbage collection 
  30.     } 
  31.     set.functions = set.functions[:index
  32.     return nil 

上面是具体的实现,有什么问题嘛?有的,undo 理论上只是控制逻辑,但是这里和业务逻辑 IntSet 的具体实现耦合在一起了

  1. type Undo []func() 
  2.  
  3. func (undo *Undo) Add(function func()) { 
  4.   *undo = append(*undo, function
  5. func (undo *Undo) Undo() error { 
  6.   functions := *undo 
  7.   if len(functions) == 0 { 
  8.     return errors.New("No functions to undo"
  9.   } 
  10.   index := len(functions) - 1 
  11.   if function := functions[index]; function != nil { 
  12.     function() 
  13.     functions[index] = nil // For garbage collection 
  14.   } 
  15.   *undo = functions[:index
  16.   return nil 

上面就是我们 Undo 的实现,跟本不用关心业务具体的逻辑

  1. type IntSet struct { 
  2.     data map[int]bool 
  3.     undo Undo 
  4.   
  5. func NewIntSet() IntSet { 
  6.     return IntSet{data: make(map[int]bool)} 
  7.  
  8. func (set *IntSet) Undo() error { 
  9.     return set.undo.Undo() 
  10.   
  11. func (set *IntSet) Contains(x int) bool { 
  12.     return set.data[x] 
  13.  
  14. func (set *IntSet) Add(x int) { 
  15.     if !set.Contains(x) { 
  16.         set.data[x] = true 
  17.         set.undo.Add(func() { set.Delete(x) }) 
  18.     } else { 
  19.         set.undo.Add(nil) 
  20.     } 
  21.   
  22. func (set *IntSet) Delete(x int) { 
  23.     if set.Contains(x) { 
  24.         delete(set.data, x) 
  25.         set.undo.Add(func() { set.Add(x) }) 
  26.     } else { 
  27.         set.undo.Add(nil) 
  28.     } 

这个就是控制反转,不再由控制逻辑 Undo 来依赖业务逻辑 IntSet, 而是由业务逻辑 IntSet 来依赖 Undo. 想看更多的细节可以看 coolshell 的博客

再举两个例子,我们有 lbs 服务,定时更新司机的坐标流,中间需要处理很多业务流程,我们埋了很多 hook 点,业务逻辑只需要对相应的点注册就可以了,新增加业务逻辑无需改动主流程的代码

 

很多公司在做中台,比如阿里做的大中台,原来各个业务线有自己的业务处理逻辑,每条业务线都有工程师只写各自业务相关的代码。中台化会抽像出共有的流程,每个新的业务只需要配置文件自定义需要的哪些模块即可,这其实也是一种控制反转的思想

 

责任编辑:武晓燕 来源: 董泽润的技术笔记
相关推荐

2020-06-09 08:06:31

RocketMQ消息耗时

2022-11-13 10:07:22

SpringSpringBoot

2020-07-10 09:55:15

程序员技能开发者

2013-12-19 09:58:36

移动应用产品市场

2012-11-12 10:46:37

2011-08-25 09:03:40

2019-07-01 09:58:05

鸿蒙系统华为

2021-06-06 16:15:57

地区接口项目

2014-05-13 13:42:54

工程师流程管理

2010-08-06 14:05:56

WPF

2013-03-26 14:17:21

架构架构设计事件驱动

2020-01-06 13:11:30

技术工具

2012-12-17 10:50:27

程序员

2012-07-10 16:09:54

2024-03-14 09:07:05

刷数任务维度后端

2022-01-04 09:01:10

开源项目开源技术

2022-04-19 08:26:20

WebAPI架构

2012-10-22 14:17:42

函数式程序员

2009-12-10 15:17:58

Linux操作系统

2016-09-26 17:26:20

点赞
收藏

51CTO技术栈公众号