Robot Framework tags와 테스트 메타데이터 차이

Robot Framework 7.5 테스트 메타데이터 사용법과 태그 차이

3줄 요약

  • 테스트마다 붙여 둔 Issue: QA-1042 태그를 7.5의 [Metadata]로 옮겨도 되는지가 이 글의 질문입니다.
  • 옮기면 태그 통계 표가 557행에서 0행으로 줄지만, --include 필터와 파일 단위 일괄 지정과 --tagstatlink 이슈 링크를 함께 잃습니다.
  • 다만 이 판단보다 먼저 확인할 것이 있습니다. 리포트를 만드는 rebot이 7.4.2 이하면 exit 252로 멈추고 파일을 한 개도 남기지 않습니다.

"리드님, 이번에 테스트마다 메타데이터를 붙일 수 있게 됐다는데 우리 Issue: 태그 다 옮길까요?"

어제 릴리스 노트를 본 팀원이 물었습니다. 저희 스위트에는 테스트 하나당 이슈 키 하나가 태그로 붙어 있고, 그 태그가 리포트의 태그 통계 표를 수백 행으로 불려 놓은 상태였거든요. 옮기면 그 표가 깨끗해집니다. 그래서 바로 옮기자고 답하려다가, 릴리스 노트에 없는 것부터 세어 보기로 했습니다.

컨테이너에 7.5와 7.4.2를 각각 다른 가상환경으로 깔고 같은 스위트를 양쪽에 돌렸습니다. 결과를 먼저 표로 놓고 시작하겠습니다.

무엇을 태그에 남기고 무엇을 메타데이터로 옮기는가

Robot Framework 7.5는 2026년 9월 14일에 나왔습니다. PyPI 업로드 시각은 2026-09-14T13:12:56.892342Z이고 KST로는 9월 14일 22시 12분입니다. 55건이 들어갔고 그중 이번 글이 다루는 것은 #4409 하나입니다.

아래가 실측 결과를 판단 기준으로 옮긴 표입니다. 이후 섹션은 이 표의 각 행이 왜 그렇게 정해졌는지를 채웁니다.

항목 태그에 남길 것 메타데이터로 옮길 것 근거
실행을 고르는 값 smoke, payment, flaky — --include·--exclude는 태그에만 붙습니다
이슈·요구사항 키 이슈 추적기 링크가 필요하면 태그 링크가 필요 없으면 메타데이터 --tagstatlink가 태그 전용입니다
담당자·환경·빌드 번호 — 메타데이터 통계 집계 대상이 아니어야 표가 깨끗합니다
파일 전체에 공통인 값 Test Tags로 일괄 지정 손으로 테스트마다 Test Metadata 설정은 없습니다
CI 도구에 넘길 값 어느 쪽도 아님 어느 쪽도 아님 xunit.xml에 둘 다 실리지 않습니다

마지막 행이 제일 먼저 확인해야 할 칸입니다. 태그든 메타데이터든 젠킨스나 GitLab이 읽는 JUnit XML에는 들어가지 않으므로, CI 화면에 이슈 키를 띄우려던 계획이라면 둘 중 무엇을 골라도 소용이 없습니다.

실측 환경

Python 3.10.12, Ubuntu 22.04.5 aarch64, 4코어 컨테이너입니다. robotframework==7.5와 robotframework==7.4.2를 서로 다른 가상환경에 설치해 같은 파일을 양쪽에 돌렸습니다.

*** Settings ***
Metadata    Suite Owner    qa-platform

*** Test Cases ***
결제 승인 정상 흐름
    [Documentation]    카드 승인 성공 경로
    [Metadata]    Issue          QA-1042
    [Metadata]    Requirement    REQ-PAY-07
    [Metadata]    Owner          haru
    [Tags]    smoke    payment
    Log    ok

결과 파일에서 실제로 달라지는 것

[Metadata]는 output.xml의 <test> 아래에 <meta> 요소로 들어갑니다. 태그가 <tag>로 들어가는 자리 바로 위입니다.

