Showing Posts From
품질
- 25 Dec, 2025
야근하며 수정한 테스트: 내일 아침 더 꼬여 있다
오후 6시, 급한 불 배포가 내일이다. PM이 슬랙에 멘션을 날렸다. "J님, 새 결제 기능 자동화 테스트 내일 아침까지 가능할까요?" 가능하냐고? 불가능하다. 하지만 대답은 정해져 있다. "네, 해보겠습니다." 정상적으로 하면 이틀 걸린다. API 스펙 확인하고, 테스트 시나리오 짜고, Page Object 설계하고, 스크립트 작성하고, 리뷰받고. 그게 정석이다. 지금 시간은 6시 반. 퇴근 시간 지났다. 옆자리 개발자들 하나둘 나간다. "먼저 갈게요." "네, 수고하세요." 모니터 3개를 켰다. 왼쪽엔 API 문서, 가운데엔 IDE, 오른쪽엔 테스트 실행 화면. 커피 한 잔 더 탔다. 네 번째다.급하게 짜는 코드의 특징 8시다. 일단 돌아가는 걸 만들었다. def test_payment(): driver.get("https://...") driver.find_element_by_id("btn_pay").click() time.sleep(5) # 일단 5초 기다림 assert "success" in driver.page_source보기 싫다. 하드코딩 범벅이다. sleep으로 때웠다. Page Object도 없다. 그냥 element 직접 찾는다. 하지만 돌아간다. 지금은 그게 중요하다. 다음 시나리오. 카드 결제 실패 케이스. def test_payment_fail(): driver.get("https://...") driver.find_element_by_id("card_num").send_keys("1234") # 잘못된 카드 driver.find_element_by_id("btn_pay").click() time.sleep(5) assert "실패" in driver.page_source복붙했다. 앞에 코드 거의 똑같다. URL도 하드코딩, 셀렉터도 하드코딩. fixture도 안 만들었다. "나중에 리팩토링하면 되지." 이 말을 몇 번 했는지 모른다. 경력 7년차가 이러면 안 되는데. 9시 반. 테스트 케이스 5개 짰다. 전부 이런 식이다. 코드 중복 천지. 하드코딩 천국. sleep 축제. 돌려봤다. 3개 성공, 2개 실패. 실패한 이유를 봤다. 타이밍 이슈다. sleep(5)가 부족했다. 10으로 늘렸다. 다시 돌렸다. 4개 성공, 1개 실패. 또 타이밍이다. sleep(15)로 늘렸다. 전부 통과했다. "좋아, 일단 된다." 커밋했다. 메시지는 "add payment test cases". 상세 설명 없다. 급하니까.다음날 아침의 충격 출근했다. 10시 10분. 늦었다. 어젯밤 12시에 퇴근했다. 슬랙을 켰다. 알림 7개. 전부 Jenkins 실패 알림이다. "어? 어제 다 통과했는데?" CI 로그를 봤다. 전부 타임아웃이다. selenium.common.exceptions.TimeoutException Element not found: btn_pay아, 맞다. 하드코딩했지. 개발자가 어젯밤에 버튼 ID를 바꿨다. btn_pay에서 button_payment로. 슬랙에 개발자 메시지가 있다. "결제 버튼 컴포넌트 리팩토링했습니다. UI 변경 없어요." UI는 안 바뀌었다. 하지만 ID는 바뀌었다. 그걸 하드코딩했으니 깨진다. 수정했다. 5군데. 다시 푸시. 다시 실행. 또 실패. 이번엔 다른 에러. AssertionError: 'success' not found in page source페이지 구조가 바뀌었다. 성공 메시지가 모달로 뜬다. page_source엔 안 보인다. 수정했다. explicit wait 추가. 모달 찾는 로직 추가. 그런데 다른 테스트도 깨졌다. 전부 같은 패턴으로 짰으니까. 10시 반. 벌써 30분 날렸다. PM이 슬랙에 물었다. "J님 테스트 통과했나요? 배포 11시예요." "조금만요, 거의 다 됐습니다." 거짓말이다. 아직 멀었다.기술 부채의 이자 점심시간이다. 배포는 연기됐다. 내 때문이다. 테스트는 겨우 통과시켰다. 하지만 더 꼬였다. 어젯밤 코드 위에 오늘 아침 땜질을 했다. 나쁜 코드 위에 더 나쁜 코드. def test_payment(): driver.get("https://...") time.sleep(3) # 페이지 로딩 btn = WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.ID, "button_payment")) ) btn.click() time.sleep(5) # 결제 처리 try: modal = driver.find_element_by_class_name("modal_success") assert "success" in modal.text except: assert "success" in driver.page_source # fallbacksleep이랑 explicit wait이 섞였다. try-except로 땜질했다. fallback 로직도 이상하다. 이게 5개 테스트에 복붙되어 있다. 팀장이 코드 리뷰를 달았다. "J님, 이거 Page Object 패턴 안 쓴 이유가 있나요?" "급해서 일단 돌아가게 만들었습니다. 리팩토링할게요." "배포 끝나고 꼭 해주세요. 이렇게 가면 유지보수 힘듭니다." 안다. 나도 안다. 하지만 리팩토링할 시간이 있을까? 다음주에 또 급한 기능이 온다. 또 급하게 짤 것이다. 또 부채가 쌓일 것이다. 악순환의 시작 2주 지났다. 결제 테스트는 그대로다. 리팩토링 할 시간이 없었다. 새 기능 테스트 짜느라. 그것도 급하게 짰다. 같은 패턴으로. 이제 급한 코드가 10개 파일이다. CI 실행시간이 20분에서 35분으로 늘었다. sleep 때문이다. Flaky 테스트가 생겼다. 타이밍 이슈로 가끔 실패한다. 개발자들이 불평한다. "테스트 왜 이렇게 느려요?" "가끔 이유 없이 실패하던데, 테스트 문제 아닌가요?" 변명할 수 없다. 내 코드가 문제다. 새로 온 QA 후배가 내 코드를 봤다. "선배님, 이 테스트 왜 이렇게 짜셨어요? Page Object 안 쓰셨네요?" "응, 급해서... 나중에 고칠 거야." "그럼 제가 새로 짤 테스트도 이렇게 짜면 돼요?" "아니, 너는 제대로 짜. 이건... 나쁜 예시야." 후배한테 나쁜 예시를 보여주고 있다. 부채의 이자는 복리다 한 달 지났다. 급한 코드가 20개 파일이 됐다. 이제 신규 기능 하나 추가하려면 기존 코드를 봐야 한다. 복붙할 게 많으니까. 그러다 나쁜 패턴도 같이 복붙된다. API가 바뀌면 20군데를 고쳐야 한다. 하드코딩했으니까. UI 컴포넌트가 바뀌면 테스트 10개가 깨진다. 셀렉터를 직접 박았으니까. CI 실행시간은 50분이다. sleep 총합이 5분이 넘는다. 테스트 커버리지는 올랐다. 65%. 좋아 보인다. 하지만 품질은 떨어졌다. 테스트를 믿을 수 없다. 개발자들이 테스트 실패를 무시한다. "저 테스트 원래 잘 깨지잖아요. 그냥 재실행하면 돼요." 틀린 말이 아니다. PM이 물었다. "테스트 자동화 많이 했는데, 버그는 왜 여전히 나와요?" 대답할 수 없었다. 리팩토링의 두려움 주말이다. 집에서 쉬고 있다. 리팩토링을 해야 한다는 생각이 머리를 떠나지 않는다. 계획을 세웠다.Page Object 패턴 적용 공통 fixture 만들기 sleep 전부 explicit wait로 교체 하드코딩된 셀렉터 전부 상수로 분리 중복 코드 제거최소 3일은 걸린다. 풀타임으로. 하지만 3일을 쓸 수 있을까? 그 사이에 버그가 나면? 긴급 패치가 들어가면? 새 기능이 또 급하게 오면? 리팩토링 도중에 기존 테스트는 돌아가지 않는다. CI가 깨진다. 팀장한테 말해야 한다. "3일간 다른 일 못 합니다. 리팩토링합니다." 승인이 날까? 더 큰 두려움이 있다. 리팩토링 하다가 기존 테스트를 깨뜨리면 어떡하지? 지금 코드가 나쁘지만, 어쨌든 돌아간다. 고치다가 더 망가뜨리면? "그냥 새로 짜는 게 나을까?" 하지만 새로 짜는 것도 3일이다. 똑같다. 결국 아무것도 안 했다. 월요일이 됐다. 작은 리팩토링의 시작 월요일 오전. 회의가 없다. 결심했다. 전체는 못 해도 일부는 한다. 가장 자주 깨지는 테스트 파일 하나만. test_payment.py. 2시간 걸렸다. Page Object 만들었다. class PaymentPage: BTN_PAYMENT = (By.ID, "button_payment") MODAL_SUCCESS = (By.CLASS_NAME, "modal_success") def __init__(self, driver): self.driver = driver def click_payment(self): element = WebDriverWait(self.driver, 10).until( EC.element_to_be_clickable(self.BTN_PAYMENT) ) element.click() def get_result_message(self): modal = WebDriverWait(self.driver, 10).until( EC.visibility_of_element_located(self.MODAL_SUCCESS) ) return modal.text테스트 코드가 깔끔해졌다. def test_payment_success(payment_page): payment_page.click_payment() result = payment_page.get_result_message() assert "success" in resultsleep이 사라졌다. 중복도 사라졌다. 읽기 쉽다. 돌려봤다. 통과했다. CI에도 통과했다. 실행 시간이 3분 줄었다. 기분이 좋다. 오후에 또 다른 파일 하나. test_cart.py. 같은 방식으로. 2시간 반 걸렸다. 이번엔 조금 빨랐다. 패턴을 알았으니까. 하루에 파일 2개씩 리팩토링한다. 10일이면 20개 끝난다. 점진적 개선 2주 지났다. 급한 코드의 절반을 고쳤다. 완벽하지 않다. 아직 개선할 부분이 많다. 하지만 확실히 나아졌다. CI 실행시간이 35분으로 줄었다. Flaky 테스트가 5개에서 2개로 줄었다. 코드 리뷰에서 지적받는 횟수가 줄었다. 후배가 말했다. "선배님, 이번 코드는 이해하기 쉬워요. 이렇게 짜면 되는 거죠?" "응, 이게 맞아." 개발자가 슬랙에 썼다. "테스트 빨라졌네요. 뭐 하셨어요?" "리팩토링 좀 했습니다." "좋네요. ㅎㅎ" 작은 칭찬이지만 기분 좋다. 팀장이 1on1에서 말했다. "테스트 코드 품질 올라간 거 보여요. 꾸준히 하고 있죠?" "네, 조금씩 하고 있습니다." "급하다고 대충 짜는 거, 나도 이해해요. 하지만 결국 돌아오더라고요." "맞아요. 배웠습니다." "이번 분기 평가에 반영할게요. 수고하고 있어요." 배운 것들 급한 코드는 빚이다. 언젠가 갚아야 한다. 이자는 복리다. 급한 코드 위에 급한 코드가 쌓인다. 한 번에 갚을 순 없다. 파산한다. 조금씩 갚는 수밖에 없다. "나중에 리팩토링하면 돼"는 거짓말이다. 나중은 안 온다. 더 급한 일만 온다. 그래서 매일 조금씩 해야 한다. 출근해서 1시간. 파일 하나. 함수 몇 개. 그럼 부채가 줄어든다. 천천히지만 확실하게. 완벽한 코드를 짤 순 없다. 급할 때도 있다. 하지만 부채를 인정하고 관리해야 한다. 급하게 짰으면 주석을 남긴다. "TODO: refactor to Page Object". 급하게 짰으면 이슈를 만든다. "기술 부채: test_payment 리팩토링". 급하게 짰으면 다음날 1시간 투자한다. 조금이라도 개선한다. 그게 쌓이면 달라진다. 지금 내 코드는 한 달 전보다 낫다. 완벽하지 않다. 아직 급한 코드가 10개 남았다. 하지만 매일 조금씩 나아진다. 그걸로 충분하다.어젯밤 야근해서 짠 코드. 오늘 아침엔 더 꼬여 있다. 그게 부채의 이자다. 하지만 매일 조금씩 갚으면, 언젠가 0이 된다. 그걸 믿고 오늘도 리팩토링 한다.
- 12 Dec, 2025
테스트 리포트를 숫자가 아닌 스토리로 전달하기
테스트 리포트를 숫자가 아닌 스토리로 전달하기 7시 30분, 리포트 정리 시작 매일 저녁 7시. 내 업무 루틴은 리포트 정리로 끝난다. Jenkins에서 떨어진 테스트 결과. Pytest 리포트. Allure 대시보드. 숫자만 잔뜩이다. Pass 2,847개. Fail 23개. Skip 15개. 이걸 그대로 공유하면? 아무도 안 본다. 작년까지 나도 그랬다. 엑셀에 숫자 복붙. 컬러 코딩. 메일 발송. 다음 날 개발팀장이 물었다. "그래서 릴리즈 해도 돼요?" 멘탈이 나갔다. 2,847개 테스트를 통과했는데. 이걸 어떻게 설명하지. 그때부터 바꿨다. 리포트는 숫자가 아니라 이야기다.CTO가 물어본 질문 리포트 방식을 바꾸게 된 계기. 3개월 전 월례 회의. CTO가 내 리포트를 보더니 물었다. "테스트 커버리지 85%면 좋은 건가요? 릴리즈 해도 되나요?" 순간 막혔다. 기술적으론 좋은 수치다. 업계 평균도 70% 정도니까. 근데 "좋다"고 말할 수 없었다. 결제 모듈 테스트가 3개 실패 중이었거든. 85%라는 숫자는 의미 없었다. 나머지 15%가 어디냐가 중요했다. 그날 깨달았다. 임원진은 QA 전문가가 아니다. 숫자보다 리스크를 알고 싶어 한다. "릴리즈해도 안전한가"가 궁금한 거다. 다음 회의부터 접근을 바꿨다. "결제 모듈 테스트 3개 실패. 릴리즈 블로킹 이슈. 다른 건 문제없음." CTO 표정이 달라졌다. "결제는 고쳐야죠. 나머진 배포 가능?" 드디어 대화가 됐다. 숫자 대신 컨텍스트 리포트 구조를 완전히 뜯어고쳤다. Before:전체 테스트: 2,885개 Pass: 2,847개 (98.7%) Fail: 23개 (0.8%) Skip: 15개 (0.5%)After:릴리즈 블로킹: 3개 (결제 API 타임아웃) 주의 필요: 5개 (UI 깨짐, UX 영향) 알려진 이슈: 15개 (백로그 등록됨) 안전: 2,847개 기능 검증 완료같은 데이터다. 전달 방식만 바뀌었다. 근데 반응이 180도 달라졌다. 개발팀장: "결제는 내일 아침까지 고칠게요." PM: "UI 깨진 건 어느 화면이에요? 급한가요?" CTO: "알려진 이슈 15개는 언제 해결 예정?" 구체적인 액션이 나왔다. 이게 진짜 리포트다.시각화는 도구일 뿐 처음엔 시각화에 집착했다. Grafana 대시보드 만들었다. 그래프 10개. 실시간 업데이트. 컬러풀하고 멋있었다. 근데 아무도 안 봤다. 문제는 간단했다. 너무 많았다. 임원진은 대시보드 10분 볼 시간이 없다. 1분 안에 핵심만 알고 싶어 한다. 지금은 3가지만 보여준다. 1. 신호등 (Traffic Light)빨강: 릴리즈 블로킹. 절대 배포 금지. 노랑: 주의 필요. 비즈니스 판단 필요. 초록: 안전. 배포 가능.한눈에 본다. 오늘이 빨강인지 초록인지. 2. 트렌드 (Trend)최근 2주 테스트 실패율 그래프 갑자기 튄 날 있으면 설명 추가 "10일 - API 배포로 인한 일시적 실패"패턴을 본다. 점점 나아지는지 악화되는지. 3. 핫스팟 (Hotspot)가장 자주 실패하는 모듈 Top 3 테스트 불안정한 기능 표시 "로그인 모듈 - 이번 주 4번 실패"리스크 영역을 본다. 어디에 집중해야 하는지. 이 3개면 충분하다. 나머지는 디테일 페이지에 넣어뒀다. 궁금하면 클릭해서 보면 된다. 대부분은 안 본다. 그래도 된다. 이야기 구조로 전달 리포트를 이야기처럼 쓴다. 기승전결 구조: 기(상황): "오늘 2,885개 테스트 실행. 밤새 4시간 걸렸습니다." 승(문제): "결제 API 테스트 3개 실패. 타임아웃 에러. 원인은 DB 쿼리 성능 이슈." 전(영향): "결제 플로우 전체에 영향. 신용카드/계좌이체 모두 영향권. 릴리즈 블로킹." 결(해결): "백엔드팀 인덱스 추가 중. 내일 오전 재테스트 예정. 그 외 영역은 배포 가능." 누구나 이해한다. 기술 배경 없어도. 실제 예시 (지난주 금요일 리포트): 제목: [빨강] 결제 모듈 블로킹, 그 외 배포 가능 "오늘 릴리즈 준비 테스트 완료했습니다. 좋은 소식: 신규 검색 기능, 마이페이지 개편 모두 통과. 2주간 준비한 기능들 문제없습니다. 나쁜 소식: 결제 API 성능 이슈 발견. 1,000건 이상 주문 시 타임아웃. 실제 운영 데이터로 테스트하면서 잡혔습니다. 영향도: 블랙프라이데이 대비 필수. 대량 주문 처리 불가능. 해결: 백엔드팀 DB 최적화 중. 월요일 오전 재검증. 결론: 검색/마이페이지는 이번 주 배포 가능. 결제는 다음 주." CTO 답장: "결제는 다음 주 가능할까요? 블프 전까진 필수." 즉시 답했다: "월요일 오후면 확실히 결론 드릴게요." 이게 소통이다.인사이트는 숨어있다 리포트는 결과만 전달하는 게 아니다. 데이터 속에서 인사이트를 찾는다. 패턴을 본다. 예시 1 - 특정 시간대 실패: "로그인 테스트가 새벽 3시에만 실패. 조사해보니 이 시간에 DB 백업 돌아감. 백업 중 타임아웃 늘어남. 테스트 타이밍 조정 또는 백업 시간 변경 필요." → 운영 이슈 발견. DevOps팀이 백업 시간 옮김. 예시 2 - 특정 OS 실패: "iOS 테스트만 3주간 실패율 높음. Android는 정상. iOS 14.8 특정 버전 이슈. 애플 버그. 우리가 고칠 수 없음. 사용자한테 OS 업데이트 권장 필요." → CS팀이 공지 올림. 문의 감소. 예시 3 - 특정 개발자 커밋: "A 개발자 PR 후 테스트 실패율 증가 패턴. 악의는 없음. 테스트 코드 작성 습관 부족. 페어 프로그래밍 제안." → 조심스럽게 팀장한테 공유. 개선됨. 인사이트는 액션으로 이어진다. 단순 결과 나열과 차원이 다르다. 템플릿을 만들었다 매일 처음부터 쓸 순 없다. 템플릿 3개 만들었다. 1. 일일 리포트 (Daily)신호등 상태 블로킹 이슈 (있으면) 새로 발견된 버그 내일 테스트 계획5분 안에 작성. Slack에 공유. 2. 주간 리포트 (Weekly)이번 주 하이라이트 트렌드 분석 (좋아짐/나빠짐) 다음 주 리스크 예상 요청 사항20분 작성. 이메일 발송. 3. 릴리즈 리포트 (Release)전체 테스트 커버리지 릴리즈 가능 여부 (Go/No-Go) 알려진 이슈 목록 릴리즈 후 모니터링 계획1시간 작성. 회의에서 발표. 템플릿 있으니 일관성 생겼다. 누가 봐도 같은 구조다. 자동화할 건 자동화 리포트 생성도 반은 자동화했다. Python 스크립트 하나 만들었다. # 내가 만든 리포트 생성기 (간략 버전) def generate_daily_report(): results = parse_test_results() blocking = [t for t in results if t.severity == 'critical' and t.status == 'failed'] warnings = [t for t in results if t.severity == 'high' and t.status == 'failed'] if blocking: signal = "🔴 빨강" elif warnings: signal = "🟡 노랑" else: signal = "🟢 초록" report = f""" [{signal}] {date.today()} 테스트 리포트 블로킹: {len(blocking)}개 {format_test_list(blocking)} 주의: {len(warnings)}개 {format_test_list(warnings)} 안전: {len([t for t in results if t.status == 'passed'])}개 검증 완료 """ return report매일 저녁 7시 자동 실행. 초안을 만들어준다. 내가 할 일은 컨텍스트 추가. "왜 실패했는지", "언제 해결되는지". 시간이 절반으로 줄었다. 예전엔 1시간. 지금은 30분. 질문 받을 준비 리포트 보내면 질문 온다. 준비해둔다. 예상 질문과 답변. 자주 오는 질문: Q: "이번 주 배포 가능?" A: 준비됨. 신호등 색깔 + 조건부 배포 가능 여부. Q: "이 버그 언제 고쳐짐?" A: 담당자 확인 + 예상 일정. 모르면 "확인 후 알려드릴게요" 솔직히. Q: "커버리지 왜 떨어졌어?" A: 신규 기능 추가됨. 분모 늘어남. 테스트 추가 중. 다음 주 회복 예정. Q: "자동화 안 되는 건 어떻게 테스트?" A: 매뉴얼 테스트 계획 있음. 담당자 배정됨. 결과는 금요일 공유. 답 못하는 질문도 있다. 그땐 솔직하게. "확인해보고 알려드릴게요. 30분 드리면 될까요?" 모르는 척 안 한다. 추측도 안 한다. 확인 후 정확히 답한다. 신뢰는 일관성에서 3개월 했더니 달라진 게 있다. 사람들이 내 리포트를 기다린다. "자동화J 리포트 왔어?" "오늘 신호등 뭐야?" "이번 릴리즈 괜찮대?" 예전엔 읽으라고 태그 걸었다. 지금은 먼저 찾아본다. 이유는 간단하다. 일관성. 매일 같은 시간. 같은 형식. 같은 수준의 디테일. 한 번도 빠진 적 없다. 금요일 밤 배포 전에도. 월요일 아침 장애 후에도. 신뢰는 거기서 온다. "자동화J 리포트면 믿을 만해." 가끔 PM이 의사결정 회의에 내 리포트 캡처해서 쓴다. "QA팀 판단으론 릴리즈 가능합니다." 뿌듯하다. 리포트가 의사결정 자료가 됐다. 실패도 공유한다 좋은 것만 보고하지 않는다. 테스트 놓친 것도 쓴다. 자동화 실패도 쓴다. 지난달 장애: "로그인 장애 발생. 테스트에서 못 잡았습니다. 원인: 타사 OAuth 서버 다운. 우리 테스트는 Mock 사용. 실제 연동은 검증 안 됨. 개선: 주 1회 실제 OAuth 통합 테스트 추가." 숨기지 않았다. 솔직하게 썼다. CTO 반응: "개선 계획 좋습니다. 다음 주부터 시작하세요." 책망 없었다. 오히려 신뢰 올라갔다. 완벽한 척 안 한다. QA도 사람이다. 다 잡을 순 없다. 중요한 건 배우고 개선하는 것. 팀원도 가르친다 후배 QA 2명 있다. 이들도 리포트 쓴다. 가르쳐줬다. 처음엔 숫자만 나열했다. 당연하다. 나도 그랬으니까. "이 숫자가 말하는 게 뭐야? 릴리즈 해도 돼?" 질문하면서 가르쳤다. 숫자 뒤 컨텍스트를 보라고. 2주 지나니 좋아졌다. 한 달 지나니 제법이다. 이제 나 없어도 리포트 돈다. 휴가 갈 수 있다. 데이터는 이야기를 원한다 숫자는 사실이다. 이야기는 의미다. 2,847개 통과는 사실. "결제 빼고 다 안전합니다"는 의미. 의미를 전달해야 움직인다. 액션이 나온다. 리포트는 보고서가 아니다. 소통 도구다.오늘도 7시 30분. 리포트 쓴다. 신호등은 초록. 내일 배포 간다.
- 09 Dec, 2025
Jenkins 파이프라인으로 CI/CD 자동화 완성하기
Jenkins 파이프라인으로 CI/CD 자동화 완성하기 파이프라인 처음 만들던 날 3년 전이었다. Jenkins 설치하고 첫 파이프라인 돌렸다. pipeline { agent any stages { stage('Test') { steps { sh 'pytest' } } } }10줄짜리 코드. 테스트만 돌렸다. 그게 시작이었다. 지금은 300줄이다. 빌드, 테스트, 배포, 알림, 롤백까지. 복잡하다. 하지만 이제 안다. 어떻게 관리하는지.오늘 우리 파이프라인 이야기한다. 실전이다. 선언형 파이프라인 기본 구조 선언형을 쓴다. 스크립트형 말고. 이유는 간단하다. 읽기 쉽다. 유지보수 쉽다. pipeline { agent { docker { image 'python:3.9' args '-v /var/run/docker.sock:/var/run/docker.sock' } } environment { TEST_ENV = 'staging' SLACK_CHANNEL = '#qa-alerts' COVERAGE_THRESHOLD = '80' } options { timestamps() timeout(time: 1, unit: 'HOURS') buildDiscarder(logRotator(numToKeepStr: '30')) disableConcurrentBuilds() } stages { stage('Setup') { steps { sh 'pip install -r requirements.txt' } } stage('Lint') { steps { sh 'flake8 tests/' sh 'pylint tests/' } } stage('Test') { steps { sh 'pytest --junitxml=report.xml' } } } post { always { junit 'report.xml' } } }이게 기본이다. agent는 어디서 돌릴지. environment는 환경변수. options는 파이프라인 설정. stages는 단계들. post는 후처리. 처음엔 agent any 썼다. 아무 노드나. 그런데 환경이 달라서 문제였다. 파이썬 버전, 라이브러리 버전. 다 달랐다. 도커로 바꿨다. 환경 통일됐다. 테스트 결과도 일관됐다. environment는 하드코딩 없애려고. 설정 값들 여기 모았다. 수정할 때 한 곳만 보면 된다. options는 필수다. timeout 없으면 무한 대기한다. 한번 그랬다. 테스트 하나 멈췄는데 8시간 돌았다. 그 후로 무조건 넣는다. disableConcurrentBuilds도 중요하다. 동시 빌드 막는다. DB 테스트할 때 충돌 났었다. 같은 데이터 동시 접근. 이것도 배웠다. 병렬 테스트 실행 구조 테스트가 많다. 1500개. 순차 실행하면 40분. 너무 길다. 병렬로 돌린다. 10분으로 줄었다. stage('Parallel Tests') { parallel { stage('Unit Tests') { steps { sh 'pytest tests/unit -n 4' } } stage('Integration Tests') { steps { sh 'pytest tests/integration -n 2' } } stage('API Tests') { steps { sh 'pytest tests/api --dist loadgroup' } } stage('UI Tests - Chrome') { steps { sh 'pytest tests/ui -k chrome --dist loadscope' } } stage('UI Tests - Firefox') { steps { sh 'pytest tests/ui -k firefox --dist loadscope' } } } }parallel 블록이다. 동시에 돌린다. 처음엔 전부 -n 4로 했다. 4개씩 병렬. 그런데 UI 테스트가 문제였다. 브라우저가 4개 뜨면 메모리 터진다. 노드가 죽었다. 그래서 분리했다. Unit은 빠르니까 4개. Integration은 무거워서 2개. UI는 loadscope로 클래스 단위 분산. 실패율도 줄었다. UI 테스트가 원래 flaky했다. 동시 실행하면 더 불안정했다. 브라우저별로 나눠서 격리했다.메트릭도 본다. 어느 단계가 오래 걸리는지. stage('Performance Check') { steps { script { def startTime = System.currentTimeMillis() sh 'pytest tests/performance' def duration = System.currentTimeMillis() - startTime if (duration > 300000) { // 5분 초과 echo "Warning: Performance tests took ${duration}ms" currentBuild.result = 'UNSTABLE' } } } }5분 넘으면 경고. UNSTABLE로 마킹. 실패는 아니다. 주의만 준다. 이거 넣고 병목 찾았다. API 테스트 중 하나가 2분 걸렸다. DB 초기화 때문이었다. 픽스처 개선했다. 20초로 줄었다. 실패 알림 자동화 시스템 테스트 실패하면 알아야 한다. 빨리. 정확하게. 슬랙 연동했다. post { failure { script { def failedTests = sh( script: "grep -o 'FAILED.*' report.xml | wc -l", returnStdout: true ).trim() def testReport = """ *Build Failed* :x: *Branch:* ${env.GIT_BRANCH} *Build:* ${env.BUILD_NUMBER} *Failed Tests:* ${failedTests} *Duration:* ${currentBuild.durationString} *Triggered by:* ${env.BUILD_USER} *View:* ${env.BUILD_URL} """ slackSend( channel: '#qa-alerts', color: 'danger', message: testReport ) } } success { slackSend( channel: '#qa-success', color: 'good', message: "*Build Success* :white_check_mark: Branch: ${env.GIT_BRANCH}" ) } unstable { slackSend( channel: '#qa-alerts', color: 'warning', message: "*Build Unstable* :warning: Some tests are flaky. Branch: ${env.GIT_BRANCH}" ) } }실패하면 #qa-alerts로. 성공하면 #qa-success로. 채널 분리했다. 노이즈 줄이려고. 처음엔 전부 한 채널이었다. 성공 알림이 너무 많았다. 실패 알림을 못 봤다. 분리하고 나서 놓치는 게 없다. 실패 알림은 상세하게. 어느 브랜치. 몇 개 실패. 누가 트리거. URL까지. def getFailedTestDetails() { def details = sh( script: """ grep 'FAILED' report.xml | \ sed 's/.*FAILED \\(.*\\)::.*/\\1/' | \ sort | uniq | head -5 """, returnStdout: true ).trim() return details }post { failure { script { def failedList = getFailedTestDetails() slackSend( channel: '#qa-alerts', message: """ *Failed Tests:* ``` ${failedList} ``` """ ) } } }실패한 테스트 이름도 보낸다. 최대 5개. 뭐가 깨졌는지 바로 안다. 이메일도 보낸다. 중요한 빌드는. post { failure { emailext( to: 'qa-team@company.com', subject: "Build Failed: ${env.JOB_NAME} #${env.BUILD_NUMBER}", body: """ <h2>Build Failed</h2> <p><b>Branch:</b> ${env.GIT_BRANCH}</p> <p><b>Build URL:</b> <a href="${env.BUILD_URL}">${env.BUILD_URL}</a></p> <p><b>Console Output:</b> <a href="${env.BUILD_URL}console">View</a></p> """, mimeType: 'text/html' ) } }HTML 형식이다. 보기 좋다. 링크 클릭하면 바로 Jenkins로. Jira 티켓도 자동 생성한다. 반복 실패하는 테스트는. def createJiraTicket(testName, buildUrl) { def issue = [ fields: [ project: [key: 'QA'], summary: "Flaky Test: ${testName}", description: "Test failed in build: ${buildUrl}", issuetype: [name: 'Bug'], priority: [name: 'Medium'], labels: ['flaky-test', 'auto-created'] ] ] def response = httpRequest( url: 'https://jira.company.com/rest/api/2/issue', httpMode: 'POST', contentType: 'APPLICATION_JSON', requestBody: groovy.json.JsonOutput.toJson(issue), authentication: 'jira-credentials' ) }post { failure { script { def failureCount = sh( script: "grep '${testName}' failure_log.txt | wc -l", returnStdout: true ).trim() as Integer if (failureCount >= 3) { createJiraTicket(testName, env.BUILD_URL) } } } }3번 연속 실패하면 티켓 생성. 자동으로. QA 백로그에 쌓인다. 주간 회의 때 리뷰한다.복잡한 파이프라인 관리 전략 파이프라인이 커진다. 300줄 넘어가면 관리가 어렵다. 첫 번째 전략. 공유 라이브러리. // vars/testPipeline.groovy def call(Map config) { pipeline { agent any stages { stage('Setup') { steps { script { setupTestEnvironment(config.testEnv) } } } stage('Test') { steps { script { runTests(config.testType) } } } } } }def setupTestEnvironment(env) { sh "pip install -r requirements-${env}.txt" }def runTests(type) { sh "pytest tests/${type} --junitxml=report.xml" }공통 로직을 라이브러리로 뺐다. Jenkinsfile은 간단해진다. @Library('my-shared-library') _testPipeline( testEnv: 'staging', testType: 'integration' )10줄이다. 읽기 쉽다. 두 번째 전략. 파라미터화. pipeline { parameters { choice( name: 'TEST_SUITE', choices: ['all', 'smoke', 'regression', 'api', 'ui'], description: 'Which test suite to run' ) string( name: 'TEST_ENV', defaultValue: 'staging', description: 'Test environment' ) booleanParam( name: 'SKIP_UI', defaultValue: false, description: 'Skip UI tests' ) choice( name: 'BROWSER', choices: ['chrome', 'firefox', 'both'], description: 'Browser for UI tests' ) } stages { stage('Test') { steps { script { if (params.TEST_SUITE == 'all') { sh 'pytest tests/' } else { sh "pytest tests/${params.TEST_SUITE}" } } } } stage('UI Test') { when { expression { !params.SKIP_UI } } steps { script { if (params.BROWSER == 'both') { parallel( chrome: { sh 'pytest tests/ui -k chrome' }, firefox: { sh 'pytest tests/ui -k firefox' } ) } else { sh "pytest tests/ui -k ${params.BROWSER}" } } } } } }유연하다. 상황마다 다르게 돌린다. 전체 테스트는 밤에. 스모크는 PR마다. 세 번째 전략. 단계별 분리. // Jenkinsfile pipeline { stages { stage('Build') { steps { build job: 'build-job', parameters: [string(name: 'BRANCH', value: env.GIT_BRANCH)] } } stage('Test') { steps { build job: 'test-job', parameters: [ string(name: 'BUILD_ID', value: env.BUILD_ID), string(name: 'TEST_ENV', value: 'staging') ] } } stage('Deploy') { when { branch 'main' } steps { build job: 'deploy-job', parameters: [string(name: 'VERSION', value: env.BUILD_ID)] } } } }빌드, 테스트, 배포 각각 job. 독립적이다. 재사용 가능하다. 테스트만 다시 돌리고 싶으면 test-job만. 배포만 하고 싶으면 deploy-job만. 네 번째 전략. 조건부 실행. stage('Integration Test') { when { anyOf { branch 'develop' branch 'main' changeRequest target: 'main' } } steps { sh 'pytest tests/integration' } }stage('E2E Test') { when { allOf { branch 'main' expression { return env.BUILD_NUMBER.toInteger() % 2 == 0 } } } steps { sh 'pytest tests/e2e' } }stage('Performance Test') { when { triggeredBy 'TimerTrigger' } steps { sh 'pytest tests/performance' } }Integration은 develop, main만. E2E는 main에서 2번에 1번. Performance는 스케줄 트리거만. 리소스 아낀다. 필요한 것만 돌린다. 다섯 번째 전략. 결과 아카이빙. post { always { junit 'reports/*.xml' publishHTML([ reportDir: 'htmlcov', reportFiles: 'index.html', reportName: 'Coverage Report' ]) archiveArtifacts artifacts: 'screenshots/*.png', allowEmptyArchive: true script { def coverage = sh( script: "coverage report | grep TOTAL | awk '{print \$4}'", returnStdout: true ).trim() currentBuild.description = "Coverage: ${coverage}" } } }테스트 결과, 커버리지, 스크린샷 다 저장한다. 나중에 볼 수 있다. 빌드 설명에 커버리지 표시. 목록에서 바로 보인다. 파이프라인 최적화 실전 팁 1년 돌리면서 배운 것들. 캐시 활용 pipeline { agent { docker { image 'python:3.9' args '-v /tmp/pip-cache:/root/.cache/pip' } } stages { stage('Install') { steps { sh 'pip install --cache-dir /root/.cache/pip -r requirements.txt' } } } }pip 캐시 볼륨 마운트. 설치 시간 3분 → 30초. 아티팩트 재사용 stage('Build') { steps { sh 'python setup.py bdist_wheel' stash includes: 'dist/*.whl', name: 'wheel' } }stage('Test') { agent { label 'test-node' } steps { unstash 'wheel' sh 'pip install dist/*.whl' sh 'pytest' } }빌드 한 번. 여러 노드에서 테스트. 중복 빌드 안 한다. 조기 실패 stage('Quick Check') { steps { sh 'pytest tests/smoke -x' // 첫 실패에서 멈춤 } }stage('Full Test') { when { expression { currentBuild.result == null } } steps { sh 'pytest tests/' } }스모크 실패하면 전체 테스트 안 돌린다. 시간 절약. 리트라이 로직 stage('Flaky Test') { steps { retry(3) { sh 'pytest tests/ui' } } }UI 테스트는 3번 재시도. Flaky 때문에. 하지만 남용 안 한다. 진짜 flaky만. 나머지는 고친다. 타임아웃 세분화 stage('Unit Test') { options { timeout(time: 5, unit: 'MINUTES') } steps { sh 'pytest tests/unit' } }stage('E2E Test') { options { timeout(time: 30, unit: 'MINUTES') } steps { sh 'pytest tests/e2e' } }각 단계마다 타임아웃. Unit은 5분. E2E는 30분. 적절하게. 로그 정리 post { always { sh 'pytest --tb=short > test_output.log' script { def log = readFile('test_output.log') if (log.size() > 10000) { writeFile file: 'test_output.log', text: log.take(10000) + "\n... (truncated)" } } archiveArtifacts 'test_output.log' } }로그 너무 길면 잘라낸다. Jenkins 느려진다. 실전 문제 해결 사례 케이스 1: 간헐적 실패 UI 테스트가 10번 중 3번 실패했다. 같은 테스트. 로그 봤다. 타임아웃이었다. 페이지 로딩이 늦을 때. // Before driver.find_element(By.ID, 'submit').click()// After wait = WebDriverWait(driver, 20) element = wait.until(EC.element_to_be_clickable((By.ID, 'submit'))) element.click()명시적 대기 추가. 실패율 0%로. 케이스 2: 메모리 부족 병렬 테스트 돌리면 노드가 죽었다. OOM. // Before parallel { stage('Test 1') { ... } stage('Test 2') { ... } stage('Test 3') { ... } stage('Test 4') { ... } }// After stage('Test Batch 1') { parallel { stage('Test 1') { ... } stage('Test 2') { ... } } }stage('Test Batch 2') { parallel { stage('Test 3') { ... } stage('Test 4') { ... } } }2개씩 배치로. 순차 병렬. 메모리 안정. 케이스 3: 느린 빌드 40분 걸렸다. 너무 길다. 프로파일링 했다. def measureStage(stageName, closure) { def start = System.currentTimeMillis() closure() def duration = System.currentTimeMillis() - start echo "${stageName} took ${duration}ms" }stage('Test') { steps { script { measureStage('Unit') { sh 'pytest tests/unit' } measureStage('Integration') { sh 'pytest tests/integration' } } } }Integration이 25분이었다. DB 초기화 때문. 픽스처 개선. 트랜잭션 롤백으로 변경. 5분으로 단축. 총 15분으로 줄었다. 케이스 4: 환경 차이 로컬은 성공. Jenkins는 실패. 환경이 달랐다. 도커 이미지 통일했다. FROM python:3.9RUN apt-get update && apt-get install -y \ chromium \ chromium-driver \ firefox-esr \ && rm -rf /var/lib/apt/lists/*COPY requirements.txt . RUN pip install -r requirements.txtWORKDIR /app로컬도 이 이미지로. Jenkins도. 결과 동일해졌다. 모니터링과 개선 파이프라인도 모니터링한다. 메트릭 수집. def collectMetrics() { def metrics = [ build_number: env.BUILD_NUMBER, duration: currentBuild.duration, result: currentBuild.result, test_count: sh(script: "grep 'tests passed' report.xml | wc -l", returnStdout: true).trim(), failed_count: sh(script: "grep 'FAILED' report.xml | wc -l", returnStdout: true).trim() ] def json = groovy.json.JsonOutput.toJson(metrics) sh "echo '${json}' >> /metrics/pipeline_metrics.jsonl" }post { always { script { collectMetrics() } } }매 빌드마다 기록. duration, result, test count. 주간 분석한다. 트렌드 본다. import pandas as pddf = pd.read_json('pipeline_metrics.jsonl', lines=True)# 평균 빌드 시간 print(f"Average duration: {df['duration'].mean() / 1000 / 60:.2f} minutes")# 실패율 failure_rate = len(df[df['result'] == 'FAILURE']) / len(df) * 100 print(f"Failure rate: {failure_rate:.2f}%")# 가장 느린 빌드 slowest = df.nlargest(5, 'duration') print(slowest[['build_number', 'duration', 'test_count']])데이터 기반 개선. 느린 빌드 찾고. 실패율 높은 테스트 찾고. 대시보드도 만들었다. Grafana로. post { always { influxDbPublisher( selectedTarget: 'jenkins-metrics', customProjectName: env.JOB_NAME, customData: [ build_duration: currentBuild.duration, test_count: testCount, failed_count: failedCount ] ) } }InfluxDB에 쌓고. Grafana로 시각화. 실시간 보인다. 지금 돌아가는 파이프라인 우리 메인 파이프라인이다. 매일 돌아간다. @Library('qa-shared-lib') _pipeline { agent none parameters { choice(name: 'ENVIRONMENT', choices: ['staging', 'production']) booleanParam(name: 'RUN_PERFORMANCE', defaultValue: false) } options { timeout(time: 1, unit: 'HOURS') buildDiscarder(logRotator(numToKeepStr: '50')) } stages { stage('Parallel Setup') { parallel { stage('Lint') { agent { docker 'python:3.9' } steps { sh 'pip install flake8 pylint' sh 'flake8 tests/' } } stage('Security Scan') { agent { docker 'python:3.9' } steps { sh 'pip install bandit' sh 'bandit -r tests/' } } } } stage('Build') { agent { docker 'python:3.9' } steps { sh 'pip install -r requirements.txt' sh 'python setup.py bdist_wheel' stash includes: 'dist/*.whl', name: 'package' } } stage('Test') { parallel { stage('Unit') { agent { docker 'python:3.9' } steps { unstash 'package' sh 'pip install dist/*.whl' sh 'pytest tests/unit --junitxml=unit.xml' } post { always { junit 'unit.xml' } } } stage('Integration') { agent { docker { image 'python:3.9' args '--network test-network' } } steps { unstash 'package' sh 'pip install dist/*.whl' sh 'pytest tests/integration --junitxml=integration.xml' } post { always { junit 'integration.xml' } } } stage('UI') { agent { label 'selenium-node' } steps { unstash 'package' sh 'pip install dist/*.whl' sh '
- 08 Dec, 2025
Pytest fixture로 테스트 데이터 관리하기: 야근 줄이는 법
Pytest fixture로 테스트 데이터 관리하기: 야근 줄이는 법 야근의 시작 금요일 오후 6시. 퇴근 30분 전이다. "J님, 테스트 스위트 돌리는 데 왜 이렇게 오래 걸려요?" 개발팀장 질문이다. 답은 알고 있다. 매번 디비 초기화하느라 30분씩 날린다. "최적화 좀 해보겠습니다." 그렇게 주말을 픽스처 공부로 보냈다.문제는 반복이었다 월요일 출근. 테스트 코드를 다시 봤다. def test_user_login(): db = create_db_connection() db.clean_all_tables() db.insert_test_user() # 실제 테스트 result = login("test@test.com") assert result.success db.close()def test_user_logout(): db = create_db_connection() db.clean_all_tables() db.insert_test_user() # 실제 테스트 result = logout() assert result.success db.close()똑같은 셋업이 50개 테스트마다 반복된다. 디비 초기화가 30초씩 걸린다. 50개 × 30초 = 25분. 순수 셋업 시간만. "이거 미친 짓이었네." 픽스처 공부한 보람이 있다. 바로 리팩토링 시작했다. fixture 기본부터 conftest.py 파일을 만들었다. import pytest@pytest.fixture def db_connection(): db = create_db_connection() yield db db.close()yield가 핵심이다. 앞은 셋업, 뒤는 티어다운. 자동으로 실행된다. 테스트 코드가 간단해졌다. def test_user_login(db_connection): db = db_connection result = login("test@test.com") assert result.successclose()를 신경 쓸 필요가 없다. 픽스처가 알아서 정리한다. 첫 번째 개선. 5분 절약.scope로 시간 줄이기 문제는 여전했다. 매 테스트마다 디비 연결을 새로 만든다. scope를 알게 됐다. @pytest.fixture(scope="session") def db_connection(): db = create_db_connection() yield db db.close()session scope. 전체 테스트 스위트에서 한 번만 실행된다. 하지만 문제가 생겼다. 테스트끼리 데이터가 꼬인다. "아, 연결은 유지하되 데이터는 초기화해야 하는구나." 다시 수정했다. @pytest.fixture(scope="session") def db_connection(): db = create_db_connection() yield db db.close()@pytest.fixture(scope="function") def clean_db(db_connection): db_connection.clean_all_tables() return db_connection연결은 세션당 한 번. 테이블 초기화는 테스트마다. 실행 시간이 25분에서 8분으로 줄었다. 17분 절약. "이제 좀 사람 사는 거 같네." 테스트 데이터 픽스처 다음 문제. 테스트 데이터 준비가 중복됐다. def test_admin_access(clean_db): admin = User(email="admin@test.com", role="admin") clean_db.insert(admin) result = access_admin_page(admin) assert result.successdef test_admin_delete(clean_db): admin = User(email="admin@test.com", role="admin") clean_db.insert(admin) result = delete_user(admin) assert result.successadmin 유저 생성 코드가 계속 반복된다. 픽스처로 뺐다. @pytest.fixture def admin_user(clean_db): admin = User(email="admin@test.com", role="admin") clean_db.insert(admin) return admindef test_admin_access(admin_user): result = access_admin_page(admin_user) assert result.success테스트 코드가 의도만 남았다. 셋업 코드가 사라졌다. 가독성이 올라갔다. 유지보수도 쉬워졌다. 파라미터로 여러 케이스 일반 유저, 관리자, 게스트. 세 가지 권한 테스트가 필요했다. 처음엔 테스트를 세 개 만들려고 했다. 비효율적이다. @pytest.fixture(params=[ {"email": "user@test.com", "role": "user"}, {"email": "admin@test.com", "role": "admin"}, {"email": "guest@test.com", "role": "guest"} ]) def test_user(request, clean_db): user = User(**request.param) clean_db.insert(user) return userdef test_user_access(test_user): result = access_page(test_user) assert result.success테스트 하나가 자동으로 세 번 실행된다. 각각 다른 유저로. pytest 출력도 깔끔하다. test_user_access[user] PASSED test_user_access[admin] PASSED test_user_access[guest] PASSED코드는 한 번 작성. 케이스는 무한 확장.autouse로 자동 실행 로그 관리가 필요했다. 모든 테스트마다. @pytest.fixture(autouse=True) def setup_logging(): logger = setup_test_logger() yield logger.save_results()autouse=True. 명시 안 해도 자동으로 실행된다. 모든 테스트 함수에 로깅이 적용됐다. 코드 수정 없이. "이건 진짜 마법 같네." 실전 구조 conftest.py를 계층화했다. tests/ conftest.py # 전역 픽스처 api/ conftest.py # API 테스트용 test_user.py ui/ conftest.py # UI 테스트용 test_login.py전역 conftest.py: @pytest.fixture(scope="session") def db_connection(): # 디비 연결 pass@pytest.fixture def clean_db(db_connection): # 테이블 초기화 passapi/conftest.py: @pytest.fixture def api_client(): # API 클라이언트 pass@pytest.fixture def auth_header(api_client): # 인증 헤더 pass필요한 픽스처만 불러온다. 테스트가 가벼워졌다. 픽스처 조합 픽스처끼리 조합할 수 있다. @pytest.fixture def user(clean_db): user = User(email="test@test.com") clean_db.insert(user) return user@pytest.fixture def logged_in_user(user, api_client): token = api_client.login(user) user.token = token return user@pytest.fixture def user_with_data(logged_in_user, clean_db): data = create_test_data() clean_db.insert_for_user(logged_in_user, data) return logged_in_user세 단계 픽스처다. 로그인까지, 데이터까지. 선택 가능하다. def test_simple(user): # 유저만 필요 passdef test_with_login(logged_in_user): # 로그인된 유저 passdef test_full_scenario(user_with_data): # 전부 준비된 상태 pass필요한 만큼만 셋업한다. 시간 절약이다. 실제 성과 리팩토링 전후 비교했다. 전:테스트 50개 실행: 32분 셋업 코드 중복: 200줄 디비 연결: 50번 티어다운 누락: 가끔후:테스트 50개 실행: 8분 픽스처 재사용: 10개 디비 연결: 1번 티어다운: 자동24분 절약. 하루에 테스트 3번 돌린다. 72분 단축. 주 5일이면 6시간. 거의 야근 하루가 사라졌다. 팀원 반응 후배 QA한테 픽스처 가르쳤다. "이거 진짜 편하네요." 개발팀장도 만족했다. "CI 파이프라인이 30분 빨라졌어요." 테크 리드가 물었다. "다른 팀도 적용 가능할까요?" "conftest.py 공유하면 됩니다." 지금은 전사 표준이 됐다. 모든 테스트가 픽스처 기반이다. 주의할 점 픽스처 남용하지 말 것. 간단한 테스트에 복잡한 픽스처는 과하다. 테스트 3줄인데 픽스처 20줄이면 문제다. scope 실수 조심. session scope에 function 데이터 넣으면 망한다. 픽스처 이름 명확하게. data보다 user_test_data가 낫다. 의존성 순환 주의. 픽스처가 서로 부르면 안 된다. 다음 단계 factory_boy 도입 검토 중이다. 픽스처 + 팩토리 패턴. @pytest.fixture def user_factory(clean_db): return UserFactorydef test_multiple_users(user_factory): users = user_factory.create_batch(10) # 10명 유저로 테스트더 유연해진다. 랜덤 데이터도 가능하다. Faker 라이브러리도 붙이면 좋겠다. 실제 같은 데이터로. 지금 시작하기 픽스처 리팩토링 순서:중복 셋업 코드 찾기 (Ctrl+F "setup") conftest.py 만들고 기본 픽스처 작성 yield로 티어다운 자동화 scope 최적화 (function → class → module → session) 파라미터로 케이스 확장 팀 공유하루면 충분하다. 효과는 즉시 나타난다. 마무리 금요일 6시. 테스트 스위트 실행했다. 8분 만에 완료. 초록불. "퇴근 가능." 야근이 사라졌다. 픽스처 덕분이다. 테스트 코드도 코드다. 리팩토링이 필요하다. 반복을 제거하고 재사용하라. 시간은 돌아온다. 야근 대신 정시 퇴근으로.이제 pytest.ini 설정도 정리해야겠다. 커버리지 리포트 경로가 엉망이다.