회사에서 곧 자동 테스트 실행 중 발생한 이슈들에 대해 JIRA API를 이용해서 자동으로 이슈를 생성하도록 대응을..해야할 것 같은데
그 전에 API문서도 좀 확인할 겸, 시범삼아 제 토이프로젝트에다가 붙여보기로 했습니다.
자동화된 UI테스트를 실행하면 생각보다 이슈가 많이 발생합니다. 근데 그 이슈는 몇 가지로 분류해볼 수 있을 것 같습니다.
그래서 요새 테스트 자동화쪽 트렌드가 뭔가 자동화된 테스트의 안정성이랄까, 저런 테스트/타이밍 이슈를 최대한 제거하려고 하는게 화두가 되어가는 것 같습니다.
어찌되었던 이슈가 발생하면 확인을 해야하고, 실제 프로덕트 이슈면 담당기능 개발자에게 티켓을 생성해서 넘겨야하는데, 이럴 때 자동으로 티켓이 생성되어있으면 티켓 생성의 번거로움을 좀 줄일 수도 있을 것입니다.
일단 JIRA의 API를 이용해서 이슈티켓을 자동으로 생성하려면 JIRA API KEY를 발급받아놓아야합니다.
현재 JIRA Cloud에서는 우측 상단 프로필 > 계정 관리 > 보안 에서 키를 발급받을 수 있습니다.
사내 설치형 JIRA를 사용하시는 분들은 사내 관리자분들께 확인하시면 될 것 같고용...
다만 이 key로 리포트하게 되면 보고자가 키를 발급한 사람이 되기 때문에, 이게 싫으시면 자동테스트용 계정을 하나 생성하는 것도 방법이 될 수 있습니다.
다음은 API를 사용할 것인데 방법은 커녕 호출 주소도 모르므로 API 문서를 확인합니다.
https://developer.atlassian.com/cloud/jira/platform/rest/v2/intro/#version
회사 JIRA API는 어떻게 사용할 수 있을진 모르겠지만, 일단 저는 rest api v2를 사용했습니다.
여기서 issue를 생성할 때는 POST create issue
를 호출하면 되는데, 그 전에 미리 알아야할 값들이 있습니다.
이슈를 생성하기 위해서는 당연히 권한을 가지고 있는 것을 증명해야합니다. v2에서는 이메일+api key 조합의 BasicAuth를 사용합니다. 자세한 건 문서 확인해보세용
이슈를 생성할 건데, 어떤 프로젝트에 생성할 것인지를 알아야합니다.
GET Projects paginated
를 확인해서 본인이 속한 Organization에서 자동테스트로 발생할 이슈를 등록할 프로젝트의 id를 확인해야합니다.
이것은 이슈를 등록할 때 할당을 누구로 할꺼냐 입니다. 이 부분은 몰라도 이슈 생성은 가능하고, 단 티켓은 할당되지 않음 상태 로 생성됩니다.
유저 아이디를 확인하려면 GET user
혹은 GET all users
를 확인합니다.
JIRA는 티켓을 생성할 때 티켓의 유형을 무엇으로 할 것인지 선택해야합니다.
이것은 프로젝트마다 생성할 수 있는 티켓 유형이 다르므로 확인을 해주어야합니다.
GET all issue types for user
로 확인합니다.
이것 굳이 몰라도 되는 것인데, 저의 계획상 테스트 실행 전에 묶음용 태스크 티켓을 하나 생성하고, 이후 각 테스트케이스마다 실패가 발생하면 버그티켓을 생성 후에 묶음용 티켓에 링크를 걸어주려고 했기 때문에 확인했습니다.
각 프로젝트에서 사용할 수 있는 티켓 간 링크 타입을 확인합니다. GET issue link types
이것은 이슈를 생성할 때 입력할 수 있는 필드들에 대해서 확인하기 위해 필요합니다.
프로젝트나 티켓 유형별로 반드시 설정해야하는 필드들이 존재하기 때문에, 사전에 이런것들을 파악한 후 어떤 필드들에 대해 어떤 값으로 채워줄 지를 계획해야합니다.
GET issue fields
이렇게 확인하면 준비는 다 되었습니다.
여기서는 클래스로 만들어서 작성했는데 굳이 클래스가 아니라 모듈형태로 만들어도 상관없습니다. 저는 그냥 만들다보니 클래스형태로 만들게 되었습니다.
일단 이슈 타입과 이슈 링크 타입을 이넘형태로 만들어주었습니다.
class JIRAIssueType(Enum):
TASK = "10001"
EPIC = "10002"
BUG = "10005"
class JIRAIssueLinkType(Enum):
BLOCK = {
"id": "10000",
"name": "Blocks",
"inward": "is blocked by",
"outward": "blocks",
}
CLONERS = {
"id": "10001",
"name": "Cloners",
"inward": "is cloned by",
"outward": "clones",
}
RELATES = {
"id": "10003",
"name": "Relates",
"inward": "relates to",
"outward": "relates to",
}
그 다음 JIRA api 전용 클래스를 하나 만들어주었고, 생성자 내부에서 몇가지 프로퍼티를 생성합니다.
class JIRAClient:
"""
지라 API 를 조작하기 위한 클라이언트
"""
def __init__(self):
self.base_url = "https://dhpractice.atlassian.net"
self.create_issue_resource = "/rest/api/2/issue"
self.project_id = "10000"
self.api_key = get_jira_env("JIRA_API_KEY")
self.user_name = get_jira_env("USER_NAME")
self.account_id = get_jira_env("ACCOUNT_ID")
self.basic_auth = HTTPBasicAuth(self.user_name, self.api_key)
self.create_issue_data = {
"fields": {
"description": "",
"issuetype": {
"id": ''
},
"labels": [
"createAPI"
],
"project": {
"id": self.project_id
},
"reporter": {
"id": self.account_id
},
"summary": ''
}
}
여기서 민감한 정보가 될 수 있는 값들은 별도 파일로 빼낸 다음 .gitignore
에 등록 후 읽어오도록 합니다. 로컬에서는 해당 파일에 값을 저장해서 사용하고, EC2같은데에 올려서 돌릴 때에는 환경변수값을 읽어와서 하도록하게 하면 됩니다.
create_issue_data
는 이슈 생성할 때 필요한 request body값을 미리 선언한 것입니다. 저는 연습용 프로젝트라서 기재해야할 필드가 별로 없었고, 각 조직/티켓 설정마다 기재해야할 필드들이 각각 다를 것입니다.
다음은 묶음용 task 티켓 생성 메소드입니다.
def create_task_ticket(self):
"""
Task ticket을 생성합니다.
이것은 버그 이슈들을 묶기 위한 트래킹용 티켓입니다.
"""
now = datetime.now()
data = self.create_issue_data.copy()
data['fields']['assignee']['id'] = self.account_id
data['fields']['description'] = "테스트 실행 후 발생한 이슈 묶음용 티켓"
data['fields']['issuetype']['id'] = JIRAIssueType.TASK.value
data['fields']['summary'] = f"{now.strftime('%y-%m-%d %H:%M')} 자동 테스트 실행 후 이슈 묶음용 티켓"
response = requests.post(auth=self.basic_auth, url=f"{self.base_url}{self.create_issue_resource}", json=data)
return response.json()['key']
위에서 잠깐 말씀드렸듯이, 테스트 최초 실행 전에 이슈추적용 티켓을 만든 다음에, 각각의 테스트가 실패하면 버그티켓을 생성하고 이슈추적용 티켓과 링크관계를 만들어줄 생각입니다. 이렇게 하면 이번 테스트를 실행하면서 무슨 이슈가 나왔는지 추적하기 쉬워집니다.
이슈는 Task 티켓으로 생성하고, 이슈의 제목은 어떤 테스트에서 발생했는지 알기쉽게 전체 테스트를 실행한 시간대를 기록하도록 하였습니다.
다음은 버그 티켓 생성용 메소드입니다.
def create_bug_ticket_when_test_failed(self, ticket_key: str, test_name: str, traceback: str,
exception_location: str):
"""
테스트를 실행했을 때 버그티켓을 설정합니다.
이때 묶음용 티켓의 키값을 전달받습니다.
"""
description_text = f"""
*Traceback*
{{code:text}}
{traceback}\n
{{code}}
Exception location : {exception_location}
"""
data = self.create_issue_data.copy()
data['fields']['issuetype']['id'] = JIRAIssueType.BUG.value
data['fields']['summary'] = f"{test_name}"
data['fields']['labels'].append("AutomationAPITest")
data["update"] = {
"issuelinks": [
{
"add": {
"type": {
"name": JIRAIssueLinkType.RELATES.value["name"],
"inward": JIRAIssueLinkType.RELATES.value['inward'],
"outward": JIRAIssueLinkType.RELATES.value['outward']
},
"outwardIssue": {
"key": ticket_key
}
}
}
]
}
data['fields']['description'] = description_text
requests.post(auth=self.basic_auth, url=f"{self.base_url}{self.create_issue_resource}", json=data)
일단 테스트를 실패하면 등록할 이슈라서, 디버깅하기 쉽도록 트레이스백을 적당히 알기쉬운 형태로 삽입할 수 있도록 정제합니다.
이슈타입은 버그 타입으로 지정하고, 이슈 제목은 실패한 테스트케이스의 이름(메소드명), 나중에 이슈 분류를 하기위해 labels
에 원하는 값을 추가해줍니다.
마지막으로 추적관리용 티켓과 링크를 해줘야하는데, 링크해주는 방식은 저의 프로젝트는 이슈 최초생성할 때는 불가능하고, 생성 후 update
를 통해 관계를 설정해주어야 합니다.
session 단위에서 자동 실행되는 fixture를 하나 생성하고, 해당 내부에 티켓 추적관리용 티켓 생성 로직을 넣습니다.
@pytest.fixture(scope="session", autouse=True)
def setup_jira_ticket(request):
"""
전체 세션을 실행하면서 최초 1회 실행됩니다.
특정 커맨드라인이 입력되어있는 상태라면, 이슈 티켓 묶음용 티켓을 생성합니다.
"""
is_report_to_jira = request.config.getoption('--report_to_jira')
if is_report_to_jira:
global _ISSUE_TICKET_ID
_ISSUE_TICKET_ID = _JIRA_CLIENT.create_task_ticket()
여기서는 실행자가 원하는 경우에만 JIRA에 리포트하도록, 커맨드라인 명령어를 전달받아 생성하도록 하였습니다.
티켓을 생성하면 돌아오는 응답값에 티켓의 id값이 포함되어있는데, 이것을 글로벌 변수에 저장해놓습니다.
pytest에는 테스트 실행 전 / 테스트 실행 직후 / 테스트 실행 완료 후에 실행되는 hook중에 pytest_runtest_makereport
라는 훅이 있습니다. 이 훅은 리포트 생성에도 관여하는 훅인데요, 이 hook을 override하면서, 여기에다가 테스트가 실패했을 때 버그 생성하는 로직을 추가해줍니다.
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
# 테스트의 결과를 확인합니다
request = item._request
report = yield
result = report.get_result()
if call.when == 'call': # 실제 테스트 실행 후 결과를 처리
if call.excinfo is not None: # 예외가 발생했는지 확인
outcome = call.excinfo.type.__name__ # 예외 유형
if ('Error' in outcome) or ('Exception' in outcome):
is_report_to_jira = request.config.getoption('--report_to_jira')
if is_report_to_jira:
traceback_text = result.longreprtext
# 실패 또는 에러가 발생했을 때 이슈를 등록합니다.
_JIRA_CLIENT.create_bug_ticket_when_test_failed(
_ISSUE_TICKET_ID,
item.nodeid,
traceback_text,
str(item.location)
)
이렇게 하고 실행하면
이렇게 테스트 전체 세션 실행 전에 생성한 추적관리용 티켓도 잘 생성되었고, 각각의 버그 티켓이 생성될 때 마다 relates to
관계가 잘 맺어진 것을 확인할 수 있습니다.
버그 티켓 내용도 확인해보면 제목은 폴더 디렉토리 경로::테스트클래스명::테스트메소드명
으로 확인하기 쉽게 잘 기재되어있고, 본문에 코드블록이 씌워진 채로 어떤 테스트코드에서 실패했는지 알기 쉽게 잘 저장되어있습니다.
레이블에도 제가 추가하려고 했던 레이블이 잘 들어가 있는 것을 확인할 수 있었습니다.
이렇게 assert
문에서 실패한 것 뿐만 아니라 테스트 실행 중 다른 코드로 부터 발생한 이슈로 인해 테스트가 중단되는 경우 (ERROR 처리) 도 잘 생성되는 것을 확인할 수 있었습니다.
로직상 버그 티켓은 테스트케이스에서 실패가 발생할 때마다 실시간으로 생성되므로, 티켓 등록을 하면 슬랙에 알람이 오도록 설정을 했다면 전체 테스트가 실행되는 것을 기다리지 않아도 실시간으로 대응해볼 수도 있을 것 같네요.
연습삼아 해본거라 오래걸리진 않았지만, pytest_runtest_makereport
의 parameter값에서 어떤 값을 확인해야 제가 원하는 내용들을 얻을 수 있는지 알아내는게 좀 시간이 걸렸습니다. Breakpoint 찍어서 값을 다 확인도 해보고...
결국 입력받는 item, call
에서는 제가 원하는 데이터는 제한적으로 들어있었고, @pytest.hookimpl
어노테이션을 통해서 다른 hook으로부터 결과를 전달받아 이용하는 것이었는데, 이 부분이 공식문서에도 좀 어렵게 기재되어있어서 이해하기가 어려웠네요.
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
# 테스트의 결과를 확인합니다
request = item._request
report = yield # 다른 내장 훅으로부터 결과를 전달받음
JIRA API 사용해보기 끄읏