<test id="s1-t1" name="결제 승인 정상 흐름" line="6">
  <doc>카드 승인 성공 경로</doc>
  <meta name="Issue">QA-1042</meta>
  <meta name="Owner">haru</meta>
  <meta name="Requirement">REQ-PAY-07</meta>
  <tag>payment</tag>
  <tag>smoke</tag>
  <status status="PASS" start="2026-09-15T17:13:26.503151" elapsed="0.000846"/>
</test>

여기서 순서를 보십시오. 스위트 파일에는 Issue, Requirement, Owner 순으로 적었는데 출력은 Issue, Owner, Requirement입니다. 이름 기준으로 정렬되므로, 읽는 사람이 보게 될 순서를 작성 순서로 조절할 수 없습니다. 중요한 값을 위에 두고 싶다면 이름 앞에 번호를 붙이는 식으로 우회해야 합니다.

태그 통계 표가 557행에서 0행이 됩니다

테스트 500개짜리 스위트를 세 벌 만들어 비교했습니다. 아무것도 안 붙인 것, 테스트마다 고유 태그 3개를 붙인 것, 같은 값을 메타데이터 3개로 붙인 것입니다.

조건 태그 통계 항목 output.xml log.html report.html 실행 시간(5회 최소)
아무것도 없음 0개 192,562B 280,287B 268,515B 0.27초
고유 태그 3개 557개 253,939B 336,616B 323,306B 0.32초
메타데이터 3개 0개 246,241B 302,296B 287,997B 0.31초

557이라는 숫자는 이슈 태그 500개와 요구사항 태그 50개, 담당자 태그 7개를 더한 값입니다. 테스트마다 다른 값을 태그로 넣으면 통계 표가 테스트 수만큼 늘어나고, 그 표는 한 행에 테스트 하나씩만 담긴 채 리포트 맨 위를 차지합니다. 메타데이터는 통계 집계 대상이 아니라서 이 표가 0행입니다.

리포트 파일 크기도 35KB 차이가 납니다. 다만 태그 쪽도 --tagstatexclude "issue-QA-*"를 주면 286,304B까지 내려가므로, 파일 크기만 보면 메타데이터로 옮길 이유가 되지 못합니다. 옵션 한 줄로 해결되는 문제입니다.

값 처리 방식은 스위트 메타데이터와 같습니다

