JB的測試之旅-測試資料的準備/構造

jb發表於2019-02-20

前言

之前看過相關的測試資料準備的文章,坦白說,看完之後,能記住只有2個:

  • api準備資料;
  • 資料庫插入;

而最近,同學恰好也問到這問題:

image.png-36.3kB

當時回覆就如上面的答覆,現在回頭想想,的確沒想到好的方案,在腦海裡,有一個所謂的"終極方案",就是讀取介面文件,自動生成測試資料,理論上可行,但一直沒去做,懶;

image.png-89.4kB

有啥辦法

做過單元/介面測試的同學都知道,其中有一個環節就是測試資料準備,而這一步是不可或缺的一步,也是需要花費大量時間投入的一步;

測試介面前就必須準備好該介面需要處理的資料,而資料又有可能依賴其他的資料,這就提高了準備資料的複雜度與難度;

那到底有什麼辦法?

  • 基於GUI操作生成測試資料;
  • 基於API呼叫生成測試資料;
  • 基於資料庫操作生成資料;
  • 基於第三方庫自建資料;
  • 結合多種方式生成資料;
  • 匯入線上/測試資料;

GUI操作生成資料

基於GUI操作生成資料,是指使用自動化指令碼或者人工執行業務流程生成資料。

現在需要測試登入功能,這就需要準備一個已經註冊的使用者,
此時,可以通過GUI操作來建立一個使用者(無論是手工還是自動化指令碼),
然後再用新建的使用者測試登入;
複製程式碼

這種方式簡單直接,並且資料來源於真實的業務流程,一定程度保證了資料的準確性。

然而,缺點也很明顯:

  • 建立資料的效率低:每次的GUI操作只生成一條資料,並且操作非常耗時;
  • 易封裝:通過GUI操作生成資料的過程,其實就是在開發自動化case的過程,加大了工作量;
  • 成功率不高:GUI的變化直接會導致資料生成失敗;
  • 引入了其他依賴:資料生成成功的前提,依賴於業務流程的正確性。

一般情況下,基本不會使用這種方式生成資料,除非沒有其他更好的方式來建立可靠的資料。

不過,操作GUI生成資料是其他兩種方式API呼叫運算元據庫的基礎,因為可以知道一條測試資料建立的過程;

API呼叫生成資料

實際上使用GUI操作生成資料,本質上就是在呼叫API

使用GUI介面註冊使用者時,實際上呼叫了createUser的API。
複製程式碼

要注意的是,一次GUI操作可能呼叫了多個API,一般情況下,都把呼叫API生成資料的過程封裝成資料準備函式

也許會有疑問,到底要怎樣才知道呼叫了哪些api?

  • 直接問開發;
  • 看原始碼;
  • 模擬一遍,抓包;

這種方式優勢在於:

  • 保證資料準確性;
  • 執行效率高;
  • 封裝成函式更靈活可控;

這種方式也不是十全十美,缺點在於:

  • 並不是所有資料建立都有對應的API;
  • 業務很複雜的情況下,需要呼叫多個API,增加複雜性;
  • 需要海量資料時,即使使用了併發,效率也盡如人意;
  • API依賴性;

因此,業界往往還會通過資料庫的CRUD操作生成測試資料;

資料庫操作生成資料

資料庫生成資料一般做法是,將建立資料需要的SQL封裝成函式,然後再進行呼叫

這樣就能直接通過資料庫操作,將測試資料插入系統資料庫。

還是使用者登入,直接往userTable和userRoleTable兩張表插入資料,即可完成註冊。
複製程式碼

這樣做的前提是,需要知道修改了哪些資料庫業務表

這種方式的優勢在於:

  • 效率高,能在短時間內生成批量資料;

缺陷也很明顯:

  • 維護成本高,當涉及到很多張表的時候,封裝的資料準備函式就需要大量時間來維護;
  • 資料容易缺失,一個業務操作設計到的表往往不止一張,容易遺漏;
  • 健壯性差,SQL語句變化時,封裝的函式必須實時同步更新,維護成本很高;

第三方庫生成資料

這種方式就比較直接,直接使用程式碼封裝成函式生成資料。

