-
測試流程
- 從Jira上面自動下載測試用例嚷狞,標(biāo)記需要執(zhí)行的用例;
- 執(zhí)行自動化測試竭翠;
- 回填自動化測試的結(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)运提,本文主要記錄大體思路;
- 歡迎交流;