테스트코드를 작성할 필요가 있나요??
입사 이후 테스트 코드를 무조건 작성해야 한다는 말을 듣고 리더에게 했던 질문입니다.
테스트 코드 작성 경험이 없던 저는 기능 개발할 시간도 부족한데 테스트 코드를 작성하는 것은 비효율적이라는 일이라고 생각했습니다.
그러다 보니 처음에는 테스트 범위를 제대로 파악하지 못하거나 단순히 통과만 하게 하는 테스트 코드를 작성했습니다.
지금은 테스트 코드의 이점을 많이 체감하고 있습니다.
특히 배포 후 발견하지 못한 사이드 이펙트가 있을까 걱정하던 저에게 좀 더 자신감이 붙었고 버그 양도 줄어들었습니다.
늘어만 가는 테스트 코드와 시간
프로젝트에 기능이 추가될수록 테스트 코드는 늘어나며 테스트 코드를 실행하는 속도 또한 비례해서 증가합니다.
테스트 코드를 실행하는 시간이 길어질수록 개발자의 생산성은 저하됩니다.
리멤버에는 만 개가 넘는 테스트 코드가 있다 보니 테스트 코드 실행이 오래 걸리는 프로젝트는 한 시간이 넘게 걸리기도 합니다.
이 시간을 줄이기 위해 병렬적으로 테스트를 실행하는 방법을 적용해보게 되었습니다.
병렬 테스트
리멤버의 많은 서버 프로젝트는 Ruby on Rails로 개발되고 있고 이를 위한 테스트 프레임워크는 RSpec을 사용하고 있습니다.
Ruby on Rails 6부터는 병렬 테스트 기능이 공식적으로 포함되었으나 아쉽게도 RSpec과 다른 테스트 프레임워크인 MiniTest에 대해서만 그 기능을 지원하고 있습니다.
따라서 RSpec을 사용하는 저희는 이 기능을 이용할 수 없었고 대체재를 찾던 중 RSpec, UnitTest 등 다양한 테스트 프레임워크를 지원하는
병렬 테스트 라이브러리 “parallel_tests”(링크)를 찾게 되었습니다.
참고: 이 글을 쓰는 시점에서는 Ruby on Rails에서 RSpec에 대한 병렬 테스트 기능을 지원하지 않지만 추후에는 지원이 가능할지도 모릅니다. 관련된 내용은 GitHub 이슈(링크)가 생성되어있으니 참고하시길 바랍니다.
parallel_tests
최근 대부분 컴퓨터는 여러 대의 CPU 코어를 갖고 있지만 RSpec은 기본적으로 하나의 CPU 코어만 사용합니다.
병렬 테스트는 여러 CPU 코어를 활용하여 테스트 코드를 그룹으로 나눠 동시에 실행합니다.
앞서 말씀드린 것처럼 parallel_tests는 여러 테스트 프레임워크를 지원하지만, 이 글에서는 RSpec을 기준으로 작성하고 있습니다.
설치
gem 'parallel_tests', group: [:test]
데이터베이스 설정
테스트 코드는 각자 데이터 침범 없이 독립적으로 실행되어야 하므로 parallel_tests는 각 테스트 그룹에 대해
테스트를 실행하는 프로세스별로 독립된 데이터베이스를 사용합니다.
각 프로세스에서 데이터베이스를 사용하기 위해 다음과 같이 config/database.yml 파일의
test environment에 TEST_ENV_NUMBER 환경 변수를 데이터베이스 이름 뒤에 추가합니다.
테스트 프로세스당 한 개의 데이터베이스를 사용하도록 설정하였으니 이제 데이터베이스를 생성해야 합니다.
아래의 명령어는 DB를 생성 후 스키마 로드까지 해줍니다.
기본적으로 사용 할 수 있는 CPU 코어를 사용하지만 직접 개수를 정의 할 수도 있습니다.
rake parallel:setup
rake parallel:setup[4]
명령어를 실행했을 때 아래와 같이 DB가 생성된 걸 확인할 수 있습니다.
이제 테스트를 돌리기 전 모든 준비는 완료가 되었습니다.
실제로 병렬테스트를 돌렸을 때 평균적으로 30초 정도 줄어든 걸 확인했습니다.
참고: 테스트 프로세스 수를 무조건 많이 늘린다고 해서 속도가 비례해서 빨라지는 것은 아닙니다. 저의 경우에는 4대로 돌렸을 때 가장 빨랐으며 그 이상으로 늘렸을 때 오히려 효율이 높지 않았습니다.
테스트 그룹 균형있게 분할하기
테스트 그룹은 균형이 맞지 않게 분할이 될 수 있습니다.
오래 걸리는 테스트가 많이 포함된 그룹이 있을 때 다른 그룹들은 가장 느린 그룹을 기다리게 하여 전체적인
테스트 완료 시간을 늦춥니다.
아래 명령어를 사용하여 테스트 그룹의 균형을 균일하게 유지합니다..rspec_parallel
파일에 다음과 같은 코드를 추가합니다.
--format progress
--format ParallelTests::RSpec::RuntimeLogger --out tmp/parallel_runtime_rspec.log
각 테스트 파일에 대한 런타임 통계를 지정된 로그 파일에 저장합니다.
다음 실행에는 테스트를 보다 균형 잡힌 그룹으로 나누는 데 사용됩니다.parallel_runtime_rspec.log
파일이 생성되기 전 첫 번째 실행일 경우에는 시간이 더 걸릴 수 있습니다.
이 로그 파일은 테스트 실행 시 로드되며 각 프로세스가 거의 같은 시간에 완료되도록 테스트가 그룹화됩니다.
캐시 문제 해결하기
한 개의 캐시 저장소만 있는 경우 다른 테스트 프로세스에서 캐시를 동시에 참조하면 캐시 관련 테스트가 쉽게 실패할 수 있습니다.
이 문제를 해결하기 위해서는 프로세스마다 독자적인 캐시 저장소가 있어야 합니다.
저의 경우에는 프로세스별로 file_store 캐시 저장소를 사용하는 방법으로 해결하였습니다.
config.cache_store = :file_store, Rails.root.join("tmp", "cache", "parallel_tests#{ENV['TEST_ENV_NUMBER']}"
이 외에도 cache_store로 Redis를 쓰는 경우에 프로세스별로 Redis channel를 나눠서 해결하는 방법 등 여러 가지 방법이 있으니 상황에 적합한 해결 방법을 찾아보시면 좋을 것 같습니다.
로거
Rspec::Summarylogger
프로세스별 테스트 출력을 기록합니다.—-format progress
명령어는 테스트 성공 여부를 점 형태로 나타냅니다.
--format progress
--format ParallelTests::RSpec::SummaryLogger --out tmp/spec_summary.log
Rspec::FailuresLogger
실패한 예제에 대해서 볼 수 있는 로그를 생성합니다.
--format progress
--format ParallelTests::RSpec::FailuresLogger --out tmp/failing_specs.log
마무리
병렬테스트를 도입하는 데에 어려운 부분이 없고 테스트 코드의 실행 속도가 많이 개선되기 때문에
도입할 가치가 충분히 있다고 생각합니다.
Test::Unit, RSpec, Cucumber, Spinach을 사용하시는 분들은 한번 사용해보시면 좋을 것 같습니다.