拿python為例,可以自己結合random()之類的函式隨機生成資料,還可以使用faker這樣的第三方庫:

from faker import Factory

fake = Factory().create('zh_CN')

def random_phone_number():
    '''隨機手機號'''
    return fake.phone_number()

def random_name():
    """隨機姓名"""
    return fake.name()

def random_address():
    """隨機地址"""
    return fake.address()

def random_email():
    """隨機email"""
    return fake.email()
複製程式碼

結合多種方式來生成數

​ 實際上,實際應用中都採用多種方式相結合的方式生成測試資料。

最典型的應用場景是,先通過API呼叫或者第三方庫生成基礎的測試資料,然後使用資料庫的CRUD操作生成符合特殊需求的資料。

比如:

# 註冊新使用者並進行綁卡
1. 使用封裝的faker庫隨機生成姓名,手機號,郵箱等資訊,並呼叫createUser API進行註冊;
2. 查詢userTableb表獲得使用者名稱,然後呼叫bindCard API實現綁卡。
其中,bindCard API中使用的userID即為上一步createUser API中產生的使用者ID;
3. 如有需要,通過資料庫操作更新其他資訊。
複製程式碼

以上就是一個常用的建立測試資料的過程;

當然也可以在測試用例執行前通api建立資料,執行後清除資料的方式;

匯入線上/測試資料

這個就是直匯入線上/測試資料,優點是更加貼近使用者,出現問題,可直接模擬,但一般都不提供這種方式,就不細說了;

資料建立時機

準備測試資料的時候,都有什麼痛點?

  • 耗時長,導致用例執行時間長;
  • 執行測試時可能會出現原先資料被修改而無法複用的情況;
  • 環境不穩定導致資料異常;

正因上面的原因,資料準備不能隨時進行,因為,建立時機很重要;

實時建立

指測試用例時實時建立需要的測試資料,所有資料都必須在測試用例開始前實時準備,比如api方式;

優點:

  • 不依賴測試用例外的資料;
  • 保證資料的準確性和可控性;

缺點:

  • 耗時長;
  • 維護成本高;
  • 資料存在複雜關聯性;
  • 依賴性;

提前建立

指在準備測試環境時就預先將需要的資料提前準備好,比如資料庫插入;

優點:

  • 節省用例執行時間;
  • 不會因為環境問題導致資料無法建立;

缺點:

  • 髒資料;

所謂的髒資料,是指資料在被實際使用前,已經被進行了非預期的修改;

而髒資料可能的來源是:

  • 被其他使用,並修改了狀態;
  • 手工測試時不小心修改了資料;
  • 除錯過程修改了資料;

如何解決:

  • 維護一份資料,執行後復原;
  • 資料分類,不同資料區段來分配使用物件,比如0-100是A團隊,100-200是B團隊,通過流程保證;

該方式不適用於只能一次性使用的場景

如何抉擇

穩定不常變化的資料,或是公用資料,建議使用提前建立的方式(資料庫),一般來說,適用於介面測試環節;

只能一次性使用,或經常變化的資料,又因環境不一致,建議使用實時建立的方式(API);

一般來說,介面測試就是用實時建立的方式,用例執行前構造資料,執行後清除資料,這樣就能儘可能保證用例之間相互不影響,也避免髒資料的產生;
複製程式碼

適用場景

一般來說,介面測試,都用

資料準備的方法

大多數採用的方法

大多數企業採用的方法就是,將測試資料準備的操作封裝成函式

舉個例子:

     def post(self,url,data,code,msg):
         resp= requests.post(readconfig('url', 'url')+url, data=data)
         self.assertEqual(200, resp.status_code)
         self.assertEqual(code, resp.json()['code'])
         self.assertIn(msg, json.dumps(resp.json()['msg']).decode("unicode-escape"))
         return resp
複製程式碼

這樣就可以把資料建立相關操作封裝成函式,業務方只需要直接呼叫函式即可;

但,致命的問題是,引數非常多,也非常複雜;如上面的例子,就需要4個引數,而實際工作,可能會多達十幾個;

而絕大部分情況下,只需要個別引數,其他引數可以使用預設值即可

