Redis内核单元测试框架

本文涉及的产品
云数据库 MongoDB,独享型 2核8GB
推荐场景:
构建全方位客户视图
云数据库 Redis 版,标准版 2GB
推荐场景:
搭建游戏排行榜
云原生内存数据库 Tair,内存型 2GB
简介: 本文将对Redis内核单元测试框架进行基本的解析,并对如何编写测试用例进行基本的讲解。

在修改Redis内核之后,第一步我们需要做的就是添加或者对应的单元测试用例来进行基本的单元测试。本文将对Redis内核单元测试框架进行基本的解析,并对如何编写测试用例进行基本的讲解。

单元测试框架流程

Redis单元测试框架是基于tcl sh脚本实现的,其启动的方式为runtest [options]。
每一类的测试case写在单独的测试文件中,测试文件列表写入到test_server中all_tests列表中。
在启动测试时,会以server模式启动一个测试服务器,再启动多个测试客户端与之通信。由测试服务器会给空闲的测试服务端发送测试任务,参数为测试用例所在脚本文件名,由测试客户端执行对应的测试用例。详细的流程图如下:
redis_runtest

1. processOptions

对选项进行解析,其中默认的模式是server模式,进入test_server_main函数; 若带了client选项则进入test_client_main函数.

2. test_server_main

  1. 内部维护了一系列当前的测试客户端状态列表;
  2. accept_test_clients 创建一个socket fd,侦听来自测试客户端的消息;
  3. 按照传入的参数,以runtest --client的方式启动n个测试client;

3. test_client_main

启动测试客户端,往测试服务器的fd上发送ready消息,开启客户端与服务端的交互流程;

4. 客户端与服务端的交互

  1. 客户端启动后,往测试服务器fd上发送ready消息;
  2. 服务端收到客户端ready或done事件后,检查所有的测试集,若还有测试任务未完成,则使用signal_idle_client方法往测试客户端发送测试任务,即"run 测试用例脚本文件名"消息;
  3. 客户端收到run消息后,调用execute_tests $data方法执行测试用例脚本文件;
  4. 客户端执行完测试case脚本后,往服务端发送done事件。再次进入第2步;

测试用例编写

1. 增加测试用例

新建一个测试用例文件,比如dummy.tcl,将之加入到test_helper.tcl的all_tests列表里

set ::all_tests {
    unit/auth
    ...
    unit/dummy
    ...
}

这样启动测试的时候,会自动执行unit/dummy.tcl里面的测试用例;

2. 测试用例文件

每个测试用例文件里面可以包含多个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}
}

3. 启动redis实例

启动单个实例

使用start_server可以启动一个redis实例. 启动的时候接受三种类型的参数:

  1. config: redis server的配置文件名,文件放到tests/assets目录下;
  2. override: 覆盖配置文件中的某个具体配置;
  3. 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}
    }
}

4. 执行redis命令

测试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}
    }
}

5. 结果判断

结果判断有几种方式:

  • assert类:详见support/test.tcl
  • fail "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*}

6. 同步等待函数

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]
        }

7. 随机生成数据

start_write_load {host port seconds}函数可以不停的往实例中写入数据。

8. 一个稍复杂的例子

