흔한 QA 엔지니어

Katalon Studio & TestRail 연동하기 본문

Test Automation/TestRail

Katalon Studio & TestRail 연동하기

블로그 닉네임 입력 제한 수는 몇 자인가요? 2025. 3. 25. 16:12

TestRail

소프트웨어 테스트를 체계적으로 계획하고, 관리하고, 추적할 수 있게 도와주는 테스트 관리 도구입니다.

QA분들이라면 한번쯤 들어봤을거라 생각합니다. 왜 이 도구가 유명한걸까요?

TestRail 핵심 기능
테스트 케이스 관리 - 테스트 케이스를 작성하고, 섹션별로 체계적으로 분류 가능
테스트 실행 (Test Run) - 실행할 테스트 케이스만 선택해서 테스트 수행
결과 기록 - 테스트 Pass/Fail 여부, 실행자, 코멘트 기록 가능
리포트 & 히스토리 - 어떤 테스트가 언제 실패했는지, 누가 실행했는지 트래킹 가능
API 연동 - Katalon, Jenkins, Postman 등 자동화 도구와 연동 가능
팀 협업 - 여러 명이 동시에 테스트 관리 가능 (역할별 권한 설정 포함)

이외에도 여러 기능이 있지만 천천히 올려보도록 하겠습니다.

 

1. TestRail 내 신규 프로젝트 생성

Add Project 버튼을 클릭하여 신규 프로젝트 생성하기
테스트케이스 저장소 구성 방식 선택 후 Add Project 버튼 클릭

Use a single repository for all cases - 단일 버전, 단순 구조
Use a single repository with baseline support - 버전(branch) 별 관리 필요
Use multiple test suites to manage cases - 기능별, 모듈별 관리 필요

2. TestRail API 키 생성 & 활성화

My Settings > API Keys 메뉴에서 API 키 생성
Admin > Site Settings > Enable API 활성화

 

3. Test Listener 생성

Test Listener는 테스트 실행 전 후에 특정 코드를 실행할 수 있게 해주는 후킹 기능입니다.

Katalon Studio 실행 결과를 Testrail에 자동으로 보내는 역할을 합니다.

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.context.TestSuiteContext
import groovy.json.JsonSlurper
import groovy.json.JsonOutput
import internal.GlobalVariable

import java.net.HttpURLConnection
import java.net.URL
import java.util.Base64

class TestrailListener {

    // TestRail 설정
    private static final String TESTRAIL_URL = "https://사용자명.testrail.io/index.php?/api/v2/"
    private static final String TESTRAIL_USERNAME = "사용자 ID"
    private static final String TESTRAIL_API_KEY = "발급받은 API KEY 값"
    private static final int PROJECT_ID = 2 // 프로젝트 ID 수정
    private static final int SECTION_ID = 2 // SECTION ID 수정
    private static final String AUTH = Base64.getEncoder().encodeToString("${TESTRAIL_USERNAME}:${TESTRAIL_API_KEY}".getBytes("UTF-8"))

    private static Set<Integer> caseIdSet = [] as Set
    private static Map<Integer, Integer> resultMap = [:]  // [caseId: status_id]

	// 테스트 케이스 이름 추출 (예: Test Cases/Login/LoginSuccess → LoginSuccess)
    private String extractTitle(String fullId) {
        return fullId.split("/").last()
    }

    // TestRail 케이스 자동 검색 + 없으면 생성
    private Integer getOrCreateTestRailCaseIdByTitle(String title) {
        def url = new URL("${TESTRAIL_URL}get_cases/${PROJECT_ID}&section_id=${SECTION_ID}")
        HttpURLConnection conn = (HttpURLConnection) url.openConnection()
        conn.setRequestMethod("GET")
        conn.setRequestProperty("Authorization", "Basic " + AUTH)
        def response = conn.inputStream.getText("UTF-8")
        def json = new JsonSlurper().parseText(response)
        def cases = json?.cases ?: []

        def existing = cases.find { it.title == title }
        if (existing) {
            println("TestRail 케이스 찾음: '${title}' → ID: ${existing.id}")
            return existing.id
        }

        // 케이스 없으면 새로 생성
        println("🆕 TestRail에 케이스 없음 → 생성: '${title}'")
        def payload = JsonOutput.toJson([
            title: title,
            template_id: 1,
            type_id: 3,
            priority_id: 2,
            custom_steps: "자동 생성된 테스트 케이스입니다. (Katalon)"
        ])

        def createUrl = new URL("${TESTRAIL_URL}add_case/${SECTION_ID}")
        HttpURLConnection createConn = (HttpURLConnection) createUrl.openConnection()
        createConn.setRequestMethod("POST")
        createConn.setRequestProperty("Authorization", "Basic " + AUTH)
        createConn.setRequestProperty("Content-Type", "application/json")
        createConn.setDoOutput(true)
        createConn.outputStream.write(payload.getBytes("UTF-8"))

        def createRes = createConn.inputStream.getText("UTF-8")
        def created = new JsonSlurper().parseText(createRes)
        println("케이스 생성 완료 - ID: ${created.id}")
        return created.id
    }

