Computer >> 컴퓨터 >  >> 프로그래밍 >> Ruby

불안정한(Flaky) 테스트 스위트의 문제와 올바른 해결 방법

최근 저는 신뢰성이 떨어져 사용하기 정말 답답한 테스트 스위트를 다룬 적이 있습니다.

테스트 대상 애플리케이션은 API 전용 Rails 애플리케이션이었고, 테스트는 JavaScript로 작성되었습니다. 사용된 프레임워크는 Chakram으로, "JSON REST 엔드포인트에 대한 엔드 투 엔드(end-to-end) 테스트를 수행하도록 설계된 API 테스팅 프레임워크"입니다.

이 테스트 스위트의 핵심 문제는 결정적(deterministic)이지 않다는 것이었습니다. 함수나 프로그램이 결정적이라는 것은 동일한 입력을 주면 항상 동일한 출력을 낸다는 의미입니다.

이 테스트 스위트는 첫 번째 실행에서는 통과하고, 두 번째 실행에서도 통과하지만, 세 번째 실행에서는 실패했습니다. 그것도 애플리케이션 코드나 테스트 코드를 전혀 변경하지 않은 상태에서 말이죠.

저는 rake db:reset을 실행하면 테스트 스위트가 다시 통과한다는 사실을 발견했습니다. 이 테스트들은 Rails 애플리케이션의 개발 환경(development environment)에서 작동했고, 테스트 환경(test environment)이 아니었습니다. 따라서 테스트 스위트가 실행될 때마다 Rails 애플리케이션의 데이터베이스가 특정 상태여야만 했습니다.

가끔은 새로 시딩(seeding)된 데이터베이스로 시작해 테스트 스위트를 실행하고, 두 번째 실행도 성공하는 경우가 있었습니다. 심지어 세 번 이상 연속으로 성공할 때도 있었습니다. 하지만 상당히 자주, 테스트 스위트가 데이터를 어딘가 꼬이게 만들어서 rake db:reset을 다시 실행해야만 테스트가 성공적으로 사용할 수 있는 상태로 되돌릴 수 있었습니다.

물론 이런 식이어서는 안 됩니다. 테스트 스위트가 실패해야 하는 유일한 이유는 단 하나, 테스트 대상 코드가 제대로 작동하지 않을 때뿐입니다.

원래 어떻게 했어야 했을까?

그렇다면 이 테스트 스위트가 문제 있는 방식으로 구성되었다면, 올바른 방법은 무엇이었을까요?

문제의 근본 원인은 데이터베이스 리셋을 사람이 수동으로 수행해야 한다는 점에 테스트 스위트가 의존하고 있었다는 것입니다. 대신 테스트 스위트의 각 개별 테스트는 실행되기 전에 데이터베이스를 자동으로 깨끗하고 준비된 상태로 만들어야 했습니다. (테스트가 실행 후 데이터베이스를 초기화해서 다른 테스트로 데이터가 "새어 나가지" 않게 하는 것도 일반적인 관례입니다. 모든 테스트가 실행 전에 데이터베이스를 정리한다면 엄밀히 필수는 아니지만, 좋은 보호 장치가 됩니다.)

또한 테스트 스위트는 개발 환경이 아닌 Rails 애플리케이션의 테스트 환경을 대상으로 해야 했습니다. 개발자가 개발 환경에서 하는 작업이 테스트 스위트의 실행을 방해해서도 안 되고, 그 반대도 마찬가지입니다.

그 외 문제가 될 수 있는 의존성들

제 사례에서 문제가 된 의존성은 제대로 정리되지 않는 데이터베이스였습니다.

또 다른 유형의 문제적 의존성은 네트워크 요청입니다. Twilio API를 호출하는 테스트 스위트가 있다고 상상해 보세요. 화요일에 실행하면 통과합니다. 그런데 수요일에 같은 테스트 스위트를 실행하면 실패합니다. 여러분은 모르지만 Twilio 쪽에 장애(outage)가 발생했고, 그래서 테스트 스위트가 실패한 것입니다. 몇 분 후 장애가 해결되면 테스트 스위트는 다시 통과합니다.

이 역시 "테스트는 오직 하나의 이유로만 실패해야 한다. 바로 애플리케이션 코드가 작동하지 않을 때"라는 원칙과 연결됩니다.

네트워크와 상호작용하는 코드를 테스트해야 한다면, Ruby/Rails를 사용 중이라면 VCR 같은 도구를 활용하는 것이 더 좋은 방법입니다. VCR은 네트워크 요청을 기록(record)해 두었다가 실제 인터넷 연결 없이 나중에 재생(replay)할 수 있게 해줍니다.

외부 의존성이 허용되는 경우

"애플리케이션이 Twilio API에 의존하고 있고 Twilio API가 다운되었다면, 애플리케이션은 실제로 고장 난 것이므로 테스트가 실패하는 게 맞다"고 주장할 수도 있습니다. 그렇다면 '테스트는 외부 조건에 의존해서는 안 된다'는 원칙과 어떻게 조화시킬 수 있을까요?

답은 통합 테스트(integration test)단위 테스트(unit test)를 구분하는 데 있습니다. 통합 테스트는 여러 계층이나 시스템을 함께 테스트하고, 단위 테스트는 하나의 기능을 격리된 상태에서 테스트합니다.

팀이 의식적인 결정을 통해 특정 테스트 집합이 네트워크를 통해 외부 서비스를 호출하면서 여러 시스템을 함께 검증하도록 한다면, 그 자체에는 아무런 "문제"가 없습니다. (대안은 아예 그 시스템들을 함께 테스트하지 않기로 하는 것입니다.) 다만 이 경우 팀은 네트워크 의존적인 통합 테스트 스위트가 결정적일 것을 보장받지 못하며 가끔 불안정하게(flaky) 동작할 수 있다는 점을 인지해야 합니다. 이런 종류의 테스트 스위트는 개발자들이 매일 회귀(regression) 확인을 위해 로컬 머신에서 돌리는 테스트 모음과는 별도로 분리해 운영해야 합니다.

테스트가 외부 조건에 의존해서는 안 된다는 규칙에 예외가 존재하지만, 대부분의 경우에는 이 규칙을 지키는 것이 좋습니다. 그래야 테스트 스위트가 결정적이고 신뢰할 수 있다는 이점을 얻을 수 있습니다.