下面是一个主从同步的例子,作为这一节的结束和测试。

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通信的服务的自动化测试框架。
简约,高效,这就是我对它的印象。

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore     ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库 ECS 实例和一台目标数据库 RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
7天前
|
存储 Java 关系型数据库
“代码界的魔法师:揭秘Micronaut框架下如何用测试驱动开发将简单图书管理系统变成性能怪兽!
【9月更文挑战第6天】Micronaut框架凭借其轻量级和高性能特性,在Java应用开发中备受青睐。本文通过一个图书管理系统的案例,介绍了在Micronaut下从单元测试到集成测试的全流程。首先,我们使用`@MicronautTest`注解编写了一个简单的`BookService`单元测试,验证添加图书功能;接着,通过集成测试验证了`BookService`与数据库的交互。整个过程展示了Micronaut强大的依赖注入和测试支持,使测试编写变得更加高效和简单。
24 4
|
10天前
|
IDE 测试技术 持续交付
Python自动化测试与单元测试框架:提升代码质量与效率
【9月更文挑战第3天】随着软件行业的迅速发展,代码质量和开发效率变得至关重要。本文探讨了Python在自动化及单元测试中的应用,介绍了Selenium、Appium、pytest等自动化测试框架,以及Python标准库中的unittest单元测试框架。通过详细阐述各框架的特点与使用方法,本文旨在帮助开发者掌握编写高效测试用例的技巧,提升代码质量与开发效率。同时,文章还提出了制定测试计划、持续集成与测试等实践建议,助力项目成功。
35 5
|
11天前
|
测试技术 C# 图形学
掌握Unity调试与测试的终极指南:从内置调试工具到自动化测试框架,全方位保障游戏品质不踩坑,打造流畅游戏体验的必备技能大揭秘!
【9月更文挑战第1天】在开发游戏时,Unity 引擎让创意变为现实。但软件开发中难免遇到 Bug,若不解决,将严重影响用户体验。调试与测试成为确保游戏质量的最后一道防线。本文介绍如何利用 Unity 的调试工具高效排查问题,并通过 Profiler 分析性能瓶颈。此外,Unity Test Framework 支持自动化测试,提高开发效率。结合单元测试与集成测试,确保游戏逻辑正确无误。对于在线游戏,还需进行压力测试以验证服务器稳定性。总之,调试与测试贯穿游戏开发全流程,确保最终作品既好玩又稳定。
38 4
|
12天前
|
测试技术 API 开发者
Python 魔法:打造你的第一个天气查询小工具自动化测试框架的构建与实践
【8月更文挑战第31天】在这篇文章中,我们将一起踏上编程的奇妙旅程。想象一下,只需几行代码,就能让计算机告诉你明天是否要带伞。是的,你没有听错,我们将用Python这把钥匙,解锁天气预报的秘密。不论你是编程新手还是想拓展技能的老手,这篇文章都会为你带来新的视角和灵感。所以,拿起你的键盘,让我们一起创造属于自己的天气小工具吧!
|
4天前
|
消息中间件 NoSQL Go
PHP转Go系列 | ThinkPHP与Gin框架之Redis延时消息队列技术实践
【9月更文挑战第7天】在从 PHP 的 ThinkPHP 框架迁移到 Go 的 Gin 框架时,涉及 Redis 延时消息队列的技术实践主要包括:理解延时消息队列概念,其能在特定时间处理消息,适用于定时任务等场景;在 ThinkPHP 中使用 Redis 实现延时队列;在 Gin 中结合 Go 的 Redis 客户端库实现类似功能;Go 具有更高性能和简洁性,适合处理大量消息。迁移过程中需考虑业务需求及系统稳定性。
|
12天前
|
测试技术 C# 开发者
“代码守护者:详解WPF开发中的单元测试策略与实践——从选择测试框架到编写模拟对象,全方位保障你的应用程序质量”
【8月更文挑战第31天】单元测试是确保软件质量的关键实践,尤其在复杂的WPF应用中更为重要。通过为每个小模块编写独立测试用例,可以验证代码的功能正确性并在早期发现错误。本文将介绍如何在WPF项目中引入单元测试,并通过具体示例演示其实施过程。首先选择合适的测试框架如NUnit或xUnit.net,并利用Moq模拟框架隔离外部依赖。接着,通过一个简单的WPF应用程序示例,展示如何模拟`IUserRepository`接口并验证`MainViewModel`加载用户数据的正确性。这有助于确保代码质量和未来的重构与扩展。
22 0
|
12天前
|
测试技术 Java Spring
Spring 框架中的测试之道:揭秘单元测试与集成测试的双重保障,你的应用真的安全了吗?
【8月更文挑战第31天】本文以问答形式深入探讨了Spring框架中的测试策略,包括单元测试与集成测试的有效编写方法,及其对提升代码质量和可靠性的重要性。通过具体示例,展示了如何使用`@MockBean`、`@SpringBootTest`等注解来进行服务和控制器的测试,同时介绍了Spring Boot提供的测试工具,如`@DataJpaTest`,以简化数据库测试流程。合理运用这些测试策略和工具,将助力开发者构建更为稳健的软件系统。
21 0
|
12天前
|
API 开发者 Java
API 版本控制不再难!Spring 框架带你玩转多样化的版本管理策略,轻松应对升级挑战!
【8月更文挑战第31天】在开发RESTful服务时,为解决向后兼容性问题,常需进行API版本控制。本文以Spring框架为例,探讨四种版本控制策略:URL版本控制、请求头版本控制、查询参数版本控制及媒体类型版本控制,并提供示例代码。此外,还介绍了通过自定义注解与过滤器实现更灵活的版本控制方案,帮助开发者根据项目需求选择最适合的方法,确保API演化的管理和客户端使用的稳定与兼容。
44 0
|
12天前
|
测试技术 持续交付 开发者
Xamarin 高效移动应用测试最佳实践大揭秘,从框架选择到持续集成,让你的应用质量无敌!
【8月更文挑战第31天】竞争激烈的移动应用市场,Xamarin 作为一款优秀的跨平台开发工具,提供了包括单元测试、集成测试及 UI 测试在内的全面测试方案。借助 Xamarin.UITest 框架,开发者能便捷地用 C# 编写测试案例,如登录功能测试;通过 Xamarin 模拟框架,则可在无需真实设备的情况下模拟各种环境测试应用表现;Xamarin.TestCloud 则支持在真实设备上执行自动化测试,确保应用兼容性。结合持续集成与部署策略,进一步提升测试效率与应用质量。掌握 Xamarin 的测试最佳实践,对确保应用稳定性和优化用户体验至关重要。
25 0
|
12天前
|
jenkins 测试技术 持续交付
自动化测试框架的构建与实践
【8月更文挑战第31天】本文将引导你了解自动化测试框架的重要性,并深入浅出地展示如何搭建和实施一个有效的自动化测试环境。通过实际案例,我们将一起探索自动化测试的“魔力”,解锁软件质量保障的新技能。

相关产品

  • 云数据库 Tair(兼容 Redis)