    // 케이스별 결과 수집
    @AfterTestCase
    def afterTestCase(TestCaseContext testCaseContext) {
        def title = extractTitle(testCaseContext.getTestCaseId())
        def caseId = getOrCreateTestRailCaseIdByTitle(title)

        def status = testCaseContext.getTestCaseStatus() == "PASSED" ? 1 : 5
        caseIdSet << caseId
        resultMap[caseId] = status

        println("케이스 결과 수집 완료 → ${title} / ID: ${caseId} / 상태: ${status}")
    }

    // Test Run 생성 + 결과 업로드 + Run 닫기
    @AfterTestSuite
    def createRunAndSendResults(TestSuiteContext context) {
        if (caseIdSet.isEmpty()) {
            println("결과가 없습니다. Test Run 생략")
            return
        }

        def payload = JsonOutput.toJson([
            name: "Katalon Auto Run - ${context.getTestSuiteId()}",
            include_all: false,
            case_ids: caseIdSet.toList()
        ])

        def url = new URL("${TESTRAIL_URL}add_run/${PROJECT_ID}")
        HttpURLConnection connection = (HttpURLConnection) url.openConnection()
        connection.setRequestMethod("POST")
        connection.setRequestProperty("Authorization", "Basic " + AUTH)
        connection.setRequestProperty("Content-Type", "application/json")
        connection.setDoOutput(true)
        connection.outputStream.write(payload.getBytes("UTF-8"))

        def response = connection.inputStream.getText("UTF-8")
        def json = new JsonSlurper().parseText(response)
        GlobalVariable.runId = json.id
        println("Test Run 생성 완료 - ID: ${GlobalVariable.runId}")

        // 결과 전송
        caseIdSet.each { cid ->
            sendResultToTestRail(cid, resultMap[cid], "[자동 전송] 실행 결과")
        }

        // Run 닫기
        def closeUrl = new URL("${TESTRAIL_URL}close_run/${GlobalVariable.runId}")
        HttpURLConnection closeConn = (HttpURLConnection) closeUrl.openConnection()
        closeConn.setRequestMethod("POST")
        closeConn.setRequestProperty("Authorization", "Basic " + AUTH)
        closeConn.setRequestProperty("Content-Type", "application/json")
        closeConn.connect()
        if (closeConn.responseCode == 200) {
            println("Test Run 닫기 완료")
        } else {
            println("Run 닫기 실패: " + closeConn.errorStream?.text)
        }
    }

    // 결과 전송
    private void sendResultToTestRail(int caseId, int status, String comment) {
        def runId = GlobalVariable.runId
        def url = new URL("${TESTRAIL_URL}add_result_for_case/${runId}/${caseId}")
        HttpURLConnection conn = (HttpURLConnection) url.openConnection()
        conn.setRequestMethod("POST")
        conn.setRequestProperty("Authorization", "Basic " + AUTH)
        conn.setRequestProperty("Content-Type", "application/json")
        conn.setDoOutput(true)

        def payload = JsonOutput.toJson([
            status_id: status,
            comment: comment
        ])
        conn.outputStream.write(payload.getBytes("UTF-8"))

        def response = conn.inputStream.getText("UTF-8")
        println("결과 전송 완료 - 케이스 $caseId: $response")
    }
}

 

5. 전역 변수 설정

default > Add 버튼 클릭하여 runId = 0 전역 변수 설정

전역 변수 설정 이유
@BeforeTestSuite에서 받은 runId를 @AfterTestCase method로 넘겨주기 위해서입니다.

 

6. Katalon Studio에서 Test Suite 실행 후 결과 확인

아무것도 없던 대시보드에서
이렇게 잘 들어온걸 확인했습니다!
테스트 케이스도 자동 생성~

 

주의) 소스 코드는 Test Case ID가 아닌 이름 기반으로 작성되었기 때문에

중복 체크를 위해서는 기존 테스트 케이스의 이름과 동일해야 합니다.