erlang - 单元测试总结(一)

本文涉及的产品
云数据库 RDS MySQL Serverless,0.5-2RCU 50GB
云数据库 RDS MySQL Serverless,价值2615元额度,1个月
简介: erlang

加入rds proxy不到一个月,有太多的东西需要重新学习,这篇博客作为对上一个月的总结(我觉得一个月总结一次蛮不错的,N年后可以看看这些年的成长路径)。

9月初加入到rds团队,开始从C++切换到erlang,一边学习新语言言,一边泛泛地看了一遍主流程的代码。中间还蹭了rds团队的outing。

rds proxy的进程模型很像hotwheel。共同点是:他们都是自己不做数据计算,都是把数据转发。

只不过rds proxy是一个商业项目,业务上要复杂的多; 而hotwheel是一个toy的开源项目。

两者都是对每一个client的连接,创建两个进程,一个做为client的代理人,一个做为server的代理人。client对真正server的数据交互,在rds上就是这两个进程的交互,因为他们代表了各自的实体。

对rds proxy的非正确理解

代码复杂度

rds proxy有固有的复杂度。

在加入rds proxy团队之前,粗略的看了ranch,cowboy,hotwheel的代码。这几个开源项目在rds proxy面前太简单了。

rds proxy是如何复杂的呢:它里面需要解析mysql变态的协议,如何握手,协商加解密,处理mysql所有的command,所有的sql语句,如何流控,如何做sql的安全,还有主备的切换,还有分库分表。。。

还有大量我暂时不知道的功能。

每一个交互都是一个状态机,每个环节都会出错。业务上的复杂导致rds proxy代码相当的复杂。

代码的模块化

在如此高的复杂下,rds proxy做了模块化设计,必须这么做。每个功能点都是一个独立的模块比如网络模块,协议处理模块,api模块,分库分表等,监控模块。

在OTP的提供的便利上,把业务做成多个supervisor监控的进程树。

这也是顺其自然的事,没有出色的地方。

代码级别一些非正确的理解

从高层上看代码的模块化已经很棒了,为了追求完美,有一些地方可以做的更好:
1) 框架代码和业务代码还存在一定的耦合度。做为一个proxy server,连接client和连接server应该使通用的,协议无关的。
2) 有的函数体积有点大,尽量使用函数特化代替cond,if,使得函数小而美等。
3) 能否把mysql的协议细节切分到多个模块中呢,看着数千行的协议代码都恐惧???而且改动起来真的是手抖阿。
4) 制定出一套编码约定:比如所有的数据全部用binary类型(Username之前都是字符串类型后来改成了binary); 变量命名方式是使用下划线,还是连在一块微软的命名方式。。。
5) 测试:目前还没有完善的单测,集成测试,系统测试等。这也是正在做的事情。
6) 类型系统:被exported出来的函数,声明类型。完善rds proxy的类型系统,借助analyzer帮助发现问题。
7) 从连接被accept到真正的建立进程路径太长,影响并发。

由于erlang和业务不熟悉,上述观点仅仅是当前的理解,目的是做为后续优化的灵感来源。

另外,最近3周在冲刺rds proxy中的mysql协议的单元测试。和mysql协议处理相关的模块逻辑很复杂,但是这些模块都是没有状态的,加上elrang的函数式特点,非常容易做单元测试。

做为一篇测试技术的总结,还是要罗唆的说一些测试相关的理论。

单元测试

对测试的理解

测试的分类:系统测试,集成测试,性能测试,单元测试,heartbeat等。前面几种测试都有测试同学的参与,同时开发同学也有相应的参与; 单元测试主要是开发同学完成。

测试同学是从产品质量的角度出发,对一个系统进行黑盒测试,保证产品的可用性,稳定性。

单元测试在函数式语言中的作用

单元测试是由开发同学深入代码,基于自己对代码的了解,对模块中的每一行代码进行测试。

设计单元测试的时候要做到:

1) 单元测试的能够保证一个模块提供的功能是正确无误的。即覆盖率要高,业界建议是80%以上,100%确实很难做到。
2) 单元测试的用例能够起到回归测试的作用。代码的重构其实比最初设计压力还要大,改的越多越心虚。因为这个时候的代码已经在线上为用户服务了,重构要达到重构目标的同时,最基本的还要保证重构后的代码逻辑和之前是一致的。
  这个时候如果能够由足够多的测试用例做回归,在重构过程中进行回归测试,会放心的多。
3) TDD,测试驱动开发。TDD在函数式语言的开发中是合适的,因为函数式语言的开发过程就是对一个个小函数拼装的过程。当模块通过了设计的单元测试用例,也就开发完成了。而且这些测试用例为以后的重构提供了保障,何乐不为呢?

