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

  • 測(cè)試流程

    1. 從Jira上面自動(dòng)下載測(cè)試用例,標(biāo)記需要執(zhí)行的用例;
    2. 執(zhí)行自動(dòng)化測(cè)試;
    3. 回填自動(dòng)化測(cè)試的結(jié)果;
  • 面臨的困難

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

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

      • key:可以唯一標(biāo)識(shí)用例;
      • folder: 用例所屬的模塊;
      • environment:執(zhí)行環(huán)境;手工或者自動(dòng)化;
      • name:用例的名字;
      • status: 目前的執(zhí)行狀態(tài);Pass/Fail等;
    • TestRun:

      • key:可以唯一標(biāo)識(shí)一個(gè)模塊,這里使用模塊的名字表示,即TestCase中的folder字段;
      • testCaseList: 同屬一個(gè)模塊的用例;
    • TestPlan:

      • key:可以唯一標(biāo)識(shí)一個(gè)測(cè)試計(jì)劃;
      • testRunList: 同屬一個(gè)測(cè)試計(jì)劃的模塊集;
    • 可以看出,他們是層層包含的關(guān)系,一個(gè)TestPlan中包含多個(gè)TestRun,而一個(gè)TestRun中有包含一個(gè)或多個(gè)TestCase;


  • 代碼實(shí)現(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")
    
    
    • 實(shí)現(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中讀取一個(gè)TestPlan對(duì)象
            :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上下載一個(gè)TestPlan
            '''
            pass
    
        @tools.consumingTime
        def update_result_to_jira(self, testObj):
            '''
            更新結(jié)果到Jira上面
            '''
            pass
    
    • 使用到的一些自定義公共方法
    class tools:
        @classmethod
        def consumingTime(self, func):
            '''
            計(jì)算一個(gè)方法執(zhí)行的消耗時(shí)間
            '''
            @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):
            '''
            將一個(gè)字節(jié)型的字符串轉(zhuǎn)化成一個(gè)類文件對(duì)象;
            :param string:
            :return:
            '''
            fileLikeObj = tempfile.NamedTemporaryFile()
            fileLikeObj.write(bytes_string)
            fileLikeObj.flush()
            fileLikeObj.seek(0)
            return  fileLikeObj
    
    • 將其打包成一個(gè)第三方包,命名為nJira,就可以在其他地方引用了;
    • 常見的使用方法;
    import nJira
    
    # 1. 建立和Jira的連接;
    nj = nJira(username, password)
    
    # 2. 獲取一個(gè)指定的測(cè)試計(jì)劃的用例, 返回的是一個(gè)TestPlan對(duì)象;
    tp = nj.get_test_plan("tp1")
    
    # 3. 過濾出自己想要的測(cè)試用例,比如說:還未執(zhí)行的自動(dòng)化用例,這里返回一個(gè)新的TestPlan
    tp1 = tp.filter(environment=('Automation',), status=('Not Executed'))
    
    # 4. 保存到xlsx文件中,文件名為TestPlan的Key值;
    tp.to_xlsx('D:/')
    
    # 5. 測(cè)試的過程中可以把最新的自動(dòng)化測(cè)試結(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. 上述就是一個(gè)典型的自動(dòng)化執(zhí)行的流程;
    
    # 8. 還有一些其他的功能;
    #   8.1. 獲取第一個(gè)測(cè)試模塊;
        tr = tp['testRunList'][0]
    #   8.2. 遍歷這個(gè)測(cè)試模塊, 將結(jié)果標(biāo)記為失敗;
        for tc in tr.walk():
            tc['status'] = 'Fail'
    #   8.3. 單獨(dú)更新這個(gè)模塊的結(jié)果
        nJra.update_result_to_jira(tr)
    #   8.4. 單獨(dú)更新某個(gè)測(cè)試用例的結(jié)果
        nJra.update_result_to_jira(tr['testCaseList'][0])
    #   8.5. 單獨(dú)把這個(gè)模塊保存到xlsx文件中,文件名字是模塊名
        tr.to_xlsx('D:/')
    #   8.6. 統(tǒng)計(jì)一個(gè)TestPlan有多少個(gè)TestCase;
        len(tp)
    #   8.7. 統(tǒng)計(jì)一個(gè)TestRun有多少個(gè)TestCase;
        len(tr)
    #   8.8 基本上所有的方法都在TestPlan/TestRun/TestCase上通用,根據(jù)對(duì)象不同,返回的東西不同;
    #   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)
    
    • 部分代碼隱藏實(shí)現(xiàn),本文主要記錄大體思路;
    • 歡迎交流;
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容