회귀 테스트
고친 사람 github-actions[bot]
회귀 테스트는 코드를 바꾼 뒤에 전부터 되던 기능이 여전히 되는지 다시 확인합니다. 방금 고친 곳만 보지 않습니다. 손대지 않은 기능까지 다시 돌려 봅니다. 한 곳을 고쳤는데 엉뚱한 곳이 깨지는 일을 사용자보다 먼저 찾으려는 것입니다.
쉽고 빠른 이해
무슨 일을 하나 — 예전에 통과시킨 테스트를 버리지 않고 모아 둡니다. 코드를 바꿀 때마다 그 테스트를 다시 돌립니다. 배송비 함수를 고친 뒤에 「3만 원 주문은 배송비가 0원」이라는 옛 테스트가 깨지면, 고치다가 멀쩡하던 규칙을 망가뜨렸다는 뜻입니다.
왜 하나 — 코드는 서로 이어져 있습니다. 한 곳을 고치면 먼 곳이 깨지기도 합니다. 고친 사람은 고친 곳만 확인하기 쉽습니다. 옛 테스트는 그 사람이 떠올리지 못한 경우까지 대신 확인합니다.
어떻게 도나
- 기능을 만들거나 버그를 고칠 때 그 동작을 확인하는 테스트를 남깁니다
- 코드를 바꿀 때마다 남긴 테스트를 다시 돌립니다
- 전에 통과하던 테스트가 깨지면 그 변경을 합치기 전에 고칩니다
대가 — 테스트가 쌓일수록 한 번 돌리는 데 오래 걸립니다. 기능을 일부러 바꿀 때는 옛 테스트도 함께 고쳐야 합니다. 그래서 곧 버릴 시제품처럼 다시 고칠 일이 없는 코드에는 테스트를 쌓아 두지 않기도 합니다.
상세
이 절은 쇼핑몰의 배송비 계산 함수 하나를 예로 들어 회귀 테스트를 봅니다. 주문 금액이 3만 원 이상이면 배송비를 받지 않습니다. 그 밑이면 3천 원을 받습니다.
전기 기사가 부엌에 콘센트를 하나 새로 달았습니다. 일을 마친 기사는 부엌만 보고 떠나지 않습니다. 거실 전등과 욕실 환풍기도 한 번씩 켜 봅니다. 벽 안의 전선이 집 안 곳곳으로 이어져 있기 때문입니다.
소프트웨어에서 회귀는 바꾸기 전에 되던 기능이 바꾼 뒤에 안 되는 일입니다. 배송비 함수를 고쳤더니 전에는 0원이던 3만 원 주문에 배송비가 붙는 일이 그렇습니다. 회귀는 「되돌아간다」는 뜻입니다. 되던 소프트웨어가 안 되던 예전 상태로 되돌아갔다는 말입니다. 통계에서 쓰는 회귀 분석과는 이름만 같습니다.
회귀 테스트는 이 일을 잡으려고 전부터 되던 기능을 변경 뒤에 다시 확인하는 테스트입니다. 새 기능이 맞게 도는지는 새 테스트가 봅니다. 회귀 테스트는 옛 기능이 여전히 맞게 도는지를 봅니다.
회귀 테스트가 필요한 까닭은 코드가 서로 기대어 있기 때문입니다. 함수 하나를 여러 화면과 여러 서비스가 같이 부릅니다. 그 함수를 한 기능에 맞춰 고치면 그 함수를 부르는 다른 기능이 영향을 받습니다. 고친 사람은 대개 자기가 고치려던 기능만 확인합니다.
회귀를 부르는 흔한 변경
이 소절은 회귀를 부르는 변경을 넷으로 나눠 봅니다. 넷 모두 바꾼 사람이 영향이 닿는 범위를 다 떠올리지 못해서 회귀가 생깁니다.
| 변경 | 깨지는 곳 |
|---|---|
| 여러 곳이 같이 부르는 함수를 고친다 | 고친 사람이 떠올리지 못한 호출 쪽 기능 |
| 라이브러리 버전을 올린다 | 옛 버전의 동작에 기대던 코드 |
| 설정 값이나 데이터베이스 스키마를 바꾼다 | 그 값이나 열을 읽던 기능 |
| 버그 하나를 고친다 | 그 버그를 피해 가도록 짜 둔 다른 코드 |
마지막 줄은 뜻밖으로 보이지만 흔합니다. 어떤 코드가 옛 버그의 틀린 결과에 맞춰 짜여 있으면, 버그를 고치는 순간 그 코드가 틀린 값을 냅니다. 버그를 고친 변경도 회귀 테스트를 거쳐야 하는 까닭입니다.
배송비 함수에서 난 회귀
이 소절은 배송비 함수에 회귀가 생겨서 잡히기까지를 따라갑니다. 처음 이 함수를 만든 개발자는 테스트 세 개를 같이 남겼습니다.
assertEquals(3000, fee(29999));
assertEquals(0, fee(30000));
assertEquals(0, fee(50000));
Java 테스트 도구 JUnit 의 assertEquals 는 앞에 적은 기대한 값과 뒤에 적은 호출이 실제로 내놓은 값을 견줍니다. 다르면 테스트가 실패합니다. 둘째 줄은 딱 3만 원인 주문을 봅니다. 「3만 원 이상」이 3만 원을 포함하는지 확인하는 테스트입니다.
몇 달 뒤 다른 개발자가 회원 등급 할인을 넣었습니다. 그 김에 이 함수의 조건을 정리하다가 비교 기호 하나를 바꿨습니다.
// 바꾸기 전
return total >= 30000 ? 0 : 3000;
// 바꾼 뒤
return total > 30000 ? 0 : 3000;
>= 는 「이상」이고 > 는 「초과」입니다. 이제 딱 3만 원인 주문에도 배송비가 붙습니다. 이 개발자가 새로 쓴 할인 테스트는 전부 통과했습니다. 할인만 확인했기 때문입니다.
코드를 올리자 남겨 둔 옛 테스트 세 개가 다시 돌았습니다. 바꾼 함수가 내는 값은 아래와 같습니다.
fee(29999); // 3000
fee(30000); // 3000 · 기대한 값은 0
fee(50000); // 0
셋 중 둘째가 깨졌습니다. 이 개발자는 딱 3만 원인 주문을 떠올리지 못했습니다. 그 경우는 처음 만든 개발자가 몇 달 전에 테스트로 적어 두었습니다. 예전에 누군가 확인한 것을 다음 변경 때마다 다시 확인하는 것, 이것이 회귀 테스트가 하는 일입니다.
종류가 아니라 쓰임새에 붙은 이름
이 소절은 회귀 테스트가 어떤 테스트를 가리키는지를 봅니다. 앞 예에서 깨진 테스트는 단위 테스트였습니다. 함수 하나를 떼어 내어 확인하는 테스트입니다.
통합 테스트는 여러 조각을 붙여서 확인합니다. 주문 코드와 데이터베이스를 함께 돌려 보는 식입니다.
종단 간 테스트는 사용자가 쓰는 흐름을 처음부터 끝까지 따라갑니다. 상품을 담고 결제하기까지를 화면에서 차례로 해 봅니다.
이 테스트들도 남겨 두었다가 변경 뒤에 다시 돌리면 회귀 테스트 노릇을 합니다. 그래서 회귀 테스트는 확인하는 범위로 정해지지 않습니다. 언제 왜 돌리느냐로 정해집니다. 전에 통과한 테스트를 변경 뒤에 다시 돌려 되던 것이 안 되는지 보면 회귀 테스트입니다.
테스트 묶음이 자라는 방식
이 소절은 다시 돌릴 테스트가 어디서 오는지를 봅니다. 회귀 테스트는 여러 테스트를 모아 한 번에 돌립니다. 이 모음을 테스트 묶음이라고 합니다. 영어로는 테스트 스위트입니다.
묶음은 두 길로 자랍니다. 기능을 새로 만들 때 그 기능을 확인하는 테스트를 더합니다. 버그를 고칠 때도 그 버그를 다시 일으키는 테스트를 먼저 하나 더합니다.
버그를 고칠 때 더하는 테스트가 특히 값을 합니다. 한 번 난 버그는 비슷한 변경이 들어올 때 다시 나기 쉽습니다. 그 버그를 재현하는 테스트가 묶음에 있으면 같은 버그가 돌아오는 순간 깨집니다.
테스트는 이렇게 늘기만 하고 잘 줄지 않습니다. 묶음은 시간이 갈수록 커집니다. 다음 소절이 이 크기를 다루는 방법을 봅니다.
무엇을 다시 돌리나
이 소절은 커진 묶음에서 무엇을 다시 돌릴지 고르는 세 방법을 봅니다. 묶음이 작을 때는 변경마다 전부 돌리면 됩니다. 테스트가 수천 개로 늘어 한 번 돌리는 데 몇 시간이 걸리면 변경마다 전부 돌리기 어렵습니다.
| 방법 | 하는 일 | 얻는 것 | 잃는 것 |
|---|---|---|---|
| 전부 다시 돌리기 | 묶음의 테스트를 하나도 빼지 않고 돌린다 | 빠뜨리는 테스트가 없다 | 가장 오래 걸린다 |
| 골라 돌리기 | 바뀐 코드를 거쳐 가는 테스트만 돌린다 | 걸리는 시간이 준다 | 고르는 기준이 틀리면 깨질 테스트를 빼먹는다 |
| 순서 매기기 | 깨질 가능성이 큰 테스트부터 돌린다 | 깨진 것을 일찍 안다 | 끝까지 돌리면 걸리는 시간은 전부 돌리기와 같다 |
셋은 섞어 쓰기도 합니다. 변경을 올릴 때는 골라 돌리거나 순서를 매겨 금방 답을 받습니다. 하루에 한 번이나 배포 직전에는 전부 돌립니다.
언제 돌리나
이 소절은 변경 하나가 합쳐지고 배포되기까지 회귀 테스트가 어느 단계에서 도는지를 봅니다.
예전에는 사람이 확인 목록을 보며 화면을 하나씩 눌러 회귀 테스트를 했습니다. 지금은 대개 코드로 짠 테스트를 기계가 돌립니다. 이것을 테스트 자동화라고 합니다.
지속적 통합은 누가 코드를 올릴 때마다 서버가 그 코드를 받아 테스트를 돌려 주는 방식입니다. 자동화된 회귀 테스트는 대개 이 흐름 안에서 돕니다.
변경을 올리면 묶음에서 골라 낸 테스트가 먼저 돕니다. 앞 소절의 골라 돌리기로 금방 답을 받는 단계입니다. 여기서 깨지면 그 변경은 메인 브랜치에 합쳐지지 않습니다. 메인 브랜치는 팀이 같이 쓰는 기준 코드입니다.
통과하면 메인 브랜치에 합쳐집니다. 배포 전에는 묶음 전부를 다시 돌립니다. 여기서 깨지면 변경은 이미 합쳐진 뒤라서 배포를 멈추고 고칩니다.
flowchart TD
A["변경을 올린다"] --> B["골라 낸 테스트를 돌린다"]
B --> C{"전부 통과하나"}
C -->|"아니다"| X["합치지 않고 고친다"]
C -->|"그렇다"| D["메인 브랜치에 합치고<br/>묶음 전부를 돌린다"]
D --> E{"전부 통과하나"}
E -->|"아니다"| Y["배포를 멈추고 고친다"]
E -->|"그렇다"| F["배포한다"]
비슷한 이름과 가르는 선
이 소절은 회귀 테스트와 자주 섞이는 이름 셋을 무엇을 확인하느냐로 가릅니다.
| 이름 | 무엇을 확인하나 |
|---|---|
| 재시험 | 고친 버그가 정말 고쳐졌나 |
| 회귀 테스트 | 고치다가 다른 기능이 깨지지 않았나 |
| 스모크 테스트 | 새 빌드가 켜지고 핵심 기능이 대충 도나 |
| 성능 회귀를 잡는 시험 | 기능은 맞는데 전보다 느려지지 않았나 |
재시험과 회귀 테스트는 버그 하나를 고친 뒤 대개 함께 합니다. 재시험은 고친 곳을 봅니다. 회귀 테스트는 고친 곳 밖을 봅니다.
기능을 보는 테스트는 답이 맞으면 통과하므로 느려진 것은 잡지 못합니다. 속도는 같은 조건에서 성능을 재는 벤치마크로 지킵니다. 바꾸기 전에 잰 값을 남겨 둡니다. 바꾼 뒤 다시 재서 둘을 견줍니다.
묶음을 지키는 데 드는 비용
이 소절은 회귀 테스트를 굴리며 치르는 비용 셋과, 그 비용을 치를 까닭이 없는 코드를 봅니다. 끝에는 회귀 테스트가 못 잡는 회귀를 봅니다.
첫째는 시간입니다. 묶음이 커질수록 한 번 돌리는 데 오래 걸립니다. 기다리다 지친 개발자는 테스트를 건너뛰고 합치고 싶어집니다. 앞에서 본 골라 돌리기와 순서 매기기가 이 비용을 줄이려는 방법입니다.
둘째는 불안정한 테스트입니다. 코드를 안 바꿨는데도 돌릴 때마다 통과와 실패가 갈리는 테스트입니다. 이런 테스트가 섞이면 사람들은 실패를 보고도 한 번 더 돌려 보기만 합니다. 그러다 진짜 회귀로 난 실패까지 넘기게 됩니다.
셋째는 일부러 바꾼 동작이 옛 테스트를 깨뜨리는 일입니다. 배송비 기준을 5만 원으로 올리면 「3만 원이면 0원」 테스트가 깨집니다. 이때는 테스트의 기대한 값을 새 규칙에 맞게 고칩니다.
위험은 이 고침이 버릇이 될 때 생깁니다. 깨진 테스트마다 기대한 값을 지금 나온 값으로 바꿔 버리면 진짜 회귀도 통과로 도장이 찍힙니다. 그래서 테스트를 고치기 전에 이 실패가 일부러 바꾼 동작 탓인지 먼저 따집니다.
이 비용은 코드를 다시 고칠 때마다 다시 돌려서 값을 합니다. 곧 버릴 시제품이나 한 번 돌리고 말 스크립트는 다시 고칠 일이 없어서 모아 둔 테스트를 다시 돌릴 때가 오지 않습니다. 회귀 테스트는 오래 두고 여러 사람이 계속 고치는 코드일수록 값을 합니다.
회귀 테스트는 테스트가 확인하는 것만 지킵니다. 앞 예에서 3만 원 경계를 보는 테스트가 없었다면 그 회귀는 빠져나갔습니다. 테스트가 코드의 어디까지 닿는지는 테스트 커버리지로 잽니다.
관련 항목
회귀 테스트로 다시 돌리는 테스트 종류
단위 테스트 · 통합 테스트 · 종단 간 테스트 · 시스템 테스트 · 인수 테스트 · 계약 테스트
회귀 테스트와 확인 대상이 다른 시험
재시험 · 스모크 테스트 · 새니티 테스트 · 탐색적 테스트
회귀 테스트를 돌리는 개발 단계
지속적 통합 · 지속적 전달 · 지속적 배포 · 배포 파이프라인 · 메인 브랜치 · 빌드
회귀 테스트 묶음을 고르고 줄이는 기법
테스트 스위트 · 회귀 테스트 선택 · 테스트 우선순위화 · 테스트 영향 분석 · 테스트 병렬 실행
기대한 값을 옛 출력에서 얻는 기법
골든 파일 테스트 · 스냅숏 테스트 · 특성화 테스트 · 골든 마스터 테스트
회귀 테스트의 빈 곳을 재는 지표
테스트 커버리지 · 분기 커버리지 · 구문 커버리지 · 뮤테이션 테스트
회귀 테스트에서 자주 나는 문제
불안정한 테스트 · 깨지기 쉬운 테스트 · 테스트 순서 의존성 · 과도한 모킹
회귀 테스트에 기대어 하는 코드 변경
리팩터링 · 버그 수정 · 라이브러리 업그레이드 · 데이터베이스 마이그레이션 · 기술 부채
속도가 나빠지는 회귀를 잡는 시험
성능 회귀 · 벤치마크 · 성능 테스트 · 소크 테스트 · 성능 기준선
회귀 테스트를 쓰고 돌리는 도구
JUnit · pytest · Jest · Selenium · Playwright
회귀 테스트가 속하는 상위 분류
QA와 테스트 · 소프트웨어 테스트 · 테스트 자동화 · 테스트 전략
다른 이름: regression test · regression testing · 회귀 시험 · 리그레션 테스트