在修改Redis内核之后,第一步我们需要做的就是添加或者对应的单元测试用例来进行基本的单元测试。本文将对Redis内核单元测试框架进行基本的解析,并对如何编写测试用例进行基本的讲解。
Redis单元测试框架是基于tcl sh脚本实现的,其启动的方式为runtest [options]。每一类的测试case写在单独的测试文件中,测试文件列表写入到test_server中all_tests列表中。在启动测试时,会以server模式启动一个测试服务器,再启动多个测试客户端与之通信。由测试服务器会给空闲的测试服务端发送测试任务,参数为测试用例所在脚本文件名,由测试客户端执行对应的测试用例。详细的流程图如下:
对选项进行解析,其中默认的模式是server模式,进入test_server_main函数; 若带了client选项则进入test_client_main函数.
启动测试客户端,往测试服务器的fd上发送ready消息,开启客户端与服务端的交互流程;
新建一个测试用例文件,比如dummy.tcl,将之加入到test_helper.tcl的all_tests列表里
set ::all_tests { unit/auth ... unit/dummy ... }这样启动测试的时候,会自动执行unit/dummy.tcl里面的测试用例;
每个测试用例文件里面可以包含多个start_server的部分,每个start_server都会启动一个redis实例。每个start_server内部包含多个test函数模块,每个test函数对应一个测试用例。例子:auth.tcl
start_server {tags {"auth"}} { test {AUTH fails if there is no password configured server side} { catch {r auth foo} err set _ $err } {ERR*no password*} } start_server {tags {"auth"} overrides {requirepass foobar}} { test {AUTH fails when a wrong password is given} { catch {r auth wrong!} err set _ $err } {ERR*invalid password} test {Arbitrary command gives an error when AUTH is required} { catch {r set foo bar} err set _ $err } {NOAUTH*} test {AUTH succeeds when the right password is given} { r auth foobar } {OK} test {Once AUTH succeeded we can actually send commands to the server} { r set foo 100 r incr foo } {101} }使用start_server可以启动一个redis实例. 启动的时候接受三种类型的参数:
config: redis server的配置文件名,文件放到tests/assets目录下;override: 覆盖配置文件中的某个具体配置;tags: 该server的标示,一般用于log输出;启动一个redis实例的例子可以见上一节的auth.tcl.在进行主从同步测试,集群测试的时候,需要同时起多个redis实例,直接在一个test_server内部,再执行test_server即可。例子:
start_server {tags {"repl"}} { start_server {} { test {First server should have role slave after SLAVEOF} { r -1 slaveof [srv 0 host] [srv 0 port] after 1000 s -1 role } {slave} } }测试case中执行redis命令用r函数.(s函数与r函数类似,只是s函数会从info中提取返回值)
proc r {args} { set level 0 if {[string is integer [lindex $args 0]]} { set level [lindex $args 0] set args [lrange $args 1 end] } [srv $level "client"] {*}$args }当同时启动多个redis实例时,使用r函数的第一个参数,标示具体在哪个实例上执行对应的命令。0为当前redis实例,-1为上一个启动的redis实例,以此类推。例如:
start_server {tags {"repl"}} { r set mykey foo start_server {} { test {Second server should have role master at first} { s role } {master} test {SLAVEOF should start with link status "down"} { r slaveof [srv -1 host] [srv -1 port] s master_link_status } {down} } }结果判断有几种方式:
assert类:详见support/test.tclfail "comment": 失败test函数最后一个参数,支持正则表达式。其匹配的对象是最后一条redis命令返回的结果。例如: test {AUTH fails when a wrong password is given} { catch {r auth wrong!} err set _ $err } {ERR*invalid password} test {Arbitrary command gives an error when AUTH is required} { catch {r set foo bar} err set _ $err } {NOAUTH*}wait_for_condition {maxtries delay e else elsescript}函数可以同步等待指定条件被满足。例:
test "Fixed AOF: Keyspace should contain values that were parseable" { set client [redis [dict get $srv host] [dict get $srv port]] wait_for_condition 50 100 { [catch {$client ping} e] == 0 } else { fail "Loading DB is taking too much time." } assert_equal "hello" [$client get foo] assert_equal "" [$client get bar] }start_write_load {host port seconds}函数可以不停的往实例中写入数据。
下面是一个主从同步的例子,作为这一节的结束和测试。
foreach dl {no yes} { start_server {tags {"repl"}} { set master [srv 0 client] $master config set repl-diskless-sync $dl set master_host [srv 0 host] set master_port [srv 0 port] set slaves {} set load_handle0 [start_write_load $master_host $master_port 3] set load_handle1 [start_write_load $master_host $master_port 5] set load_handle2 [start_write_load $master_host $master_port 20] set load_handle3 [start_write_load $master_host $master_port 8] set load_handle4 [start_write_load $master_host $master_port 4] start_server {} { lappend slaves [srv 0 client] start_server {} { lappend slaves [srv 0 client] start_server {} { lappend slaves [srv 0 client] test "Connect multiple slaves at the same time (issue #141), diskless=$dl" { # Send SALVEOF commands to slaves [lindex $slaves 0] slaveof $master_host $master_port [lindex $slaves 1] slaveof $master_host $master_port [lindex $slaves 2] slaveof $master_host $master_port # Wait for all the three slaves to reach the "online" # state from the POV of the master. set retry 500 while {$retry} { set info [r -3 info] if {[string match {*slave0:*state=online*slave1:*state=online*slave2:*state=online*} $info]} { break } else { incr retry -1 after 100 } } if {$retry == 0} { error "assertion:Slaves not correctly synchronized" } # Wait that slaves acknowledge they are online so # we are sure that DBSIZE and DEBUG DIGEST will not # fail because of timing issues. wait_for_condition 500 100 { [lindex [[lindex $slaves 0] role] 3] eq {connected} && [lindex [[lindex $slaves 1] role] 3] eq {connected} && [lindex [[lindex $slaves 2] role] 3] eq {connected} } else { fail "Slaves still not connected after some time" } # Stop the write load stop_write_load $load_handle0 stop_write_load $load_handle1 stop_write_load $load_handle2 stop_write_load $load_handle3 stop_write_load $load_handle4 # Make sure that slaves and master have same # number of keys wait_for_condition 500 100 { [$master dbsize] == [[lindex $slaves 0] dbsize] && [$master dbsize] == [[lindex $slaves 1] dbsize] && [$master dbsize] == [[lindex $slaves 2] dbsize] } else { fail "Different number of keys between masted and slave after too long time." } # Check digests set digest [$master debug digest] set digest0 [[lindex $slaves 0] debug digest] set digest1 [[lindex $slaves 1] debug digest] set digest2 [[lindex $slaves 2] debug digest] assert {$digest ne 0000000000000000000000000000000000000000} assert {$digest eq $digest0} assert {$digest eq $digest1} assert {$digest eq $digest2} } } } } } }Redis内核自动化测试框架可以同时启动多个测试客户端进行测试,其测试用例编写简便,测试效率高,使用起来非常方便。该测试框架也可以很方便的改造成其它基于socket通信的服务的自动化测试框架。简约,高效,这就是我对它的印象。
相关资源:miniredis:用于Go单元测试的Pure Go Redis服务器-源码