오래된 PHP 코드를 고쳐야 했는데 테스트가 하나도 없다. 고치면 무엇이 깨지는지 알 방법이 없다.
Table of contents
Open Table of contents
먼저 현재 동작을 기록했다
PHPUnit 으로 테스트를 새로 짜려니 무엇이 맞는지 몰랐다. 명세가 없고 코드가 곧 명세다.
그래서 지금 동작을 그대로 기록했다. 입력 여러 개를 넣고 출력을 JSON 파일로 저장했다.
input_001.json → output_001.json
input_002.json → output_002.json
...
이 JSON 이 맞는지는 모른다. 다만 지금은 이렇게 동작한다는 사실이고 그것이 기준이 된다.
고친 뒤 비교했다
고치고 나서 같은 입력을 넣고 출력을 diff 로 비교했다. 달라진 것이 있으면 둘 중 하나다.
의도한 변경이다 → 저장된 출력을 갱신한다
의도 안 한 변경이다 → 버그다
diff 에 뜬 것이 의도한 것인지 아닌지를 가르는 것이 이 방식의 전부였다. 지금 동작이 이상해 보여도 일단 그대로 기록하니 옳고 그름을 판단하지 않아도 된다.
무엇이 달라졌는지를 아는 것과 모르는 것의 차이가 이 작업의 안전을 갈랐다.
입력을 어디서 구하나
가짜 입력을 만들면 실제와 다르므로 실제 데이터를 써야 했다. access_log 에 요청 본문이 남아 있는 곳이 있어서 거기서 가져왔다.
개인정보가 있는 것은 JSON 구조는 유지하고 값만 바꿨다. 여러 종류를 골고루 뽑았는데 정상 케이스뿐 아니라 이상한 것도 넣었다.
필드가 빠진 것
값이 비정상적으로 큰 것
인코딩이 이상한 것
이상한 입력이 특히 중요했다. 고치다가 그런 케이스의 처리를 바꾸기 쉽고 손으로 만들면 정상적인 경우만 넣게 된다.
함수 하나부터
전체를 한 번에 감싸려니 어려웠다. MySQL 도 붙고 외부 호출도 있다.
// 원래
function process($id) {
$data = $db->fetch($id);
$result = /* 복잡한 계산 */;
$db->save($result);
}
// 분리
function calculate($data) { // ← 이 부분만 테스트 가능
return /* 복잡한 계산 */;
}
process 에서 순수한 계산 부분부터 떼어냈다. 입출력이 있는 부분과 계산하는 부분을 나누면 calculate 쪽은 쉽게 테스트된다.
calculate 로 떼어내면서 코드가 읽기 쉬워지기도 했는데 부수효과가 있었다.
판단 기준 — 어디까지 감쌀지
시간이 한정돼 있어서 우선순위를 뒀다. 고칠 PHP 파일과 그 주변만 감쌌고 안 건드릴 곳은 안 했다.
그리고 자주 고치는 파일을 우선했는데 한 번 만들어 두면 계속 쓰기 때문이다. 어디가 자주 고쳐지는지는 svn log 에서 셌다. 전부 감싸겠다고 하면 시작도 못 한다.
이 작업 중에 기존 버그를 하나 찾았다. 특정 입력에서 출력이 이상했고 고치려던 것과 무관한 부분이었다.
고칠지 말지 고민했다. 고치면 그 동작에 의존하는 곳이 있을 수 있고 안 고치면 알면서 두는 것이다.
확인하고 나서 결정했다. 그 출력을 쓰는 곳을 grep 으로 찾아보니 실제로 문제가 되고 있어서 고쳤다.
svn commit 을 따로 했고 원래 하려던 작업과 섞지 않았다. 나중에 문제가 생기면 어느 쪽인지 갈려야 하니까.
정리
- 테스트가 없으면 현재 동작을 그대로 기록하는 것부터 한다
- 맞는지는 몰라도 지금 이렇다는 것은 사실이다
- 고친 뒤
diff로 비교해서 의도한 변경과 아닌 것을 가른다 - 무엇이 달라졌는지 아는 것과 모르는 것의 차이가 크다
- 입력은 실제 데이터에서 뽑고 이상한 케이스도 포함한다
- 손으로 만들면 정상적인 경우만 넣게 된다
- 계산 부분을 떼어내면 테스트가 쉬워지고 코드도 읽기 쉬워진다
- 전부 감싸지 말고 고칠 부분과 자주 고치는 부분부터 한다
- 과정에서 발견한 기존 버그는 확인하고 따로 커밋한다