鏈路性能測試中參數多樣性方法分享


在之前的寫過單鏈路性能測試實踐中,部分接口的參數雖然有依賴性,但是由於部分接口是多次請求的,所以有一部分接口的參數比較隨意,因爲除了響應結果的基礎驗證之外沒有任何其他校驗部分。例如下面代碼片段:

    JSONObject response = clazz.recommend(ks.id, ks.type, ks.level)
    clazz.recommend(ks.id, ks.type, ks.level)                clazz.recommend(ks.id, ks.type, ks.level)

這裏的clazz.recommend方法就被調用了三次,三次的請求參數都是一樣的,但接下來的邏輯處理只用到了某一次(這裏用了第一次)請求的結果,提取參數作爲下一次請求的參數。

下面分享幾種我工作中常用到的增加參數多樣性的方法。

業務無關量

這個是最常用到的,當部分的接口參數對接下來的用例沒有實際影響的時候我通常會用一個隨機的數字和字符串來設置參數。

隨機數字

這個相對簡單,我是Java,分享一下簡單的代碼:

    /**
     * 獲取隨機數,獲取1~num 的數字,包含 num
     *
     * @param num 隨機數上限
     * @return 隨機數
     */

    public static int getRandomInt(int num) {
        return random.get().nextInt(num) + 1;
    }

    /**
     * 隨機範圍int,取頭不取尾
     *
     * @param start
     * @param end
     * @return
     */

    public static int getRandomIntRange(int start, int end) {
        if (end <= start) return TEST_ERROR_CODE;
        return random.get().nextInt(end - start) + start;
    }

線程安全隨機

在做性能測試的時候,我經常會有在不同線程隨機出某個唯一的量的需求,這裏我一般採用兩段組成:多線程唯一量線程內唯一量

舉個例子來講:

/**
 * 用於非單純的http請求以及非HTTP請求,沒有httprequestbase對象的標記方法,自己實現的虛擬類,可用戶標記header固定字段或者隨機參數,使用T作爲參數載體,目前只能使用在T爲string類纔行
 */

public class ParamMark extends SourceCode implements MarkThreadCloneableSerializable {

    private static final long serialVersionUID = -5532592151245141262L;

    public static AtomicInteger threadName = new AtomicInteger(getRandomIntRange(10009000));

    /**
     * 用於標記執行線程
     */

    String name;

    int num = getRandomIntRange(100999) * 1000;

    @Override
    public String mark(ThreadBase threadBase) {
        return name + num++;
    }

    @Override
    public ParamMark clone() {
        ParamMark paramMark = new ParamMark();
        return paramMark;
    }

    public ParamMark() {
        this.name = threadName.getAndIncrement() + EMPTY;
    }

    public ParamMark(String name) {
        this();
        this.name = name;
    }


}

這裏我用了一個全局的靜態變量threadName作爲一個基礎值,之所以這裏也隨機是想讓每次運行的時候儘量都不一樣,沒有使用時間戳是因爲時間戳太長了,現在這個比較滿足需求。

下面每個對象創建的時候調用的構造方法:

    public ParamMark() {
        this.name = threadName.getAndIncrement() + EMPTY;
    }

這裏就可以保證每一個線程拿到的值都是不一樣的,當然這個功能還可以通過ThreadLocal在鏈路性能測試中實踐中提到的方法解決,這裏就不多說了。

    @Override
    public String mark(ThreadBase threadBase) {
        return name + num++;
    }

這裏我就可以通過num++獲取一個線程內的唯一數字,然後和name組合成爲一個全局唯一的量。

隨機字符串