elang 中的单元测试框架 eunit

eunit是从其他语言的单元测试框架(JUnit,SUnit, CPPUnit)中获取灵感。都提供了一些断言的宏,然后对测试的结果进行收集和统计。

功能是一样的,关键的是如何做到好用。

http://www.erlang.org/doc/apps/eunit/chapter.html

eunit的使用

方式一 单测代码放在模块后面(如cowbow)

准备工作(头文件)

使用eunit最简单的方式就是把测试代码放在被测试代码模块文件的可后面(如cowboy),需要包含eunit的头文件
-include_lib("eunit/include/eunit.hrl").
这个文件3个作用:
    1) 创建一个被exported的函数text(),这个函数会调用所有测试用例。
    2) 使所有函数名为..._test() 或者 ..._test_()函数被exported。
    3) 定义eunit中所有的预处理宏。
注意eunit.hrl要在erlang的搜索路径里面。

开始使用eunit

eunit会自动的调用所有..._test()的函数,返回值会自动丢掉。
最简单的测试用例 - 验证是否crash
reverse_test() -> lists:reverse([1,2,3]).
这个测试了lists:reverse在输入参数是[1,2,3]时,是否能够正常返回,并没有对返回值做任何的期望。仅仅期望这个函数不要crash。
reverse_nil_test() -> [] = lists:reverse([]).
    reverse_one_test() -> [1] = lists:reverse([1]).
    reverse_two_test() -> [2,1] = lists:reverse([1,2]).
这个3个测试,对lists:reverse的返回结果做了期望,如果不符合就会match失败,抛出异常。
使用断言宏
eunit和其他语言一样提供了各种断言宏
length_test() -> ?assert(length([1,2,3]) =:= 3).
这是一个对boolean值的断言,如果表达式返回的不是true,则抛出一个异常。

运行单元测试

通过include的方式,eunit.hrl已经把test()和所有..._test()函数exported出来了,可以直接运行。
 加入你的模块名字是m,那么有2种方式运行m中的单测:
     1) m:test().
     2) eunit:test(m).

方式二 把单测代码单独放置在另一个文件里

对模块m做单元测试,eunit会自动寻找m_tests()模块,并运行其中的测试代码。

eunit对标准输出的捕获

eunit对测试代码中的标准输出进行了捕获,只有当测试用例失败的时候才会把你的输出一起打印出来。
推荐使用eunit提供的宏打印调试中的信息。Debugging macros

eunit test as data

对一些简单的方法编写单测,eunit提供了方便的生成器,方法...test_()的返回fun列表会逐一被执行。

eunit的关闭

eunit可以关闭,只要定义NOTEST宏。如,

erlc -DNOTEST my_module.erl

或者在include Eunit头文件之前显示的定义:

-define(NOTEST, 1).

开启eunit:

erlc -DTEST my_module.erl

避免对测试代码的编译

当你把你的项目开源出去的时候,不希望测试代码被使用者编译,可以使用宏把所有的测试代码包起来。

-ifdef(TEST).
   -include_lib("eunit/include/eunit.hrl").
   -endif.

erlang中mock框架 meck

单元测试过程中有时候需要外部模块的配合,而外部模块的行为又不要控制,这个时候就需要自己动手写‘桩模块’。

erlang中的meck就是这样一个框架。

https://github.com/eproxus/meck/blob/master/src/meck.erl

meck的使用

入门级使用

Eshell V5.8.4  (abort with ^G)
    1> meck:new(dog).
    ok
    2> meck:expect(dog, bark, fun() -> "Woof!" end).
    ok
    3> dog:bark().
    "Woof!"
    4> meck:validate(dog).
    true
    5> meck:unload(dog).
    ok
    6> dog:bark().
    exception error: undefined function dog:bark/0
可以看到meck的使用很简单:
1) meck:new(dog) 先装载一个模块dog。
2) meck:expect(dog, bark, fun() -> "Woof!" end) 声明dog中的bark函数被调用的时候,执行fun() -> "Woof!" end。
3) meck:unload(dog)。使用完之后卸载dog模块。
这样在调用dog:bark()的时候,就调用了meck的函数。

meck模拟异常

meck可以模拟一个函数抛出异常,以此达到调用模块对异常的处理能力。
5> meck:expect(dog, meow, fun() -> meck:exception(error, not_a_cat) end).
    ok
    6> catch dog:meow().
    {'EXIT',{not_a_cat,[{meck,exception,2},
    {meck,exec,4},
    {dog,meow,[]},
    {erl_eval,do_apply,5},
    {erl_eval,expr,5},
    {shell,exprs,6},
    {shell,eval_exprs,6},
    {shell,eval_loop,3}]}}
    7> meck:validate(dog).
    true