변수는 해석됩니다. [Metadata] Issue QA-${BUILD}는 QA-1042로 저장됐습니다. 같은 이름을 두 번 적으면 뒤에 적은 값이 이깁니다. 값을 비워 두면 <meta name="빈값"/>으로 이름만 남습니다. 문서 서식도 그대로 동작해서 [https://jira.example.com/QA-1042|QA-1042]는 log.html에서 실제 <a href> 링크로 렌더됐습니다.

옮기고 나면 못 하게 되는 일

여기부터가 팀원에게 바로 답하지 않은 이유입니다.

필터가 없습니다

--include와 --exclude는 태그만 봅니다. 메타데이터로 테스트를 고르는 옵션은 7.5의 --help 어디에도 없습니다. --metadata가 있긴 한데 도움말 원문이 Set metadata of the top level suite라서 최상위 스위트 전용이고, 그나마 값을 읽는 것이 아니라 설정하는 옵션입니다.

이슈 키로 테스트를 골라 돌리는 습관이 있다면 옮기는 순간 그 경로가 끊깁니다. 저희 팀은 장애 재현 때 --include QA-1042를 자주 쓰는데, 이 한 가지 때문에 이슈 키는 태그에 남기기로 했습니다.

파일 단위 일괄 지정이 없습니다

태그에는 Test Tags가 있어서 파일 하나의 모든 테스트에 같은 태그를 한 줄로 붙일 수 있습니다. 메타데이터에는 대응하는 설정이 없습니다. 흉내 내려고 Test Metadata를 적어 봤더니 Error in file '...' on line 3: Non-existing setting 'Test Metadata'.가 나왔습니다.

조용히 넘어가는 함정이 하나 붙어 있습니다. 이 오류가 나도 테스트는 그대로 실행되고 종료 코드는 0입니다. 실행 로그를 눈으로 안 보는 CI라면 팀 이름이 하나도 안 붙은 채로 초록불이 켜집니다. output.xml의 <errors> 섹션을 확인하는 단계가 파이프라인에 없다면 여기서 새는지 모릅니다.

이슈 추적기 링크가 사라집니다

--tagstatlink는 태그 값을 이슈 추적기 주소로 바꿔 주는 옵션입니다. --tagstatlink "QA-*:https://jira.example.com/browse/%1:Issue Tracker"를 주고 돌리니 report.html에 jira.example.com/browse/1042가 실제 링크로 들어갔습니다. %1에는 태그 전체가 아니라 *가 잡아낸 부분만 들어갑니다.

메타데이터에는 이 옵션이 없습니다. 링크를 넣으려면 값 안에 [주소|제목] 서식을 손으로 써야 하는데, 그러면 이슈 키가 값 문자열 안에 파묻혀 나중에 기계로 뽑기 어려워집니다.

여기서 태그 쪽 함정도 하나 확인했습니다. 태그 이름 자체에 콜론이 들어가면(issue:QA-1042) 패턴 문자열 issue:QA-*:주소:제목의 구분이 깨져서 링크가 안 붙습니다. 오류도 경고도 없이 그냥 안 붙습니다. 이슈 태그에 접두사를 붙이는 관행이 흔한데, 그 팀이라면 접두사를 하이픈으로 바꿔야 합니다.

CI 리포트에는 어느 쪽도 실리지 않습니다

--xunit으로 뽑은 파일에서 smoke를 찾으면 0건, QA-1042를 찾아도 0건입니다. 실리는 것은 스위트 메타데이터뿐입니다.

<testcase classname="Meta" name="결제 승인 정상 흐름" time="0.001">
</testcase>
<properties>
<property name="Suite Owner" value="qa-platform"/>
</properties>

테스트 단위 정보를 CI 화면으로 보내려면 결국 리스너나 결과 모델을 거쳐야 합니다. 이 구조는 pytest 재시도 기록이 JUnit XML에 남지 않는 문제와 같습니다. 프레임워크가 아는 것과 JUnit XML 형식이 담을 수 있는 것이 다르고, 그 차이는 대개 직접 짜서 메웁니다.

다행히 읽는 쪽은 쉽습니다. 리스너 v3에서 result.metadata가 그냥 딕셔너리로 나옵니다.

# 리스너 v3 — 테스트 메타데이터를 읽어 CI 로 내보내는 최소 형태
ROBOT_LISTENER_API_VERSION = 3

rows = []

def end_test(data, result):
    # result.metadata 는 이름 기준으로 정렬된 딕셔너리입니다
    rows.append((result.name, dict(result.metadata), list(result.tags), result.status))

def close():
    for name, meta, tags, status in rows:
        print(f"{name}\t{meta.get('Issue', '')}\t{status}")

실행 결과는 아래와 같았습니다. output.xml을 나중에 ExecutionResult로 열어도 같은 값이 나옵니다.

테스트 result.metadata result.tags 상태
결제 승인 정상 흐름 {'Issue': 'QA-1042', 'Owner': 'haru', 'Requirement': 'REQ-PAY-07'} ['payment', 'smoke'] PASS
결제 취소 정상 흐름 {'Issue': 'QA-1043'} ['payment'] PASS
실패하는 테스트 {'Issue': 'QA-1044'} ['flaky', 'payment'] FAIL

버전이 섞이면 어디서 멈추는가

앞의 비교는 전부 7.5만 쓸 때의 이야기입니다. 실무에서는 실행기와 리포트 생성기가 다른 곳에 있는 경우가 흔하고, 저희도 그렇습니다.

7.4.2로 실행하면 테스트가 실패합니다

경고가 아니라 실패입니다. [Metadata]가 붙은 테스트 3개가 전부 이 메시지로 죽었습니다.

결제 승인 정상 흐름 :: 카드 승인 성공 경로                            | FAIL |
Setting 'Metadata' is not allowed with tests or tasks.

한 명이라도 구버전으로 로컬 실행을 하면 그 사람 화면은 전부 빨간색이 됩니다. 원인이 코드가 아니라 자기 가상환경이라는 걸 알아채기까지 시간이 걸립니다.

rebot이 구버전이면 파일이 한 개도 안 나옵니다

이게 이번 실측에서 제일 아팠던 부분입니다. 7.5로 만든 output.xml을 7.4.2의 rebot에 넣으면 이렇게 됩니다.

조합 결과 종료 코드 생성 파일
7.4.2 rebot ← 7.5 출력(메타데이터 있음) Incompatible child element 'meta' for 'test'. 252 0개
7.4.2 rebot ← 7.5 출력(메타데이터 없음) 정상 1(실패 테스트 수) 3개
7.5 rebot ← 7.4.2 출력 정상 3(실패 테스트 수) 3개
7.4.2 ExecutionResult ← 7.5 출력 DataError 발생 — —

252는 Robot Framework가 잘못된 입력에 쓰는 코드입니다. log.html도 report.html도 만들어지지 않습니다. 병렬로 나눠 돌린 조각을 rebot으로 합치는 파이프라인이라면, 합치는 단계에서 통째로 무너지고 결과를 아무것도 못 봅니다. 리포트 산출물이 조용히 끊기는 양상은 Allure 히스토리를 이어 붙이지 않았을 때와 비슷한데, 그때는 표가 한 칸으로 줄었을 뿐이고 이번에는 파일 자체가 없습니다.

버전 표시로는 감지할 수 없습니다

output.xml의 schemaversion 속성을 확인해 봤는데 7.4.2도 5, 7.5도 5였습니다. 새 자식 요소가 생겼는데 스키마 버전은 그대로입니다. 결과 파일을 받아 처리하는 쪽에서 버전 숫자만 보고 호환 여부를 판단하는 코드가 있다면 그 코드는 이 변경을 못 잡습니다.

판정 기준은 결국 파일 안에 <meta가 있는지를 직접 보는 것입니다. 파이프라인 앞단에 한 줄 넣어 두는 편이 낫습니다.

# rebot 을 돌리기 전에 실행기와 rebot 의 버전이 같은지 확인합니다
python -m robot --version            # 실행에 쓴 버전
python -m robot.rebot --version      # 리포트 생성에 쓸 버전

# 테스트 단위 메타데이터가 들어 있는지 먼저 봅니다 (있으면 7.5 이상 rebot 필요)
if grep -qE '<test [^>]*>(.|\n)*?<meta name=' output.xml; then
  echo "테스트 메타데이터 있음 — rebot 7.5 이상이 필요합니다"
fi

같이 따라오는 변경 중 CI를 건드리는 것

메타데이터만 보고 올리면 놓치는 것이 있습니다. 태그 패턴 문법이 조용히 경고를 내기 시작했습니다.

[ WARN ] Problems when including tests by tags: The behavior of tag pattern
'payment&smoke' will change in Robot Framework 8.0: Boolean operator '&' is
deprecated, use 'AND' instead.

7.4.2에서는 같은 명령에 경고가 없었습니다. 종료 코드는 0이고 테스트 선택도 정상이니 당장 깨지지는 않지만, 경고를 오류로 취급하는 파이프라인이라면 올리는 날 빨간불이 켜집니다. --include "payment AND smoke"로 바꾸면 사라집니다.

릴리스 노트는 XORY처럼 붙여 쓴 OR 패턴도 폐기 대상으로 적고 있는데, 제 환경에서 --include "paymentORsmoke"를 돌렸을 때는 경고가 뜨지 않았습니다. 어떤 조건에서 뜨는지는 확인된 근거가 없습니다.

그 밖에 Telnet 라이브러리와 Testdoc 도구가 폐기 예정으로 바뀌었고, 내부 TimeoutExceeded가 Exception 대신 BaseException을 상속하도록 바뀌었습니다. 마지막 것은 커스텀 라이브러리에서 except Exception:으로 감싸 두고 타임아웃까지 삼키던 코드가 있다면 동작이 달라집니다. 자세한 목록은 7.5 릴리스 노트에 있습니다.

우리 팀이 이번 주에 어디까지 할 것인가

저는 이슈 키를 태그에 그대로 두기로 했고요, 담당자와 환경 두 가지만 메타데이터로 내렸습니다. 이유는 단순합니다. 이슈 키는 골라 돌릴 일이 있고 담당자는 없습니다.

옮길 값이 정해졌다면 변환은 기계로 하면 됩니다. Issue: QA-1042 형태의 태그를 골라 [Metadata] 줄로 바꾸고 나머지 태그는 그대로 두는 스크립트입니다.

# 기존 `Issue: QA-1042` 형태의 태그를 [Metadata] 로 옮기는 변환기
import re, sys, pathlib

PREFIX = re.compile(r'^(?P<key>Issue|Requirement|Owner):\s*(?P<val>.+)$', re.I)

def convert(text: str) -> str:
    out, moved = [], 0
    for line in text.splitlines():
        m = re.match(r'^(\s+)\[Tags\]\s+(.*)$', line)
        if not m:                      # [Tags] 줄이 아니면 그대로 둡니다
            out.append(line)
            continue
        indent, rest = m.group(1), m.group(2)
        keep, metas = [], []
        for tag in re.split(r'\s{2,}|\t', rest.strip()):
            t = PREFIX.match(tag.strip())
            if t:                      # 접두사가 붙은 태그만 메타데이터로 옮깁니다
                metas.append((t.group('key').capitalize(), t.group('val').strip()))
                moved += 1
            elif tag.strip():
                keep.append(tag.strip())
        for k, v in metas:
            out.append(f'{indent}[Metadata]    {k}    {v}')
        if keep:                       # 남은 태그가 있을 때만 [Tags] 줄을 다시 씁니다
            out.append(f'{indent}[Tags]    ' + '    '.join(keep))
    sys.stderr.write(f'옮긴 항목 {moved}개\n')
    return '\n'.join(out) + '\n'

if __name__ == '__main__':
    p = pathlib.Path(sys.argv[1])
    p.write_text(convert(p.read_text(encoding='utf-8')), encoding='utf-8')

네 줄짜리 예제 파일에 돌려 항목 2개를 옮겼고, 변환 후 실행에서 <meta name="Issue">QA-1042</meta>와 남은 태그 2개가 모두 정상으로 나왔습니다. 다만 이 스크립트는 [Tags]가 여러 줄로 이어진 경우(... 계속 줄)를 처리하지 않습니다. 그런 파일이 있으면 손으로 보셔야 합니다.

올리기 전에 볼 것은 아래 다섯 줄이면 충분합니다.

□ rebot 을 돌리는 단계의 Robot Framework 버전이 실행 단계와 같은가
□ 결과 파일을 읽는 사내 도구가 <test> 아래 <meta> 요소를 만나도 죽지 않는가
□ --include 로 골라 돌리는 값이 메타데이터로 넘어가 있지는 않은가
□ Test Metadata 를 적어 두고 오류를 못 본 파일이 없는가 (output.xml 의 <errors> 확인)
□ --include 에 & 를 쓴 명령이 파이프라인에 남아 있지 않은가

이 다섯 줄 중 첫 줄만 통과하면 나머지는 천천히 해도 됩니다. 첫 줄이 막히면 리포트가 아예 안 나오니까요.


댓글

이 블로그의 인기 게시물

옵시디언 3기기 동기화, 유료 결제 없이 구글 드라이브로 끝내기

최저가 주유소, 얼마나 돌아가면 손해일까?

회의시간 줄이는 법, 개수보다 배치 먼저

유류세 인하 2개월 연장, 주유소 가격 실제 인하폭 계산

국민연금 추납 조회, 미루면 얼마나 더 낼까?