일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
- katalontestops
- 테스트케이스
- 앱테스트자동화
- test
- 카탈론
- qa #sqa #qa엔지니어
- sqa
- 공개키기반구조
- QA
- appium
- 지라
- 모바일앱테스트
- 테스트플로우
- 모바일테스트자동화
- Git
- 카탈론스튜디오
- 지라연동
- jenkins
- JIRA
- confluence
- tastrail
- 테스트
- PKI
- 모바일자동화
- github
- 테스트자동화
- katalonstudio
- Katalon
- openssl
- testautomation
- Today
- Total
흔한 QA 엔지니어
Katalon Studio & Jira 연동하기 본문
Katalon Studio에서 실행한 테스트케이스 내 에러 발생 시
Jira 내 자동으로 이슈를 생성하기 위함입니다!
개발팀에서 바로 확인 가능하도록
에러 당시 스크린샷 + 테스트케이스 스크립트 파일을 첨부했습니다.
또한 Jira 프로젝트 내 중복 체크 후 이미 존재할 경우
기존 이슈 내 코멘트만 작성합니다.
1. Jira 연동 정보 확인
Katalon Studio 연동을 위해 여러 정보에 대한 확인이 필요합니다.
Jira 계정 정보(이메일 형식 ID) / 프로젝트 Key / API Token / 프로젝트 URL / / Jira Issue Type
2. Katalon Studio 내 .gitignore 파일 및 console.properties 설정
console.properties를 통해 인증 정보나 환경 설정을 코드와 분리하면
형상 관리 도구에 소스코드를 안전하게 공유할 수 있고
API 키 등 민감 정보 노출을 예방할 수 있습니다.
# .gitignore 파일
# Katalon reports
Reports/
# Test results
bin/
Libs/
# IntelliJ or Eclipse files
*.iml
*.classpath
*.project
.idea/
.settings/
# 민감 설정
Include/resources/console.properties
# Include/resources/console.properties 파일
# Jira
jira.username=test@test.com // Jira 이메일 형식 계정
jira.api.token= // JIRA API 토큰
jira.url=https://test-team.atlassian.net/ // Jira 프로젝트 URL
jira.project.key= TEST // Jira 프로젝트 키
jira.issue.type=Task // Jira 프로젝트 내 자동으로 생성할 이슈 타입
3. Test Listener 생성
Test Listener 파일 내 자동 method 생성 옵션
옵션 선택 후 리스너 파일 생성 시 코드 작성에 용이하지만 저희는 붙여넣기 할거니까요
Generate sample Before Test Case method - Test Case 실행 전 호출되는 method
Generate sample After Test Case method - Test Case 실행 후 호출되는 method
Generate sample Before Test Suite method - Test Suite 실행 전 호출되는 method
Generate sample After Test Suite method - Test Suite 실행 후 호출되는 method
4. 스크립트 작성
주석 참고해주세요
import com.kms.katalon.core.annotation.*
import com.kms.katalon.core.context.TestCaseContext
import com.kms.katalon.core.util.KeywordUtil
import com.kms.katalon.core.webui.keyword.WebUiBuiltInKeywords as WebUI
import com.kms.katalon.core.configuration.RunConfiguration
import java.util.Properties
import java.net.HttpURLConnection
import java.net.URL
import groovy.json.JsonSlurper
import org.apache.commons.text.StringEscapeUtils
class JiraTestListener {
// 테스트케이스 실행 후 실패 케이스를 Jira 프로젝트 내 자동 생성
@AfterTestCase
def afterTestCase(TestCaseContext testCaseContext) {
if (testCaseContext.getTestCaseStatus() == 'FAILED') {
Properties props = loadProps()
String jiraUsername = props.getProperty("jira.username")
String jiraApiToken = props.getProperty("jira.api.token")
String jiraUrl = props.getProperty("jira.url")
String projectKey = props.getProperty("jira.project.key")
String issueType = props.getProperty("jira.issue.type")
String encodedAuth = "${jiraUsername}:${jiraApiToken}".bytes.encodeBase64().toString()
String testCaseId = testCaseContext.getTestCaseId()
String testCaseName = testCaseId.split("/")[-1] // LoginSuccess
String summary = "Katalon Test Failure: ${testCaseName}"
String errorMessage = testCaseContext.getMessage() ?: "No error message"
String failedLine = extractFailedLine(errorMessage)
String timestamp = new Date().format("yyyy-MM-dd HH:mm:ss")
// 중복 이슈 검색
String issueKey = findExistingIssue(testCaseId, jiraUrl, encodedAuth)
if (issueKey != null) {
KeywordUtil.logInfo("중복 이슈 감지 - ${issueKey}, 코멘트만 추가")
addCommentToJiraIssue(jiraUrl, issueKey, errorMessage, encodedAuth)
return
}
// 새 이슈 생성
def descriptionText = """
### ❌ Katalon Test Failure
- **Test Case**: ${testCaseId}
- **Failed Line**: ${failedLine}
- **Status**: FAILED
- **Timestamp**: ${timestamp}
- **Error**: ${errorMessage}
"""
String escapedDescription = StringEscapeUtils.escapeJson(descriptionText)
String requestBody = """
{
"fields": {
"project": { "key": "${projectKey}" },
"summary": "${summary}",
"description": "${escapedDescription}",
"issuetype": { "name": "${issueType}" }
}
}
"""
URL issueUrl = new URL("${jiraUrl}/rest/api/2/issue")
HttpURLConnection conn = (HttpURLConnection) issueUrl.openConnection()
conn.setRequestMethod("POST")
conn.setDoOutput(true)
conn.setRequestProperty("Authorization", "Basic " + encodedAuth)
conn.setRequestProperty("Content-Type", "application/json")
conn.outputStream.write(requestBody.getBytes("UTF-8"))
int responseCode = conn.getResponseCode()
if (responseCode == 201) {
def json = new JsonSlurper().parseText(conn.inputStream.text)
issueKey = json.key
KeywordUtil.logInfo("Jira 이슈 생성 완료: ${issueKey}")
// 에러 발생 당시 스크린샷 첨부
String screenshotPath = RunConfiguration.getReportFolder() + "/Failed_${System.currentTimeMillis()}.png"
WebUI.takeScreenshot(screenshotPath)
File screenshotFile = new File(screenshotPath)
if (screenshotFile.exists()) {
uploadFileToJira(jiraUrl, issueKey, screenshotFile, encodedAuth)
}
// 테스트 케이스 스크립트 첨부
File scriptFile = findTestCaseScriptFile(testCaseId)
if (scriptFile != null && scriptFile.exists()) {
uploadFileToJira(jiraUrl, issueKey, scriptFile, encodedAuth)
} else {
KeywordUtil.logInfo("테스트 케이스 스크립트를 찾을 수 없습니다.")
}
} else {
KeywordUtil.markFailed("Jira 이슈 생성 실패 - 응답 코드: ${responseCode}")
}
}
}
// Failed Line 추출 함수
private String extractFailedLine(String message) {
def matcher = message =~ /at (\w+)\.run\((.+):(\d+)\)/
return matcher.find() ? "${matcher.group(2)}: Line ${matcher.group(3)}" : "Unknown"
}
// 테스트 케이스 스크립트 파일 탐색
private File findTestCaseScriptFile(String testCaseId) {
if (!testCaseId?.startsWith("Test Cases/")) return null
String relativePath = testCaseId.replace("Test Cases/", "").trim()
File folder = new File(RunConfiguration.getProjectDir() + "/Scripts/" + relativePath)
if (!folder.exists() || !folder.isDirectory()) return null
return folder.listFiles()?.find { it.name.endsWith(".groovy") }
}
// 중복 이슈 확인 (열린 이슈 중 summary 동일한 것 검색)
private String findExistingIssue(String testCaseId, String jiraUrl, String auth) {
Properties props = loadProps()
String projectKey = props.getProperty("jira.project.key")
// 키워드만 추출
String keyword = testCaseId.split("/")[-1] // LoginSuccess
// Done 상태의 이슈는 찾지 않음
String jql = "project = ${projectKey} AND summary ~ '${keyword}' AND statusCategory != Done"
// 모든 상태의 이슈 찾기
// String jql = "project = ${projectKey} AND summary ~ '${keyword}'"
KeywordUtil.logInfo("중복 체크 JQL (raw): ${jql}")
String encodedJql = URLEncoder.encode(jql, "UTF-8")
URL searchUrl = new URL("${jiraUrl.replaceAll("/+\$", "")}/rest/api/2/search?jql=${encodedJql}")
HttpURLConnection conn = (HttpURLConnection) searchUrl.openConnection()
conn.setRequestMethod("GET")
conn.setRequestProperty("Authorization", "Basic " + auth)
conn.setRequestProperty("Content-Type", "application/json")
def responseText = conn.inputStream.text
KeywordUtil.logInfo("Jira 응답: ${responseText}")
def json = new JsonSlurper().parseText(responseText)
if (json.issues && json.issues.size() > 0) {
KeywordUtil.logInfo("중복 이슈 발견됨: " + json.issues[0].key)
return json.issues[0].key
}
KeywordUtil.logInfo("중복 이슈 없음 → 새로 생성")
return null
}
// 기존 이슈에 코멘트 추가
private void addCommentToJiraIssue(String jiraUrl, String issueKey, String message, String auth) {
def payload = [
body: "재발생한 에러:\n```\n${message.take(400)}\n```"
]
String commentJson = new groovy.json.JsonBuilder(payload).toString()
URL url = new URL("${jiraUrl}/rest/api/2/issue/${issueKey}/comment")
HttpURLConnection conn = (HttpURLConnection) url.openConnection()
conn.setRequestMethod("POST")
conn.setDoOutput(true)
conn.setRequestProperty("Authorization", "Basic " + auth)
conn.setRequestProperty("Content-Type", "application/json")
conn.outputStream.write(commentJson.getBytes("UTF-8"))
if (conn.responseCode == 201) {
KeywordUtil.logInfo("코멘트 추가 완료 - 이슈 ${issueKey}")
} else {
KeywordUtil.logInfo("코멘트 추가 실패: ${conn.responseCode}")
}
}
// Jira 내 파일 업로드
private void uploadFileToJira(String jiraUrl, String issueKey, File file, String auth) {
String boundary = "----Boundary" + System.currentTimeMillis()
URL url = new URL("${jiraUrl}/rest/api/2/issue/${issueKey}/attachments")
HttpURLConnection conn = (HttpURLConnection) url.openConnection()
conn.setDoOutput(true)
conn.setRequestMethod("POST")
conn.setRequestProperty("Authorization", "Basic " + auth)
conn.setRequestProperty("X-Atlassian-Token", "no-check")
conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=${boundary}")
OutputStream output = conn.getOutputStream()
PrintWriter writer = new PrintWriter(new OutputStreamWriter(output, "UTF-8"), true)
writer.append("--${boundary}\r\n")
writer.append("Content-Disposition: form-data; name=\"file\"; filename=\"${file.getName()}\"\r\n")
writer.append("Content-Type: application/octet-stream\r\n\r\n")
writer.flush()
FileInputStream input = new FileInputStream(file)
byte[] buffer = new byte[4096]
int bytesRead
while ((bytesRead = input.read(buffer)) != -1) {
output.write(buffer, 0, bytesRead)
}
output.flush()
writer.append("\r\n--${boundary}--\r\n")
writer.flush()
writer.close()
input.close()
if (conn.responseCode in [200, 201]) {
KeywordUtil.logInfo("파일 첨부 완료: ${file.name}")
} else {
KeywordUtil.logInfo("파일 첨부 실패: ${conn.responseCode}")
}
}
private Properties loadProps() {
Properties props = new Properties()
File file = new File(RunConfiguration.getProjectDir() + "/Include/resources/console.properties")
if (file.exists()) {
props.load(new FileInputStream(file))
}
return props
}
}
6. Katalon Studio에서 Test Suite 실행 후 결과 확인
LoginSuccess 케이스가 실패하도록 변경 후 실행했습니다.
주의 ) 기존에 존재하던 이슈가 Done(완료) 상태인 경우 중복 체크를 진행하지 않습니다.
모든 상태에 대한 중복 체크를 원할 경우 스크립트 내 findExistingIssue 메소드의 주석을 참고해주세요.
7. Katalon Studio & Jira 연동 참고 영상
'Test Automation > Jira' 카테고리의 다른 글
Jira 스크럼 프로젝트 가이드 (0) | 2025.03.28 |
---|---|
Jira & Confluence 기초 가이드 (0) | 2025.03.28 |