那樣,程式碼就會演變成這樣:

def xx(A='',B=True,C="xx"):
    ...
    return jbtest(A,B,C)

def xxx():
    ...
    return jbtest(A,B,C)

def xxxx(A=''):
    ...
    return jbtest(A,B,C)
複製程式碼

這樣封裝,對於一些常用的資料組合,可以通過一次呼叫就生成需要的資料;

對於不常用的資料,可以直接調預設的函式來建立,這樣就可以更靈活處理;

但是,也有弊端:

  • 引數越多,封裝的函式數量隨之增加,最終可能演變成上百個函式;
  • 可維護性差,底層函式會影響所有封裝的函式,動一發而牽全身;

大公司怎麼玩

既然上面的方法有問題,那能否優化下?同時,大公司怎麼玩?

想想老東家,談不上是小公司,但基本也是用上面的方式,前段時間看了茹炳晟老師也有提及到這點,就是引入Builder Pattern封裝方式;

Builder Pattern

基本概念

到底什麼是Builder Pattern,翻譯過來是建造者模式,目的就是將一個物件的構建與它的表示分離,使得同樣的構建過程可以建立不同的表示

沒看懂?直接來例子:

不引入Builder Pattern,買車的條件有產地,座位數,油耗:
Car.buy(Country="",Seats="",FuelConsumption="")
隨著條件越多,傳參隨之增加;

引入Builder Pattern:
例子1:買一輛車,沒其他要求:
Car.buy();

例子2:買一輛車,中國產的:
Car.withBuildCountry("China").buy();

例子3:買一輛車,中國產的,7座的:
Car.withBuildCountry("China").withSeats("Seven").buy();
複製程式碼

明白了嗎?核心就是在使用者不知道物件的建造過程和細節的情況下,可以直接建立物件

這3個例子,可以反向說明解決了什麼問題:

  • 方便使用者建立物件時,不需要知道實現過程,只需要給出指定物件的型別和內容即可;
  • 程式碼複用性 & 封裝性,將構建過程和細節進行封裝;
1. 工廠(建造者模式):負責製造汽車(組裝過程和細節在工廠內) 
2. 汽車購買者(使用者):你只需要說出你需要的型號(物件的型別和內容),然後直接購買就可以使用了 
(不需要知道汽車是怎麼組裝的(車輪、車門、發動機、方向盤等等))
複製程式碼

結構圖

image.png-36.5kB

組成

建造者模式包含如下角色: Builder:抽象建造者 ConcreteBuilder:具體建造者 Director:指揮者 Product:產品角色

職責

角色 職責
Builder 建立一個Product物件的各個部件指定抽象介面
ConcreteBuilder 實現Builder的介面以構造和裝配該產品的各個部件,定義並明確它所建立的表示,提供一個檢索產品的介面;
Director 構造一個使用Builder介面的物件;
Product 表示被構造的物件,包含定義組成部件的類;

換種說法

  • 指揮者(Director)直接和客戶(Client)進行需求溝通;
  • 溝通後指揮者將客戶建立產品的需求劃分為各個部件的建造請求(Builder);
  • 將各個部件的建造請求委派到具體的建造者(ConcreteBuilder);
  • 各個具體建造者負責進行產品部件的構建;
  • 最終構建成具體產品(Product)。

優點

  • 將一個物件分解為各個元件,相對獨立,不受影響;
  • 將物件元件的構造封裝起來,客戶端不需要知道內部細節;
  • 可以控制整個物件的生成過程;

缺點

  • 對不同型別的物件需要實現不同的具體構造器的類,這可能大大增加類的數量;
  • 使用範圍受限制,只適用於產品組成功能相似的產品,即可複用;

什麼時候適用建造者模式

  • 生成的產品物件有複雜的內部結構;
  • 生成的產品物件的屬性相互依賴,建造者模式可以強迫生成順序;
  • 在物件建立過程中會使用到系統中的一些其它物件,這些物件在產品物件的建立過程中不易得到;

例子1-微信公眾號訊息推送

相信大家在使用微信時,也都收到過訊息推送吧,來看看官網提供的一個例項:

{
    "touser":"OPENID",
    "template_id":"ngqIpbwh8bUfcSsECmogfXcV14J0tQlEpBO27izEYtY",
    "url":"http://weixin.qq.com/download",
    "miniprogram":{
        "appid":"xiaochengxuappid12345",
        "pagepath":"index?foo=bar"
    },
    "data":{
        "first":{
            "value":"恭喜你購買成功!",
            "color":"#173177"
        },
        "keynote1":{
            "value":"巧克力",
            "color":"#173177"
        },
        "keynote2":{
            "value":"39.8元",
            "color":"#173177"
        },
        "keynote3":{
            "value":"2014年9月22日",
            "color":"#173177"
        },
        "remark":{
            "value":"歡迎再次購買!",
            "color":"#173177"
        }
    }
}

複製程式碼

具體引數請自行到公眾號開發平臺查詢,這裡思考的是,怎麼設計通用模板?

方法很多,但是這裡給出建造者模式的做法,建立builder類(為了方便,去掉了miniprogram引數)::

# -*- coding: utf-8 -*-
 
from collections import OrderedDict
import json
 
 
# 模版中“data”節點的各個元素的資料結構
class Metadata:
    def __init__(self, value, color):
        self.value = value
        self.color = color
 
 
# 微信訊息的建造器
class MessageBuilder:
    __contentDict = OrderedDict()  # 定義整個模版的資料結構,保持新增的順序
    __dataDict = OrderedDict()  # 定義data節點的資料結構,保持新增的順序
    __dataNoteNext = 1  # data節點要新增的下一個元素的序號
 
    def __init__(self, touser, template_id, url):
        self.__contentDict['touser'] = touser
        self.__contentDict['template_id'] = template_id
        self.__contentDict['url'] = url
        self.__contentDict['data'] = self.__dataDict
 
    def add_first_data(self, value, color):
        data = Metadata(value, color)
        self.__dataDict['first'] = data
        return self
 
    def add_remark_data(self, value, color):
        data = Metadata(value, color)
        self.__dataDict['remark'] = data
        return self
 
    def add_note_data(self, value, color):
        data = Metadata(value, color)
        self.__dataDict['keynote' + str(self.__dataNoteNext)] = data
        self.__dataNoteNext += 1
        return self
 
    def build(self):
        # 為列印出來看的方便,這裡將json序列化後的結果縮排2個空格,並且不把中文轉為unicode
        return json.dumps(self.__contentDict, default=lambda o: o.__dict__, indent=2, ensure_ascii=False)
複製程式碼

有兩點要說明下:

  • 建造者內部的字典採用OrderedDict,是為了保持順序與微信示例一致;
  • 建造者每個方法都返回了本物件的引用;

建造者有了,就來生成訊息吧,想起上幾天fc的通知:

image.png-88.1kB

模擬作如上兩條微信訊息:

if __name__ == '__main__':
    pickup_builder = MessageBuilder('jb', 'template_id_pickup', '') \
        .add_first_data('您有一個快遞在蜂巢櫃裡等你來取哦!', '#173177') \
        .add_note_data('123456', '#173177') \
        .add_note_data('jb快遞', '#173177') \
        .add_note_data('789456123', '#173177') \
        .add_note_data('15914255XXX', '#173177') \
        .add_note_data('廣州', '#173177') \
        .add_remark_data('元宵節快到了,人不在家,也要把愛寄回家~', '#173177')
    print('生成取件通知微信訊息')
    print(order_builder.build())
 
 
    print()
 
 
    takeout_builder = MessageBuilder('user222222', 'template_id_takeout', '') \
        .add_first_data('您的包裹已被取出啦', '#173177') \
        .add_note_data('jb快遞', '#173177') \
        .add_note_data('78954', '#173177') \
        .add_note_data('15914255XXX', '#173177') \
        .add_note_data('廣州', '#173177') \
        .add_remark_data('點選詳情檢視物流進度', '#173177')
    print('生成取出微信訊息')
    print(send_builder.build())

複製程式碼

這樣看下來,是不是程式碼清晰多了,而且可複用,好像很不錯的感覺~

例子2-組建身體

該例子來源於此處

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import abc