這個好像沒有特別大的需求量,之前寫過一個StringUtil的工具類來完成,一般爲了生成一個固定長度的隨機字符串,我都是調用一個方法:

    /**
     * 獲取隨機字符串
     * @param i
     * @return
     */

    static String getString(int i) {
        def re = new StringBuffer()
        if (i < 1return re
        for (int j in 1..i) 
{
            re.append(getChar())
        }
        re.toString()
    }

或者:

    /**
     * 獲取隨機字符串,沒有數字
     * @param i
     * @return
     */

    static String getStringWithoutNum(int i) {
        def re = new StringBuffer()
        if (i < 1return re
        for (int j in 1..i) 
{
            re << getWord()
        }
        re.toString()
    }

同樣這裏也有線程安全的問題,可以採用上面的多線程唯一量線程內唯一量的方式解決。如果是的唯一性要求嚴格的話,我一般採用時間戳的形式。這樣做比較方便,如果毫秒的時間錯不足以滿足需求,可以採用納秒的時間戳,自測完全沒問題。

    /**
     * 獲取納秒的時間標記
     *
     * @return
     */

    public static long getNanoMark() {
        return System.nanoTime();
    }

業務相關量

這裏分享一下,業務相關的參數相關的多樣性問題我的解決方案。還是以之前的文章單鏈路性能測試實踐中例子。

隨機相關量

這個主要場景指的是有指定的隨機範圍,比如說某個接口數值型參數的範圍是0-7,那麼我們就可以通過隨機這個參數來豐富該接口的請求參數。

有的接口幾個參數是關聯性的,我們就需要從一個List中隨機或者是數組中隨機出一個對象,FunTester通常會把多個關聯參數封裝成一個對象,例如:

    private static class K extends AbstractBean {

        int id

        int type

        int level

        K(int id, int type, int level) 
{
            this.id = id
            this.type = type
            this.level = level
        }
    }

這樣我們就可以通過將可用的參數放到一個列表中獲取,此方法跟下面的造數據有些區別,一個取是訪問一下,一個取是取走了,各位可以待會對比一下。

分享一些代碼片段:


        params.put("pullnew", random(123));
        params.put("class_id", random(Common.CLASSES));

其中public static final Integer[] CLASSES = property.getIntArray("classes");

實現方法:

    /**
     * 隨機選擇某一個值
     *
     * @param fs
     * @param <F>
     * @return
     */

    public static <F extends Number> random(F... fs) {
        return fs[getRandomInt(fs.length) - 1];
    }

    /**
     * 隨機選擇某一個字符串
     *
     * @param fs
     * @return
     */

    public static String random(String... fs) {
        if (ArrayUtils.isEmpty(fs)) ParamException.fail("數組不能爲空!");
        return fs[getRandomInt(fs.length) - 1];
    }

    /**
     * 隨機選擇某一個對象
     *
     * @param list
     * @param <F>
     * @return
     */

    public static <F extends Object> random(List<F> list) {
        if (list == null || list.isEmpty()) ParamException.fail("數組不能爲空!");
        return list.get(getRandomInt(list.size()) - 1);
    }
  • 如果是 Groovy來寫,可以用 0..10來選擇隨機數字範圍。

上游接口獲取

例如JSONObject response = clazz.recommend(ks.id, ks.type, ks.level)這個接口請求中,用到的K對象是從上游接口def klist = mirro.getKList()中獲取的,所有可用的K對象都在接口中返回。

如果想豐富clazz.recommend()方法的請求參數的話,一定要將mirro.getKList()中的響應結果解析成List<K>的形式。

因爲多層結構,我只解析了第一層的。

            def karray = klist.getJSONArray("data")
            def kss = []
            karray.each {
                JSONObject parse = JSON.parse(JSON.toJSONString(it))
                def level = parse.getIntValue("node_level")
                def type = parse.getIntValue("ktype")
                def id = parse.getIntValue("id")
                kss << new K(id, type, level)
            }

這裏我沒有提前將所以可用的參數都存起來,原因有二:用戶不會這麼做;數據可能會變動。在無數據驅動自動化測試文中我提到無數據驅動,這就是個例子,不給用例輸入除賬號密碼外的數據。

            K ks = kss.get(0)
            K ks2 = kss.get(1)
            K ks3 = kss.get(3)
            clazz.recommend(ks3.id, ks3.type, ks3.level)
            clazz.recommend(ks2.id, ks2.type, ks2.level)
            JSONObject response = clazz.recommend(ks.id, ks.type, ks.level)

上游接口造數據

這個比如收藏和取消收藏接口,正常來講,首頁可以收藏和取消收藏。在獲取個人列表頁可以取消收藏。這裏我設置了先收藏,再去個人列表頁,然後取消收藏。

這樣既保證正常業務流程下,最後取消接口的是有足夠的可用接口的。

            JSONObject response = clazz.recommend(ks.id, ks.type, ks.level)
            def minis = []
            int i = 0
            response.getJSONArray("data").each {
                if (i++ < 2) {
                    JSONObject parse = JSON.parse(JSON.toJSONString(it))
                    int value = parse.getIntValue("minicourse_id")
                    clazz.collect(value, ks.type, ks.id)
                }
            }
            mirro.getMiniCourseListV3(ks.id, ks.type, 0, ks.level)
            def res = mirro.getMiniCourseListV3(ks.id, ks.type, 0, ks.level)
            res.getJSONObject("data").getJSONArray("minicourse_list").each {
                def ss = JSON.parseObject(JSON.toJSONString(it))
                def mid = ss.getIntValue("minicourse_id")
                clazz.unCollect(mid, ks.type, ks.id)
            }

提前造數據

這個就更常用了,有些時候我們在性能測試的時候,必須要進行前期大量測試數據的構造工作。關於這個話題,很多文章都寫過了。我這裏分享一下在多線程條件下,如何保證每個線程拿到參數唯一性的方法。

在之前的文章講過如何對消息隊列做性能測試,我先講構造的數據通過配置文件(這裏可以臨時從數據庫中查)讀取到一個線程安全的LinkedBlockingQueue中,然後每個線程每次獲取都取走一個對象,這樣就可以滿足需求了。

    static LinkedBlockingQueue<String> sqls = new LinkedBlockingQueue<>();

存儲和獲取的方法:

/**
     * 添加存儲任務,數據庫存儲服務用
     *
     * @param sql
     * @return
     */

    public static boolean addWork(String sql) {
        try {
            sqls.put(sql);
        } catch (InterruptedException e) {
            logger.warn("添加數據庫存儲任務失敗!", e);
            return false;
        }
        return true;
    }

    /**
     * 從任務池裏面獲取任務
     *
     * @return
     */

    static String getWork() {
        String sql = null;
        try {
            sql = sqls.poll(SqlConstant.MYSQLWORK_TIMEOUT, TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            logger.warn("獲取存儲任務失敗!", e);
        } finally {
            return sql;
        }
    }

這裏需要提前預估消耗量,不然容易因爲數據量準備不足導致測試失敗。當然,也可以單獨寫一個線程,不斷往隊列中添加數據以保障測試用例順利執行。

題外話

在一些測試場景中,有時候上面的幾種方式都不能很好地滿足我們的需求的話,還有一些我覺得可能會比較耗時。這個時候我們就需要一些非常規的手段。請參考可測性經驗分享


FunTester騰訊雲社區欽定年度作者,非著名測試開發er,歡迎關注。

點擊閱讀原文,查看公衆號歷史文章
- END -

本文分享自微信公衆號 - FunTester(NuclearTester)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章