mock部分函数

在meck:new(dog)的时候,会隐藏掉dog模块中所有的函数。
通过传入passthrough可以使meck仅隐藏被expect的函数。
Eshell V5.8.4  (abort with ^G)
    1> meck:new(string, [unstick, passthrough]).
    ok
    2> string:strip("  test  ").
    "test"
也可以在expect中调用passthrough/1
比如,
Eshell V5.8.4  (abort with ^G)
    1> meck:new(string, [unstick]).
    ok
    2> meck:expect(string, strip, fun(String) -> meck:passthrough([String]) end).
    ok
    3> string:strip("  test  ").
    "test"
    4> meck:unload(string).
    ok
    5> string:strip("  test  ").
    "test"
更好的选项可以直接察看meck.erl。

meck 一个函数的多个返回结果

meck:sequence可以指定一个列表,使得对同一个函数的多次调用,返回不同的结果。
Ret = ["woof1", "Woof2", "Woof3"],
    meck:new(dog, [passthrough]),
    meck:sequence(dog, bark, 0, Ret),
    dog:bark(), % "Woof1"
    dog:bark(), % "Woof2"
    dog:bark(), % "Woof3"

更多的使用技巧直接看meck的代码吧。

相关实践学习
基于CentOS快速搭建LAMP环境
本教程介绍如何搭建LAMP环境,其中LAMP分别代表Linux、Apache、MySQL和PHP。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助     相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
Java 测试技术 Maven
一次单元测试优化的过程总结
本文将介绍淘宝用户运营平台团队最近在实践单元测试过程中遇到的一个问题。
324 0
一次单元测试优化的过程总结
|
测试技术 监控 NoSQL
提升单元测试体验的利器--Mockito使用总结
为神马要使用Mockito?   在编写单元测试的时候,为了尽可能的保证隔离性,我们时常需要对某些不容易构造或者不容易获取或者对外部环境有依赖的对象,用一个虚拟的对象来创建以便于测试.假设你正在开发的的代码中使用到了公司其他部门的接口(通过RPC服务),当编写单元测试的时候你可能为了不让接口真的去调...
1444 0
|
Java 测试技术 数据格式
单元测试框架TestNg使用总结
工欲善其事,必先利其器 单元测试的重要性是不言而喻的。但如果没有好的单元测试工具,是无法激起开发人员的欲望。 Testng便是利器之一。TestNG是基于Annotation的测试框架的先驱,他拥有通过添加诸如灵活的装置、测试分类、参数测试和依赖方法等特性来克服JUnit3的一些不足之处。
1101 0
|
Java 测试技术 Apache
给servlet写单元测试的总结
servlet的测试一般来说需要容器的支持,不是像通常的java类的junit测试一样简单, 下面通过对HelloWorld代码的测试阐述了几种servlet测试方法。 被测试的HelloWorld类的代码如下: /** * 被测试的servlet */ import java.
1025 0
|
1月前
|
Java 测试技术 开发者
Java单元测试与集成测试:确保代码质量的最佳实践
【4月更文挑战第2天】在软件开发中,单元测试验证单个代码单元(如Java类或方法)的功能,确保其正确性;而集成测试则关注多个组件协作时的交互。JUnit是常见的Java单元测试框架,集成测试则检验组件间接口的兼容性。Spring框架提供了集成测试的支持。遵循良好编码习惯,编写可测试代码,设计全面的测试用例,是保证代码质量和稳定性的关键。
|
1月前
|
Java 测试技术
SpringBoot整合单元测试&&关于SpringBoot单元测试找不到Mapper和Service报java.lang.NullPointerException的错误
SpringBoot整合单元测试&&关于SpringBoot单元测试找不到Mapper和Service报java.lang.NullPointerException的错误
22 0
|
4天前
|
测试技术
测试基础 Junit单元测试框架
测试基础 Junit单元测试框架
11 2
测试基础 Junit单元测试框架
|
12天前
|
安全 测试技术 Go
Golang深入浅出之-Go语言单元测试与基准测试:testing包详解
【4月更文挑战第27天】Go语言的`testing`包是单元测试和基准测试的核心,简化了测试流程并鼓励编写高质量测试代码。本文介绍了测试文件命名规范、常用断言方法,以及如何进行基准测试。同时,讨论了测试中常见的问题,如状态干扰、并发同步、依赖外部服务和测试覆盖率低,并提出了相应的避免策略,包括使用`t.Cleanup`、`t.Parallel()`、模拟对象和检查覆盖率。良好的测试实践能提升代码质量和项目稳定性。
17 1