class Builder(object):
    __metaclass__ = abc.ABCMeta

    @abc.abstractmethod
    def create_header(self):
        pass

    @abc.abstractmethod
    def create_body(self):
        pass

    @abc.abstractmethod
    def create_hand(self):
        pass

    @abc.abstractmethod
    def create_foot(self):
        pass

class Thin(Builder):

    def create_header(self):
        print '瘦子的頭'

    def create_body(self):
        print '瘦子的身體'

    def create_hand(self):
        print '瘦子的手'

    def create_foot(self):
        print '瘦子的腳'

class Fat(Builder):

    def create_header(self):
        print '胖子的頭'

    def create_body(self):
        print '胖子的身體'

    def create_hand(self):
        print '胖子的手'

    def create_foot(self):
        print '胖子的腳'

class Director(object):

    def __init__(self, person):
        self.person = person

    def create_preson(self):
        self.person.create_header()
        self.person.create_body()
        self.person.create_hand()
        self.person.create_foot()


if __name__=="__main__":
    thin = Thin()
    fat = Fat()
    director_thin = Director(thin)
    director_fat = Director(fat)
    director_thin.create_preson()
    director_fat.create_preson()
複製程式碼

上面類的設計如下圖,

image.png-99.5kB

指揮者Director 呼叫建造者Builder的物件,具體的建造過程是在Builder的子類中實現的;

回到正文,理解完建造者模式,突然發現,好像跟上面的封裝概念相似的?

def xx(A='',B=True,C="xx"):
    ...
    return jbtest(A,B,C)
def xxx():
    ...
    return jbtest(A,B,C)
def xxxx(A=''):
    ...
    return jbtest(A,B,C)
複製程式碼

是的,Builder Pattern 也是封裝方式,一般來說,會基於原有的封裝再二次封裝,這樣的好處就是業務方無需關心內部邏輯,營造用的好爽的感覺,而Builder Pattern內部還是使用api或者資料庫的方式來創造資料,只是進行易用性封裝而已;

image.png-252.7kB

對業務方來說,是用的爽,對於維護者來說,苦的一逼,詳情請看上面的缺點,簡單就是維護成本高,容易出現動一發而牽全身,一般來說,只有大廠才會做這事;

平臺化

建造者模式是一種設計的思路,因此可適用於不同語言,但不同公司使用的語言不一樣,有Java、Python、php等等,因此,同一套程式碼,不同環境,就不適用了;

因此,解決這問題的核心在於封裝成api,並且結合GUI介面,做成平臺的形式,也就是所謂的測試資料平臺

但目前來看,業界沒看到類似開源的例子,可能都是內部使用;

憧憬

雖然建立資料越來越方便了,但每次都需要建立資料,部分可能還是重複資料;

能否建立前先搜尋,如果有符合條件的資料,直接返回,沒有再建立資料,這樣的話,測試資料也會越來越龐大,便於平臺化後的資料複用;

不過,這只是想而已,目前來說,jb自認沒這能力寫搜尋邏輯,但一直希望,讓自動化更自動;

比如介面測試,可以直接解析介面文件,根據每個欄位型別,自動生成資料,這樣連資料建立都不需要了;
複製程式碼

小結

本文主要介紹資料建立相關的內容,大部分在資料建立,有兩種方法:

  • 直接使用暴露全部引數的資料準備函式,好處是靈活,弊端是每次呼叫前都需要準備大量資料;
  • 使用封裝函式,會更加靈活,但是可維護性差;

因此會引入建造者模式的概念,本質上也是使用api跟運算元據庫兩種方式來建立資料,只是基於原來的封裝再進行二次易用性封裝,優點在於業務方可以快速生成需要的資料;

並且介紹了後續平臺化的想法,以及個人的一些憧憬;

建造者模式不是萬能的,依然對使用場景有限制,用的不好,就會導致易用性差的情況;

溫馨提示,當用例執行完畢,需要把公共資料復原,儘可能減少對其他業務方的干擾

如果需要記錄使用的資料,可單獨把測試過程的資料入庫,以便後面出現問題後有記錄復現跟進;

最後,謝謝大家~

1-140R3154U8.jpg-9kB

相關文章