實現(xiàn)Jira上測試用例的下載和執(zhí)行結(jié)果的回填

  • 測試流程

    1. 從Jira上面自動下載測試用例嚷狞,標(biāo)記需要執(zhí)行的用例;
    2. 執(zhí)行自動化測試竭翠;
    3. 回填自動化測試的結(jié)果斋扰;
  • 面臨的困難

    • 之前用QC管理用例啃洋,現(xiàn)在切換到Jira宏娄,先前的工具無法使用,需要從新開發(fā)一套自動化工具;
    • 目前可以通過訪問公司Jira的RESTful接口十饥,可以獲取Json格式的數(shù)據(jù)祖乳;
    • 盡量使其模塊化眷昆、并易于維護和擴展亚斋;

  • 數(shù)據(jù)結(jié)構(gòu)設(shè)計
    • TestCase:為基本的測試單元;

      • key:可以唯一標(biāo)識用例;
      • folder: 用例所屬的模塊纸泡;
      • environment:執(zhí)行環(huán)境女揭;手工或者自動化吧兔;
      • name:用例的名字境蔼;
      • status: 目前的執(zhí)行狀態(tài)欧穴;Pass/Fail等涮帘;
    • TestRun:

      • key:可以唯一標(biāo)識一個模塊拼苍,這里使用模塊的名字表示,即TestCase中的folder字段调缨;
      • testCaseList: 同屬一個模塊的用例疮鲫;
    • TestPlan:

      • key:可以唯一標(biāo)識一個測試計劃;
      • testRunList: 同屬一個測試計劃的模塊集弦叶;
    • 可以看出俊犯,他們是層層包含的關(guān)系,一個TestPlan中包含多個TestRun伤哺,而一個TestRun中有包含一個或多個TestCase燕侠;


  • 代碼實現(xiàn)

    • 首先定義使用到的數(shù)據(jù)字段類型,這么做是為了以后和數(shù)據(jù)庫結(jié)合立莉;
    class Field(object):
        def __init__(self, name, f_type):
            self.name = name
            self.f_type = f_type
    
        def __str__(self):
            return f"<{self.__class__.__name__}: {self.name}>"
    
    class StringField(Field):
        '''
        字符串
        '''
        def __init__(self, name):
            super(StringField, self).__init__(name, 'String')
    
    class ListField(Field):
        '''
        集合
        '''
        def __init__(self, name):
            super(ListField, self).__init__(name, 'List')
    
    • 定義模型的元類,使用它來創(chuàng)建所有的模型饶氏,也是為了以后和數(shù)據(jù)庫結(jié)合柠衅;
    class ModelMetaclass(type):
        def __new__(cls, clsname, bases, attrs):
            # Ingore 'Model' class
            if clsname == 'Model':
                return type.__new__(cls, clsname, bases, attrs)
            Logger.info(f"Found model: {clsname}")
            mappings = dict()
            # Save all data field to one named '__mappings__'
            for k, v in attrs.items():
                if isinstance(v, Field):
                    mappings[k] = v
                    Logger.info(f"Found mapping: {k}===={v}")
            # Delete them in original class
            for k in mappings.keys():
                attrs.pop(k)
            attrs['__mappings__'] = mappings
            return type.__new__(cls, clsname, bases, attrs)
    
    • 定義TestPlan/TestRun/TestCase的父類
    class Model(dict, metaclass=ModelMetaclass):
        '''
        Subclass of dict;
        '''
        def __init__(self, **kwargs):
            super(Model, self).__init__(**kwargs)
    
        def __getattr__(self, key):
            try:
                return self[key]
            except KeyError as e:
                raise e
    
        def __setattr__(self, key, value):
            self[key] = value
    
        def __str__(self):
            return f"{self.__class__.__name__}:" + "".join([
                f"{k}:{self[k]} " for k in self.__mappings__.keys()
            ])
    
        def __len__(self):
            '''
            復(fù)寫__len__()方法势誊,使其返回TestCase的數(shù)量查近;
            '''
            if(isinstance(self, TestRun)):
                return self["testCaseList"].__len__()
            elif(isinstance(self, TestPlan)):
                try:
                    return reduce(lambda x,y: x+y, [v.__len__() for v in self["testRunList"]])
                except TypeError:
                    return self["testRunList"].__len__()
            else:
                return self.__len__()
    
    
    • 為Model類增加方法册烈,使TestPlan/TestRun/TestCase繼承到;
    def _save_to_excel(self, ws, header):
        if(isinstance(self, TestCase)):
            ws.append([self.get(v) for v in header])
        elif(isinstance(self, TestRun)):
            for v in self["testCaseList"]:
                v._save_to_excel(ws, header)
    
    def to_xlsx(self, folder, backup=False):
        '''
        保存到xlsx文件中;
        :param folder:
        :return:
        '''
        if os.path.isdir(folder):
            if isinstance(self, TestPlan):
                file = os.path.join(folder, f"{self['testPlanKey']}.xlsx")
            elif isinstance(self, TestRun):
                file = os.path.join(folder, f"{self['testRunKey']}.xlsx")
            elif isinstance(self, TestCase):
                file = os.path.join(folder, f"{self['testCaseKey']}.xlsx")
            # Header for xlsx file.
            file_header = list(TestCase.__mappings__.keys())
            if os.path.isfile(file):
                if backup:
                    suffix = time.strftime(r'%Y_%m_%d_%H_%M_%S')
                    copyfile(file,
                             file.split(r'.xlsx')[0].__add__(f'_{suffix}.xlsx'))
                os.remove(file)
            # new
            wb = Workbook()
            ws = wb.active
            ws.append(file_header)
            # write
            for item in self.walk():
                item._save_to_excel(ws, file_header)
            wb.save(file)
            wb.close()
        else:
            raise Exception(r'Parameter MUST be a existed folder.')
    
    def to_json(self):
        '''
        轉(zhuǎn)化為json格式
        :return:
        '''
        return json.dumps(self, default=lambda obj:obj.__dict__)
    
    def filter(self, environment=('Automation', 'Manual'), caseType=None, status=('Not Executed'), owner=None):
        '''
        過濾TestCase
        :param environment: 'Automation', 'Manual'
        :param CaseType: 'GUI' or 'CLI'
        :param Status: 'Pass', 'Fail' .etc
        :param owner:
        :return: The filtered object.
        '''
        if(isinstance(self, TestCase)):
            envirFlag = self.get("environment") in environment if environment else True
            stateFlag = self.get("status") in status if status else True
            ownerFlag = self.get("owner") in owner if owner else True
            GUIType = True if(re.search(r"by gui", self.get("testCaseName").strip(), re.IGNORECASE)) else False
            CLIType = True if(re.search(r"by cli", self.get("testCaseName").strip(), re.IGNORECASE)) else False
            TypeFlag = ((CLIType and caseType == "CLI") or
                        (GUIType and caseType == "GUI") or
                        (not GUIType and not CLIType and caseType == "GUI")) if caseType else True
            if(envirFlag and stateFlag and TypeFlag and ownerFlag):
                return self
            else:
                return None
        elif(isinstance(self, TestRun)):
            return TestRun(testRunKey=self.get("testRunKey"), testCaseList= list(filter(
                lambda x:x is not None, map(
                lambda y:y.filter(environment, caseType, status, owner), self.get("testCaseList")
            ))))
        elif(isinstance(self, TestPlan)):
            return TestPlan(testPlanKey=self.get("testPlanKey"), testRunList= list(filter(
                lambda x:len(x)>0, map(
                lambda y:y.filter(environment, caseType, status, owner), self.get("testRunList")
            ))))
    
    def walk(self):
        '''
        遍歷所有的TestCase;
        :return: A iterator of TestCase
        '''
        if isinstance(self, TestCase):
            return self
        elif isinstance(self, TestRun):
            for v in self.get("testCaseList"):
                yield v
        elif isinstance(self, TestPlan):
            for run in self.get("testRunList"):
                for case in run.walk():
                    yield case
    
    • 定義TestPlan/TestRun/TestCase
    class TestCase(Model):
        '''
        Basic test object
        '''
        testRunKey = StringField("testRunKey")
        testCaseKey = StringField("testCaseKey")
        folder = StringField("folder")
        environment = StringField("environment")
        testCaseName = StringField("testCaseName")
        status = StringField("status")
        owner = StringField("owner")
    
    class TestRun(Model):
        """
        Obtain a series of TestCase
        """
        testRunKey = StringField("testRunKey")
        testCaseList = ListField("testCaseList")
    
    class TestPlan(Model):
        """
        Obtain a series of TestRun
        """
        testPlanKey = StringField("testPlanKey")
        testRunList = ListField("testRunList")
    
    
    • 實現(xiàn)和Jira交互的部分
    class NJira(requests.Session):
        '''
        Init
        '''
        def __init__(self, username, password):
            super(NtgrJira, self).__init__()
            self.auth = (username, password)
            self.verify = False
            self._login()
    
        def close(self):
            super(NtgrJira, self).close()
            self._logout()
    
        def _login(self):
            pass
    
        def _logout(self):
            pass
    
        @classmethod
        def read_xlsx(self, file):
            '''
            從xlsx中讀取一個TestPlan對象
            :param file: named after TestPlanKey
            :return:
            '''
            wb = load_workbook(file, read_only=True)
            ws = wb.active
            # Get header
            file_header = [v.value for v in ws[1]]
            # walk all test cases
            testCasesList = []
            for row in ws.iter_rows(min_row=2, max_col=file_header.__len__()):
                kw = dict(zip(file_header, [v.value for v in row]))
                testCasesList.append([TestCase(**kw)])
            testCasesList = tools.classify(testCasesList, identifier="folder")
            # Return a TestPlan
            tp = {
                'testPlanKey': os.path.basename(file).split(r'.')[0],
                'testRunList': []
            }
            for tr in testCasesList:
                kw = {
                    'testRunKey': tr[0]['folder'],
                    'testCaseList': tr
                }
                tp['testRunList'].append(TestRun(**kw))
            return TestPlan(**tp)
    
        @tools.consumingTime
        def get_test_plan(self, key):
            '''
            從Jira上下載一個TestPlan
            '''
            pass
    
        @tools.consumingTime
        def update_result_to_jira(self, testObj):
            '''
            更新結(jié)果到Jira上面
            '''
            pass
    
    • 使用到的一些自定義公共方法
    class tools:
        @classmethod
        def consumingTime(self, func):
            '''
            計算一個方法執(zhí)行的消耗時間
            '''
            @functools.wraps(func)
            def wrapper(*args, **kwargs):
                sTime = time.time()
                f = func(*args, **kwargs)
                cTime = time.time() - sTime
                Logger.info(f"Execute {func.__name__} consuming {cTime} seconds.")
                return f
            return wrapper
    
        @classmethod
        def classify(self, caseList, identifier="folder"):
            '''
            為元素分類
            :param caseList: [[{'folder': 'IP', 'key': '1'}], [{'folder': 'IP', 'key': '2'}], [{'folder': 'Mac', 'key': '3'}]]
            :param identifier:
            :return: [[{'folder': 'IP', 'key': '1'}, {'folder': 'IP', 'key': '2'}], [{'folder': 'Mac', 'key': '3'}]]
            '''
            retList = []
            while len(caseList) > 0:
                # Same identifier with first case.
                sameIdentifierList =  functools.reduce(lambda x,y: x + y if x[0][identifier] == y[0][identifier] else x, caseList)
                # Delete them in basic list.
                caseList = list(filter(lambda x: not x[0] in sameIdentifierList, caseList))
                # Add to return list
                retList.append(sameIdentifierList)
            return retList
    
        @classmethod
        def bytes_to_file(self, bytes_string):
            '''
            將一個字節(jié)型的字符串轉(zhuǎn)化成一個類文件對象;
            :param string:
            :return:
            '''
            fileLikeObj = tempfile.NamedTemporaryFile()
            fileLikeObj.write(bytes_string)
            fileLikeObj.flush()
            fileLikeObj.seek(0)
            return  fileLikeObj
    
    • 將其打包成一個第三方包,命名為nJira怀吻,就可以在其他地方引用了;
    • 常見的使用方法萨赁;
    import nJira
    
    # 1. 建立和Jira的連接详瑞;
    nj = nJira(username, password)
    
    # 2. 獲取一個指定的測試計劃的用例, 返回的是一個TestPlan對象;
    tp = nj.get_test_plan("tp1")
    
    # 3. 過濾出自己想要的測試用例锣杂,比如說:還未執(zhí)行的自動化用例蝶押,這里返回一個新的TestPlan
    tp1 = tp.filter(environment=('Automation',), status=('Not Executed'))
    
    # 4. 保存到xlsx文件中,文件名為TestPlan的Key值;
    tp.to_xlsx('D:/')
    
    # 5. 測試的過程中可以把最新的自動化測試結(jié)果更新到xlsx文件中撕攒;
    
    # 6. 讀取xlsx中最新的結(jié)果,選取已經(jīng)執(zhí)行的,更新到Jira上蹬叭;
    tp2 = nJira.read_xlsx('D:/tp1.xlsx').filter(status=('Pass', 'Fail', 'N/A'))
    nJra.update_result_to_jira(tp2)
    
    # 7. 上述就是一個典型的自動化執(zhí)行的流程;
    
    # 8. 還有一些其他的功能;
    #   8.1. 獲取第一個測試模塊瓣铣;
        tr = tp['testRunList'][0]
    #   8.2. 遍歷這個測試模塊, 將結(jié)果標(biāo)記為失敗;
        for tc in tr.walk():
            tc['status'] = 'Fail'
    #   8.3. 單獨更新這個模塊的結(jié)果
        nJra.update_result_to_jira(tr)
    #   8.4. 單獨更新某個測試用例的結(jié)果
        nJra.update_result_to_jira(tr['testCaseList'][0])
    #   8.5. 單獨把這個模塊保存到xlsx文件中,文件名字是模塊名
        tr.to_xlsx('D:/')
    #   8.6. 統(tǒng)計一個TestPlan有多少個TestCase禽绪;
        len(tp)
    #   8.7. 統(tǒng)計一個TestRun有多少個TestCase;
        len(tr)
    #   8.8 基本上所有的方法都在TestPlan/TestRun/TestCase上通用础钠,根據(jù)對象不同,返回的東西不同翻具;
    #   8.9. 支持上下文管理器的寫法
        with nJira(username, password) as nj:
            tp = nj.get_test_plan("tp1")   
            tp1 = tp.filter(environment=('Automation',), status=('Not Executed'))
            tp.to_xlsx('D:/')
            tp2 = nJira.read_xlsx('D:/tp1.xlsx').filter(status=('Pass', 'Fail', 'N/A'))
            nJra.update_result_to_jira(tp2)
    
    • 部分代碼隱藏實現(xiàn)运提,本文主要記錄大體思路;
    • 歡迎交流;
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末市框,一起剝皮案震驚了整個濱河市蒋得,隨后出現(xiàn)的幾起案子窍侧,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,639評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件梧却,死亡現(xiàn)場離奇詭異三椿,居然都是意外死亡,警方通過查閱死者的電腦和手機蛋叼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,277評論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來松却,“玉大人飞几,你說我怎么就攤上這事〈缌剩” “怎么了吏砂?”我有些...
    開封第一講書人閱讀 157,221評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長牡直。 經(jīng)常有香客問我劲件,道長零远,這世上最難降的妖魔是什么奴饮? 我笑而不...
    開封第一講書人閱讀 56,474評論 1 283
  • 正文 為了忘掉前任师脂,我火速辦了婚禮安券,結(jié)果婚禮上余舶,老公的妹妹穿的比我還像新娘锹淌。我一直安慰自己匿值,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,570評論 6 386
  • 文/花漫 我一把揭開白布赂摆。 她就那樣靜靜地躺著挟憔,像睡著了一般。 火紅的嫁衣襯著肌膚如雪烟号。 梳的紋絲不亂的頭發(fā)上绊谭,一...
    開封第一講書人閱讀 49,816評論 1 290
  • 那天,我揣著相機與錄音汪拥,去河邊找鬼达传。 笑死,一個胖子當(dāng)著我的面吹牛迫筑,可吹牛的內(nèi)容都是我干的宪赶。 我是一名探鬼主播,決...
    沈念sama閱讀 38,957評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼脯燃,長吁一口氣:“原來是場噩夢啊……” “哼搂妻!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起辕棚,我...
    開封第一講書人閱讀 37,718評論 0 266
  • 序言:老撾萬榮一對情侶失蹤欲主,失蹤者是張志新(化名)和其女友劉穎追他,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體岛蚤,經(jīng)...
    沈念sama閱讀 44,176評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,511評論 2 327
  • 正文 我和宋清朗相戀三年懈糯,在試婚紗的時候發(fā)現(xiàn)自己被綠了涤妒。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,646評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡赚哗,死狀恐怖她紫,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情屿储,我是刑警寧澤贿讹,帶...
    沈念sama閱讀 34,322評論 4 330
  • 正文 年R本政府宣布,位于F島的核電站够掠,受9級特大地震影響民褂,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜疯潭,卻給世界環(huán)境...
    茶點故事閱讀 39,934評論 3 313
  • 文/蒙蒙 一赊堪、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧竖哩,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,755評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至增淹,卻和暖如春椿访,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背埠通。 一陣腳步聲響...
    開封第一講書人閱讀 31,987評論 1 266
  • 我被黑心中介騙來泰國打工赎离, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人端辱。 一個月前我還...
    沈念sama閱讀 46,358評論 2 360
  • 正文 我出身青樓梁剔,卻偏偏與公主長得像,于是被迫代替她去往敵國和親舞蔽。 傳聞我的和親對象是個殘疾皇子荣病,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,514評論 2 348

推薦閱讀更多精彩內(nèi)容