테스트 코드 개선에 대한 고찰
travis build 에서의 테스트 코드 실행 시간 줄이기
현재 상황
- travis build 가 너무 오래 걸린다.
- PR 을 올리면 CI 툴인 travis 를 통해 travis build 가 실행
.travis.yml
에 정의한 configuration 대로 build 가 실행되며 PR 에 해당하는 테스트 코드가 실행- 개발자들이 올린 PR 마다 실행되는 build 는 travis queue 에서 처리가 되며 동시에 n개까지 처리가 가능
- 주된 업무 시간에는 queue 에 build 들이 쌓이며, 늦으면 몇 시간까지도 build 완료가 밀리는 일이 발생
- build 가 실행될 때, 보통 다른 업무를 하지만 때로는 build 가 빨리 실행돼야 하는 순간들이 있음 (예: 핫픽스 준비)
- build 실행 시간, 상세히는 build 에서의 테스트 코드 실행 시간을 단축하여 업무 효율을 높이고자 함
Chained PR
PR 에서 이미 travis build 중인 commit 이 amend 되더라도, 기존에 실행되던 build 는 취소되지 않음
- travis-ci 설정의 Auto Cancellation 설정에 대한 설명
Builds will only be canceled if they are waiting to run, allowing for any running jobs to finish.
- https://app.travis-ci.com/github/crema/crema/pull_requests 에서 현재 build running 중인 PR 정보 확인 가능
- 참고: travis build 중인 PR 이 닫힐 때, 일정 시간 후 build 취소됨
위 문제에 대한 해결은, commit amend -> force push 를 남발하여 build queue 를 가득 채워버리는 행위를 지양하는 것이다. 우리 회사는 1PR - 1commit 의 방법을 택하기 때문에, chain PR 을 만드는 일이 흔하다. chain PR 에서 비교적 앞쪽 PR 에 대한 리뷰 수정요청을 반영하면, 뒤에 연결된 PR 들은 전부 rebase branch 되어 새로운 빌드가 실행된다. 이 때, build queue 는 한 명의 작업에 의해 독점된다. 로컬 환경에서 lint(rubocop) 체크를 빼먹거나, 테스트 코드에 의한 검증을 빼먹고 PR 을 올린다면 이런 문제가 더 심화되게 된다.
사실, 이 문제는 개발하다보면 놓치기 쉬운 부분이고 체크하기 귀찮은 부분이라고 생각한다. 본질적인 해결보다는 증상 완화를 위한 방안 정도가 아닐까 싶다.
Monolithic Architecture
우리 회사는 여러 프로젝트가 하나의 repository 내에서 관리되는 monolith 구조를 택하고 있다. 프로젝트 공통으로 활용되는 로직 등은 core 경로에서 관리되는데, core 에 위치한 파일에 수정이 있을 경우, 그 core 파일을 활용하는 모든 프로젝트에 대한 테스트가 수행된다. 주요 로직은 Rails 에서 관리되기 때문에, core 파일이 수정되면 Rails 프로젝트들은 전부 테스트가 전부 실행된다.
1
2
3
4
5
6
7
class TravisRunner
def changes_detected_in_core?(project)
return false unless CORE_PROJECTS.include?(project)
return true if git_diffs.any? {|path| path.start_with?('core')}
false
end
end
테스트 코드의 범위 혹은 수준에는 ‘유닛 테스트’와 ‘통합 테스트’가 있다. 정의를 보면 아래와 같다.
유닛 테스트(Unit Test):
동작 범위: 가장 작은 단위의 코드인 개별 함수나 메서드, 클래스를 테스트합니다.
목적: 각각의 모듈이 독립적으로 정확하게 동작하는지 확인합니다.
특징: 외부 의존성을 최대한 제거하여 빠르고 간결하게 실행됩니다.
통합 테스트(Integration Test):
동작 범위: 여러 모듈이 함께 동작하는지 확인하기 위해 두 개 이상의 유닛을 조합하여 테스트합니다.
목적: 모듈 간의 상호작용이나 데이터 흐름이 예상대로 이루어지는지 점검합니다.
특징: 의존성을 포함하기 때문에 유닛 테스트보다는 복잡하고 시간이 더 걸릴 수 있습니다.
현재, 우리 회사의 테스트 코드에는 유닛 테스트, 통합 테스트가 명확하게 구분돼있지 않은 것들이 많은데 그 것들을 구분지으면 core 코드에 수정이 있을 때마다 모든 테스트 코드를 실행하는 것을 막을 수 있을 것 같다.
내가 생각하는 이상적인 구조는 아래와 같다. core 코드(A), 그 파일을 사용하는 코드(B)가 있다고 가정하자.
- A, B 는 각각의 유닛 테스트 코드를 가진다.
- 외부 의존성을 가능한 제거하여 외부 코드의 수정에 테스트 실행 결과가 영향 받지 않도록 작성한다.
stub
,mock
을 적극 활용하면 의존성 제거하기 쉽다.
- A 의 수정이 B 에 영향을 주는 의존성이 있다면, 통합 테스트를 별도의 경로에 작성한다.
- A 에서 수정 생긴 경우, 그에 해당하는 A의 유닛 테스트를 실행한다.
- A 의 수정에 영향을 받는 B, C, D 등에 연관된 통합 테스트들을 실행할 수 있도록 한다.
- A 의 수정과 무관한 통합 테스트들은 실행될 필요가 없기에 제외할 수 있도록 한다.
- 파일 경로나 이름 등으로 제어할 수 있을듯
결론
아직 이론적으로만 생각해본 것이라, 실제 적용에서 어떤 어려움이 있을지는 모르겠다. 피드백을 받아볼 필요가 있다.