Files
doris/docs/zh-CN/community/developer-guide/regression-testing.md
jiafeng.zhang 031fba4259 [typo](fix)Fix community documentation link errors (#11758)
Fix community documentation link errors
2022-08-13 21:24:54 +08:00

21 KiB

title, language
title language
回归测试 zh-CN

回归测试

概念

  1. Suite: 一个测试用例,目前仅用来指代测试用例文件名
  2. Group: 一个测试集,目前仅用于指代测试用例所属的目录
  3. Action: 一个封装好的具体测试行为,比如用于执行sql的sql Action,用于校验结果的test Action,用于导入数据的streamLoad Action等

测试步骤

  1. 需要预先安装好集群
  2. 修改配置文件${DORIS_HOME}/regression-test/conf/regression-conf.groovy,设置jdbc url、用户等配置项
  3. 创建测试用例文件并编写用例
  4. 如果用例文件包含qt Action,则需要创建关联的data文件,比如suites/demo/qt_action.groovy这个例子,需要用到data/demo/qt_action.out这个TSV文件来校验输出是否一致
  5. 运行${DORIS_HOME}/run-regression-test.sh测试全部用例,或运行${DORIS_HOME}/run-regression-test.sh --run <suiteName> 测试若干用例,更多例子见"启动脚本例子"章节

目录结构

开发时需要关注的重要文件/目录

  1. run-regression-test.sh: 启动脚本
  2. regression-conf.groovy: 回归测试的默认配置
  3. data: 存放输入数据和输出校验数据
  4. suites: 存放用例
./${DORIS_HOME}
    |-- **run-regression-test.sh**           回归测试启动脚本
    |-- regression-test
    |   |-- plugins                          插件目录
    |   |-- conf
    |   |   |-- logback.xml                  日志配置文件
    |   |   |-- **regression-conf.groovy**   默认配置文件
    |   |
    |   |-- framework                        回归测试框架源码
    |   |-- **data**                         用例的输入输出文件
    |   |   |-- demo                         存放demo的输入输出文件
    |   |   |-- correctness                  存放正确性测试用例的输入输出文件
    |   |   |-- performance                  存放性能测试用例的输入输出文件
    |   |   |-- utils                        存放其他工具的输入输出文件
    |   |
    |   |-- **suites**                       回归测试用例
    |       |-- demo                         存放测试用例的demo
    |       |-- correctness                  存放正确性测试用例
    |       |-- performance                  存放性能测试用例
    |       |-- utils                        其他工具
    |
    |-- output
        |-- regression-test
            |-- log                          回归测试日志

框架默认配置

测试时需要实际情况修改jdbc和fe的配置


/* ============ 一般只需要关注下面这部分 ============ */
// 默认DB,如果未创建,则会尝试创建这个db
defaultDb = "regression_test"

// Jdbc配置
jdbcUrl = "jdbc:mysql://127.0.0.1:9030/?"
jdbcUser = "root"
jdbcPassword = ""

// fe地址配置, 用于stream load
feHttpAddress = "127.0.0.1:8030"
feHttpUser = "root"
feHttpPassword = ""

/* ============ 一般不需要修改下面的部分 ============ */

// DORIS_HOME变量是通过run-regression-test.sh传入的
// 即 java -DDORIS_HOME=./

// 设置回归测试用例的目录
suitePath = "${DORIS_HOME}/regression-test/suites"
// 设置输入输出数据的目录
dataPath = "${DORIS_HOME}/regression-test/data"
// 设置插件的目录
pluginPath = "${DORIS_HOME}/regression-test/plugins"

// 默认会读所有的组,读多个组可以用半角逗号隔开,如: "demo,performance"
// 一般不需要在配置文件中修改,而是通过run-regression-test.sh --run -g来动态指定和覆盖
testGroups = ""
// 默认会读所有的用例, 同样可以使用run-regression-test.sh --run -s来动态指定和覆盖
testSuites = ""
// 默认会加载的用例目录, 可以通过run-regression-test.sh --run -d来动态指定和覆盖
testDirectories = ""

// 排除这些组的用例,可通过run-regression-test.sh --run -xg来动态指定和覆盖
excludeGroups = ""
// 排除这些suite,可通过run-regression-test.sh --run -xs来动态指定和覆盖
excludeSuites = ""
// 排除这些目录,可通过run-regression-test.sh --run -xd来动态指定和覆盖
excludeDirectories = ""

// 其他自定义配置
customConf1 = "test_custom_conf_value"

编写用例的步骤

  1. 进入${DORIS_HOME}/regression-test目录
  2. 根据测试的目的来选择用例的目录,正确性测试存在suites/correctness,而性能测试存在suites/performance
  3. 新建一个groovy用例文件,增加若干Action用于测试,Action将在后续章节具体说明

Action

Action是一个测试框架默认提供的测试行为,使用DSL来定义。

sql action

sql action用于提交sql并获取结果,如果查询失败则会抛出异常

参数如下

  • String sql: 输入的sql字符串
  • return List<List<Object>>: 查询结果,如果是DDL/DML,则返回一行一列,唯一的值是updateRowCount

下面的样例代码存放于${DORIS_HOME}/regression-test/suites/demo/sql_action.groovy:

suite("sql_action", "demo") {
    // execute sql and ignore result
    sql "show databases"

    // execute sql and get result, outer List denote rows, inner List denote columns in a single row
    List<List<Object>> tables = sql "show tables"

    // assertXxx() will invoke junit5's Assertions.assertXxx() dynamically
    assertTrue(tables.size() >= 0) // test rowCount >= 0

    // syntax error
    try {
        sql "a b c d e"
        throw new IllegalStateException("Should be syntax error")
    } catch (java.sql.SQLException t) {
        assertTrue(true)
    }

    def testTable = "test_sql_action1"

    try {
        sql "DROP TABLE IF EXISTS ${testTable}"

        // multi-line sql
        def result1 = sql """
                        CREATE TABLE IF NOT EXISTS ${testTable} (
                            id int
                        )
                        DISTRIBUTED BY HASH(id) BUCKETS 1
                        PROPERTIES (
                          "replication_num" = "1"
                        ) 
                        """

        // DDL/DML return 1 row and 1 column, the only value is update row count
        assertTrue(result1.size() == 1)
        assertTrue(result1[0].size() == 1)
        assertTrue(result1[0][0] == 0, "Create table should update 0 rows")

        def result2 = sql "INSERT INTO test_sql_action1 values(1), (2), (3)"
        assertTrue(result2.size() == 1)
        assertTrue(result2[0].size() == 1)
        assertTrue(result2[0][0] == 3, "Insert should update 3 rows")
    } finally {
        /**
         * try_xxx(args) means:
         *
         * try {
         *    return xxx(args)
         * } catch (Throwable t) {
         *     // do nothing
         *     return null
         * }
         */
        try_sql("DROP TABLE IF EXISTS ${testTable}")

        // you can see the error sql will not throw exception and return
        try {
            def errorSqlResult = try_sql("a b c d e f g")
            assertTrue(errorSqlResult == null)
        } catch (Throwable t) {
            assertTrue(false, "Never catch exception")
        }
    }

    // order_sql(sqlStr) equals to sql(sqlStr, isOrder=true)
    // sort result by string dict
    def list = order_sql """
                select 2
                union all
                select 1
                union all
                select null
                union all
                select 15
                union all
                select 3
                """

    assertEquals(null, list[0][0])
    assertEquals(1, list[1][0])
    assertEquals(15, list[2][0])
    assertEquals(2, list[3][0])
    assertEquals(3, list[4][0])
}

qt action

qt action用于提交sql,并使用对应的.out TSV文件来校验结果

  • String sql: 输入sql字符串
  • return void

下面的样例代码存放于${DORIS_HOME}/regression-test/suites/demo/qt_action.groovy:

suite("qt_action", "demo") {
    /**
     * qt_xxx sql equals to quickTest(xxx, sql) witch xxx is tag.
     * the result will be compare to the relate file: ${DORIS_HOME}/regression_test/data/qt_action.out.
     *
     * if you want to generate .out tsv file for real execute result. you can run with -genOut or -forceGenOut option.
     * e.g
     *   ${DORIS_HOME}/run-regression-test.sh --run qt_action -genOut
     *   ${DORIS_HOME}/run-regression-test.sh --run qt_action -forceGenOut
     */
    qt_select "select 1, 'beijing' union all select 2, 'shanghai'"

    qt_select2 "select 2"

    // order result by string dict then compare to .out file.
    // order_qt_xxx sql equals to quickTest(xxx, sql, true).
    order_qt_union_all  """
                select 2
                union all
                select 1
                union all
                select null
                union all
                select 15
                union all
                select 3
                """
}

test action

test action可以使用更复杂的校验规则来测试,比如验证行数、执行时间、是否抛出异常

可用参数

  • String sql: 输入的sql字符串
  • List<List<Object>> result: 提供一个List对象,用于比较真实查询结果与List对象是否相等
  • Iterator<Object> resultIterator: 提供一个Iterator对象,用于比较真实查询结果与Iterator是否相等
  • String resultFile: 提供一个文件Uri(可以是本地文件相对路径,或http(s)路径),用于比较真实查询结果与http响应流是否相等,格式与.out文件格式类似,但没有块头和注释
  • String exception: 校验抛出的异常是否包含某些字符串
  • long rowNum: 验证结果行数
  • long time: 验证执行时间是否小于这个值,单位是毫秒
  • Closure<List<List<Object>>, Throwable, Long, Long> check: 自定义回调校验,可传入结果、异常、时间。存在回调函数时,其他校验方式会失效。

下面的样例代码存放于${DORIS_HOME}/regression-test/suites/demo/test_action.groovy:

suite("test_action", "demo") {
    test {
        sql "abcdefg"
        // check exception message contains
        exception "errCode = 2, detailMessage = Syntax error"
    }

    test {
        sql """
            select *
            from (
                select 1 id
                union all
                select 2
            ) a
            order by id"""

        // multi check condition

        // check return 2 rows
        rowNum 2
        // execute time must <= 5000 millisecond
        time 5000
        // check result, must be 2 rows and 1 column, the first row is 1, second is 2
        result(
            [[1], [2]]
        )
    }

    test {
        sql "a b c d e f g"

        // other check will not work because already declared a check callback
        exception "aaaaaaaaa"

        // callback
        check { result, exception, startTime, endTime ->
            // assertXxx() will invoke junit5's Assertions.assertXxx() dynamically
            assertTrue(exception != null)
        }
    }

    test {
        sql  """
                select 2
                union all
                select 1
                union all
                select null
                union all
                select 15
                union all
                select 3
                """

        check { result, ex, startTime, endTime ->
            // same as order_sql(sqlStr)
            result = sortRows(result)

            assertEquals(null, result[0][0])
            assertEquals(1, result[1][0])
            assertEquals(15, result[2][0])
            assertEquals(2, result[3][0])
            assertEquals(3, result[4][0])
        }
    }

    // execute sql and order query result, then compare to iterator
    def selectValues = [1, 2, 3, 4]
    test {
        order true
        sql selectUnionAll(selectValues)
        resultIterator(selectValues.iterator())
    }

    // compare to data/demo/test_action.csv
    test {
        order true
        sql selectUnionAll(selectValues)

        // you can set to http://xxx or https://xxx
        // and compare to http response body
        resultFile "test_action.csv"
    }
}

explain action

explain action用来校验explain返回的字符串是否包含某些字符串

可用参数:

  • String sql: 查询的sql,需要去掉sql中的explain
  • String contains: 校验explain是否包含某些字符串,可多次调用校验同时多个结果
  • String notContains: 校验explain是否不含某些字符串,可多次调用校验同时多个结果
  • Closure<String> check: 自定义校验回调函数,可以获取返回的字符串,存在校验函数时,其他校验方式会失效
  • Closure<String, Throwable, Long, Long> check: 自定义校验回调函数,可以额外获取异常和时间

下面的样例代码存放于${DORIS_HOME}/regression-test/suites/demo/explain_action.groovy:

suite("explain_action", "demo") {
    explain {
        sql("select 100")

        // contains("OUTPUT EXPRS:<slot 0> 100\n") && contains("PARTITION: UNPARTITIONED\n")
        contains "OUTPUT EXPRS:<slot 0> 100\n"
        contains "PARTITION: UNPARTITIONED\n"
    }

    explain {
        sql("select 100")

        // contains(" 100\n") && !contains("abcdefg") && !("1234567")
        contains " 100\n"
        notContains "abcdefg"
        notContains "1234567"
    }

    explain {
        sql("select 100")
        // simple callback
        check { explainStr -> explainStr.contains("abcdefg") || explainStr.contains(" 100\n") }
    }

    explain {
        sql("a b c d e")
        // callback with exception and time
        check { explainStr, exception, startTime, endTime ->
            // assertXxx() will invoke junit5's Assertions.assertXxx() dynamically
            assertTrue(exception != null)
        }
    }
}

streamLoad action

streamLoad action用于导入数据 可用参数为

  • String db: db,默认值为regression-conf.groovy中的defaultDb
  • String table: 表名
  • String file: 要导入的文件路径,可以写data目录下的相对路径,或者写http url来导入网络文件
  • Iterator<List<Object>> inputIterator: 要导入的迭代器
  • String inputText: 要导入的文本, 较为少用
  • InputStream inputStream: 要导入的字节流,较为少用
  • long time: 验证执行时间是否小于这个值,单位是毫秒
  • void set(String key, String value): 设置stream load的http请求的header,如label、columnSeparator
  • Closure<String, Throwable, Long, Long> check: 自定义校验回调函数,可以获取返回结果、异常和超时时间。当存在回调函数时,其他校验项会失效。

下面的样例代码存放于${DORIS_HOME}/regression-test/suites/demo/streamLoad_action.groovy:

suite("streamLoad_action", "demo") {

    def tableName = "test_streamload_action1"

    sql """
            CREATE TABLE IF NOT EXISTS ${tableName} (
                id int,
                name varchar(255)
            )
            DISTRIBUTED BY HASH(id) BUCKETS 1
            PROPERTIES (
              "replication_num" = "1"
            ) 
        """

    streamLoad {
        // you can skip declare db, because a default db already specify in ${DORIS_HOME}/conf/regression-conf.groovy
        // db 'regression_test'
        table tableName

        // default label is UUID:
        // set 'label' UUID.randomUUID().toString()

        // default column_separator is specify in doris fe config, usually is '\t'.
        // this line change to ','
        set 'column_separator', ','

        // relate to ${DORIS_HOME}/regression-test/data/demo/streamload_input.csv.
        // also, you can stream load a http stream, e.g. http://xxx/some.csv
        file 'streamload_input.csv'

        time 10000 // limit inflight 10s

        // stream load action will check result, include Success status, and NumberTotalRows == NumberLoadedRows
    }


    // stream load 100 rows
    def rowCount = 100
    // range: [0, rowCount)
    // or rangeClosed: [0, rowCount]
    def rowIt = range(0, rowCount)
            .mapToObj({i -> [i, "a_" + i]}) // change Long to List<Long, String>
            .iterator()

    streamLoad {
        table tableName
        // also, you can upload a memory iterator
        inputIterator rowIt

        // if declared a check callback, the default check condition will ignore.
        // So you must check all condition
        check { result, exception, startTime, endTime ->
            if (exception != null) {
                throw exception
            }
            log.info("Stream load result: ${result}".toString())
            def json = parseJson(result)
            assertEquals("success", json.Status.toLowerCase())
            assertEquals(json.NumberTotalRows, json.NumberLoadedRows)
            assertTrue(json.NumberLoadedRows > 0 && json.LoadBytes > 0)
        }
    }
}

其他Action

thread, lazyCheck, events, connect, selectUnionAll 具体可以在这个目录找到例子: ${DORIS_HOME}/regression-test/suites/demo

启动脚本例子

# 查看脚本参数说明
./run-regression-test.sh h

# 查看框架参数说明
./run-regression-test.sh --run -h

# 测试所有用例
./run-regression-test.sh 

# 删除测试框架编译结果和测试日志
./run-regression-test.sh --clean

# 测试suiteName为sql_action的用例, 目前suiteName等于文件名前缀,例子对应的用例文件是sql_action.groovy
./run-regression-test.sh --run sql_action 

# 测试suiteName包含'sql'的用例,**注意需要用单引号括起来**
./run-regression-test.sh --run '*sql*' 

# 测试demo和perfomance group
./run-regression-test.sh --run -g 'demo,performance'

# 测试demo group下的sql_action
./run-regression-test.sh --run -g demo -s sql_action

# 测试demo目录下的sql_action
./run-regression-test.sh --run -d demo -s sql_action

# 测试demo目录下用例,排除sql_action用例
./run-regression-test.sh --run -d demo -xs sql_action

# 排除demo目录的用例
./run-regression-test.sh --run -xd demo

# 排除demo group的用例
./run-regression-test.sh --run -xg demo

# 自定义配置
./run-regression-test.sh --run -conf a=b

# 并发执行
./run-regression-test.sh --run -parallel 5 -suiteParallel 10 -actionParallel 20

使用查询结果自动生成.out文件

# 使用查询结果自动生成sql_action用例的.out文件,如果.out文件存在则忽略
./run-regression-test.sh --run sql_action -genOut

# 使用查询结果自动生成sql_action用例的.out文件,如果.out文件存在则覆盖
./run-regression-test.sh --run sql_action -forceGenOut

Suite插件

有的时候我们需要拓展Suite类,但不便于修改Suite类的源码,则可以通过插件来进行拓展。默认插件目录为${DORIS_HOME}/regression-test/plugins,在其中可以通过groovy脚本定义拓展方法,以plugin_example.groovy为例,为Suite类增加了testPlugin函数用于打印日志:

import org.apache.doris.regression.suite.Suite

// register `testPlugin` function to Suite,
// and invoke in ${DORIS_HOME}/regression-test/suites/demo/test_plugin.groovy
Suite.metaClass.testPlugin = { String info /* param */ ->

    // which suite invoke current function?
    Suite suite = delegate as Suite

    // function body
    suite.getLogger().info("Test plugin: suiteName: ${suite.name}, info: ${info}".toString())

    // optional return value
    return "OK"
}

logger.info("Added 'testPlugin' function to Suite")

增加了testPlugin函数后,则可以在普通用例中使用它,以${DORIS_HOME}/regression-test/suites/demo/test_plugin.groovy为例:

suite("test_plugin", "demo") {
    // register testPlugin function in ${DORIS_HOME}/regression-test/plugins/plugin_example.groovy
    def result = testPlugin("message from suite")
    assertEquals("OK", result)
}

CI/CD的支持

TeamCity

TeamCity可以通过stdout识别Service Message。当使用--teamcity参数启动回归测试框架时,回归测试框架就会在stdout打印TeamCity Service Message,TeamCity将会自动读取stdout中的事件日志,并在当前流水线中展示Tests,其中会展示测试的test及其日志。 因此只需要配置下面一行启动回归测试框架的命令即可。其中-Dteamcity.enableStdErr=false可以让错误日志也打印到stdout中,方便按时间顺序分析日志。

JAVA_OPTS="-Dteamcity.enableStdErr=${enableStdErr}" ./run-regression-test.sh --teamcity --run