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

Minitest 작동 원리 완벽 분석: 루비 테스팅 라이브러리의 내부 들여다보기

Minitest란 무엇인가?

Minitest는 Ruby 테스팅 라이브러리로, TDD(Test-Driven Development) 스타일로 코드에 대한 테스트를 작성할 수 있게 해주는 도구입니다.

Rails의 기본 테스팅 프레임워크이며, Rails 창시자 DHH(David Heinemeier Hansson)가 가장 선호하는 도구이기도 합니다.

주요 대안인 RSpec에 비해 문법이 단순하고 코드량이 적다는 이유로 Minitest를 선호하는 개발자들도 많습니다.

아래 그림에서 그 차이를 확인할 수 있습니다:

Minitest 작동 원리 완벽 분석: 루비 테스팅 라이브러리의 내부 들여다보기

다만 이 글은 어느 쪽을 선택해야 하는지, 무엇이 '더 낫다'는 것을 판단하기 위한 글이 아닙니다.

이 글의 주제는 바로 Minitest가 어떻게 작동하는가입니다.

궁금하시다면: 편하신 것을 사용하면 됩니다. 다만 두 프레임워크 모두 익숙해져 두는 것이 좋습니다 🙂

사물의 작동 방식을 배우는 것을 좋아하신다면 이 글을 재미있게 읽으실 수 있을 겁니다.

어떤 테스팅 라이브러리를 선호하든 상관없이 말이죠!

내부 구조 살펴보기

소스 코드를 직접 읽어보라는 조언(저 역시 그렇습니다)을 자주 듣게 되는데, 여기에는 이유가 있습니다. 코드가 어떻게 작동하는지 배우는 훌륭한 방법일 뿐만 아니라, 이전에 접해보지 못한 새로운 Ruby 기법도 자연스럽게 익힐 수 있기 때문입니다.

저도 Minitest의 소스 코드를 직접 읽어봤고, 그 과정에서 배운 내용을 지금부터 여러분과 공유하려고 합니다.

먼저 실제 테스트 코드부터 살펴보면서, 이 코드가 Minitest의 동작 방식과 어떻게 연결되는지 논의해 보겠습니다.

class Thingy < Minitest::Test
  def test_it_works
    assert_equal 1, 1
  end
end

그렇다면 Minitest는 어떻게 test_it_works 같은 테스트 메서드를 찾아내고 실행할까요?

그 비밀은 약간의 메타프로그래밍 '마법'에 있습니다:

def self.methods_matching(re)
  public_instance_methods(true).grep(re).map(&:to_s)
end

이 코드는 lib/minitest.rb에 정의된 Runnable 클래스에서 가져온 것입니다. 현재 클래스의 모든 인스턴스 메서드를 조회한 뒤, 정규식과 일치하는 메서드만 골라냅니다.

따라서 methods_matching(/^test_/)을 호출하면 test_로 시작하는 모든 메서드 이름이 담긴 배열을 얻을 수 있습니다.

Minitest의 작동 방식

Minitest는 이렇게 찾아낸 test_ 메서드들을 실제로 호출합니다.

이 과정은 lib/minitest/test.rb 파일에서 일어나며, 더 구체적으로는 runnable_methods 메서드가 담당합니다. 이 메서드는 메서드 목록을 무작위(random) 순서로 반환하는 역할도 함께 수행합니다.

중요한 포인트:
이것이 가능한 이유는 Minitest::TestRunnable의 하위 클래스이기 때문입니다.

퍼즐의 마지막 조각은 Runnablerun 클래스 메서드입니다. 이 메서드는 추가적인 필터링을 거친 후, 각 메서드 이름과 리포터(reporter) 객체를 인자로 run_one_method를 호출합니다.

해당 코드는 다음과 같습니다:

filtered_methods.each do |method_name|
  run_one_method self, method_name, reporter
end

그리고 이 호출은 최종적으로 Minitest::Testrun 인스턴스 메서드로 이어집니다:

capture_exceptions do
  before_setup; setup; after_setup

  self.send self.name
end

send는 문자열이나 심볼을 사용해 객체의 특정 메서드를 동적으로 호출할 수 있게 해주는 메타프로그래밍 메서드입니다.

capture_exceptions 블록은 테스트 실패와 코드 실행 중 발생한 예외를 기록하는 데 사용됩니다.

def capture_exceptions # :nodoc:
  yield
rescue *PASSTHROUGH_EXCEPTIONS
  raise
rescue Assertion => e
  self.failures << e
rescue Exception => e
  self.failures << UnexpectedError.new(e)
end

제가 코드를 읽는 방식은 이렇습니다. 읽고 있는 코드에서 딱 한 가지 측면이나 기능에 집중한 뒤, 양파 껍질을 벗기듯 층층이 파고드는 것입니다.

yield 키워드처럼 의미를 모르는 요소가 나오면 바로 찾아보세요.

그것이 학습 과정의 일부니까요!

마무리

이번 글에서는 Minitest가 메타프로그래밍을 활용해 테스트 메서드를 찾아내고 호출하는 원리를 배웠습니다. 또한 테스트 중 발생한 오류와 예외가 보고를 위해 배열에 어떻게 수집되는지도 함께 살펴봤습니다.

이런 종류의 "코드 분석" 아티클이 마음에 드시나요?

댓글로 의견을 남겨주세요 🙂

그리고 실력 향상을 목표로 하는 7,000명 이상의 Ruby 개발자 커뮤니티에 아직 참여하지 않으셨다면, 아래 뉴스레터를 구독하시고 좋아하는 소셜 네트워크에 이 글을 공유하는 것도 잊지 마세요!