Rails 애플리케이션 테스트하기

이 가이드는 Rails에서 애플리케이션을 테스트하는 데 사용되는 내장 메커니즘을 다룹니다.

이 가이드를 읽고 나면 다음을 알 수 있습니다:

  • Rails 테스팅 용어
  • 애플리케이션의 단위 테스트, 기능 테스트, 통합 테스트, 시스템 테스트 작성 방법
  • 기타 인기 있는 테스팅 접근 방식과 플러그인

왜 Rails 애플리케이션을 테스트해야 할까요?

Rails는 테스트 작성을 매우 쉽게 해줍니다. 모델과 컨트롤러를 생성할 때 스켈레톤 테스트 코드를 생성합니다.

Rails 테스트를 실행하면 주요 코드 리팩토링 후에도 원하는 기능을 준수하는지 확인할 수 있습니다.

Rails 테스트는 브라우저 요청을 시뮬레이션할 수 있으므로 브라우저를 통해 테스트하지 않고도 애플리케이션의 응답을 테스트할 수 있습니다.

테스팅 소개

테스팅 지원은 Rails 프레임워크에 처음부터 통합되어 있습니다. 이는 “새롭고 멋진 기능이니 테스트 실행을 지원해야겠다"는 사후 결정이 아닙니다.

Rails는 테스팅을 위해 준비되어 있습니다

Rails는 rails new 명령어로 프로젝트를 생성하자마자 test 디렉토리를 만듭니다. 이 디렉토리의 내용을 살펴보면 다음과 같습니다:

$ ls -F test
application_system_test_case.rb  controllers/                     helpers/                         mailers/                         system/
channels/                        fixtures/                        integration/                     models/                          test_helper.rb

helpers, mailers, models 디렉토리는 각각 뷰 헬퍼, 메일러, 모델에 대한 테스트를 보관합니다. channels 디렉토리는 Action Cable 연결 및 채널에 대한 테스트를 보관합니다. controllers 디렉토리는 컨트롤러, 라우트, 뷰에 대한 테스트를 보관합니다. integration 디렉토리는 컨트롤러 간 상호 작용에 대한 테스트를 보관합니다.

시스템 테스트 디렉토리에는 시스템 테스트가 포함되어 있습니다. 이는 애플리케이션을 사용자가 경험하는 방식으로 전체 브라우저 테스팅을 수행하는 데 사용됩니다. 시스템 테스트는 Capybara를 상속받아 애플리케이션에 대한 브라우저 기반 테스트를 수행합니다.

Fixtures는 테스트 데이터를 구성하는 방법이며, fixtures 디렉토리에 있습니다.

jobs 디렉토리는 관련 테스트가 처음 생성될 때 생성됩니다.

test_helper.rb 파일은 테스트에 대한 기본 구성을 포함합니다.

application_system_test_case.rb는 시스템 테스트에 대한 기본 구성을 포함합니다.

테스트 환경

기본적으로 모든 Rails 애플리케이션에는 개발, 테스트, 프로덕션의 세 가지 환경이 있습니다.

각 환경의 구성은 유사하게 수정할 수 있습니다. 이 경우 config/environments/test.rb에서 테스트 환경의 옵션을 수정할 수 있습니다.

참고: 테스트는 RAILS_ENV=test 환경에서 실행됩니다.

Rails와 Minitest

기억하시겠지만, Rails 시작하기 가이드에서 bin/rails generate model 명령어를 사용했습니다. 첫 번째 모델을 생성했고, 그 과정에서 test 디렉토리에 테스트 스텁이 생성되었습니다:

$ bin/rails generate model article title:string body:text
...
create  app/models/article.rb
create  test/models/article_test.rb
create  test/fixtures/articles.yml
...

test/models/article_test.rb의 기본 테스트 스텁은 다음과 같습니다:

require "test_helper"

class ArticleTest < ActiveSupport::TestCase
  # test "the truth" do
  #   assert true
  # end
end

이 파일을 줄 단위로 살펴보면 Rails 테스팅 코드와 용어에 익숙해질 수 있습니다.

require "test_helper"

이 파일을 요구함으로써 테스트를 실행하기 위한 기본 구성이 로드됩니다. 우리는 모든 테스트에서 이 파일을 포함할 것이며, 이 파일에 추가된 메서드는 모든 테스트에서 사용할 수 있습니다.

class ArticleTest < ActiveSupport::TestCase
  # ...
end

ArticleTest 클래스는 ActiveSupport::TestCase를 상속하므로 테스트 케이스를 정의합니다. ArticleTestActiveSupport::TestCase에서 사용 가능한 모든 메서드를 사용할 수 있습니다. 이 가이드의 후반부에서 몇 가지 메서드를 살펴볼 것입니다.

Minitest::Test(이는 ActiveSupport::TestCase의 상위 클래스)에서 정의된 메서드 중 test_로 시작하는 메서드는 테스트로 간주됩니다. 따라서 test_passwordtest_valid_password와 같은 메서드 정의가 유효한 테스트 이름이며 자동으로 실행됩니다.

Rails는 또한 테스트 이름과 블록을 받는 test 메서드를 추가합니다. 이는 일반적인 Minitest::Unit 테스트를 생성하며 메서드 이름에 test_ 접두사를 추가합니다. 따라서 메서드 이름을 걱정할 필요가 없으며 다음과 같이 작성할 수 있습니다:

test "the truth" do
  assert true
end

이는 다음과 같이 작성하는 것과 대략 동일합니다:

def test_the_truth
  assert true
end

그러나 정규 메서드 정의를 사용할 수도 있습니다. test 매크로를 사용하면 테스트 이름을 더 읽기 쉽게 작성할 수 있습니다.

참고: 메서드 이름은 공백을 밑줄로 대체하여 생성됩니다. 결과는 유효한 Ruby 식별자일 필요는 없습니다 - 구두점 문자 등을 포함할 수 있습니다. 이는 기술적으로 모든 문자열이 메서드 이름이 될 수 있기 때문입니다. 이를 제대로 작동시키려면 define_methodsend 호출을 사용해야 할 수 있지만, 이름에 대한 공식적인 제한은 거의 없습니다.

다음으로 첫 번째 어설션을 살펴보겠습니다:

assert true

어설션은 예상 결과에 대해 객체(또는 표현식)를 평가하는 코드 줄입니다. 예를 들어, 어설션은 다음을 확인할 수 있습니다:

  • 이 값 = 저 값?
  • 이 객체가 nil?
  • 이 코드 줄이 예외를 발생시킵니까?
  • 사용자의 비밀번호가 5자 이상입니까?

각 테스트에는 하나 이상의 어설션이 포함될 수 있으며, 허용되는 어설션 수에 대한 제한은 없습니다. 모든 어설션이 성공적이어야 테스트가 통과됩니다.

첫 번째 실패 테스트

테스트 실패가 어떻게 보고되는지 확인하려면 article_test.rb 테스트 케이스에 실패하는 테스트를 추가할 수 있습니다.

test "should not save article without title" do
  article = Article.new
  assert_not article.save
end

방금 추가한 테스트(6번째 줄에 정의됨)를 실행해 보겠습니다.

$ bin/rails test test/models/article_test.rb:6
Run options: --seed 44656

# Running:

F

Failure:
ArticleTest#test_should_not_save_article_without_title [/path/to/blog/test/models/article_test.rb:6]:
Expected true to be nil or false


bin/rails test test/models/article_test.rb:6



Finished in 0.023918s, 41.8090 runs/s, 41.8090 assertions/s.

1 runs, 1 assertions, 1 failures, 0 errors, 0 skips

출력에서 F는 실패를 나타냅니다. Failure 아래에는 실패한 테스트의 이름과 해당 트레이스가 표시됩니다. 다음 몇 줄에는 스택 트레이스와 어설션에 의해 예상된 값과 실제 값의 차이를 나타내는 메시지가 포함되어 있습니다. 기본 어설션 메시지는 오류를 찾는 데 도움이 될 만큼의 정보만 제공합니다. 어설션 실패 메시지를 더 읽기 쉽게 만들려면 모든 어설션에 선택적 메시지 매개변수를 제공할 수 있습니다:

test "should not save article without title" do
  article = Article.new
  assert_not article.save, "Saved the article without a title"
end

이 테스트를 실행하면 더 친숙한 어설션 메시지가 표시됩니다:

Failure:
ArticleTest#test_should_not_save_article_without_title [/path/to/blog/test/models/article_test.rb:6]:
Saved the article without a title

이제 이 테스트를 통과시키려면 모델 수준의 title 필드에 대한 유효성 검사를 추가해야 합니다.

class Article < ApplicationRecord
  validates :title, presence: true
end

이제 테스트가 통과해야 합니다. 다시 한 번 테스트를 실행해 보겠습니다:

$ bin/rails test test/models/article_test.rb:6
Run options: --seed 31252

# Running:

.

Finished in 0.027476s, 36.3952 runs/s, 36.3952 assertions/s.

1 runs, 1 assertions, 0 failures, 0 errors, 0 skips

이제 원하는 기능에 대해 실패하는 테스트를 먼저 작성하고, 그 다음에 기능을 추가하는 코드를 작성하고, 마지막으로 테스트가 통과되는지 확인했습니다. 이러한 소프트웨어 개발 접근 방식을 테스트 주도 개발(TDD)이라고 합니다.

오류는 어떻게 보이나요?

오류가 보고되는 방식을 확인하려면 오류가 포함된 테스트가 있습니다:

test "should report error" do
  # some_undefined_variable은 이 테스트 케이스의 다른 곳에 정의되지 않습니다.
  some_undefined_variable
  assert true
end

이제 콘솔에서 테스트를 실행하면 더 많은 출력을 볼 수 있습니다:

$ bin/rails test test/models/article_test.rb
Run options: --seed 1808

# Running:

.E

Error:
ArticleTest#test_should_report_error:
NameError: undefined local variable or method 'some_undefined_variable' for #<ArticleTest:0x007fee3aa71798>
    test/models/article_test.rb:11:in 'block in <class:ArticleTest>'


bin/rails test test/models/article_test.rb:9



Finished in 0.040609s, 49.2500 runs/s, 24.6250 assertions/s.

2 runs, 1 assertions, 0 failures, 1 errors, 0 skips

‘E'가 출네, 계속해서 번역하겠습니다.

출력에서 'E'는 오류가 있는 테스트를 나타냅니다.

참고: 각 테스트 메서드는 오류나 어설션 실패가 발생하면 즉시 실행이 중지되고 다음 메서드로 계속됩니다. 모든 테스트 메서드는 무작위 순서로 실행됩니다. config.active_support.test_order 옵션을 사용하여 테스트 순서를 구성할 수 있습니다.

테스트가 실패하면 해당 백트레이스가 표시됩니다. 기본적으로 Rails는 이 백트레이스를 필터링하여 애플리케이션과 관련된 줄만 인쇄합니다. 이를 통해 프레임워크 노이즈를 제거하고 코드에 집중할 수 있습니다. 그러나 전체 백트레이스를 보고 싶은 경우도 있습니다. -b (또는 --backtrace) 인수를 설정하면 이 동작을 활성화할 수 있습니다:

$ bin/rails test -b test/models/article_test.rb

이 테스트를 통과시키려면 assert_raises를 사용하도록 수정할 수 있습니다:

test "should report error" do
  # some_undefined_variable은 이 테스트 케이스의 다른 곳에 정의되지 않습니다.
  assert_raises(NameError) do
    some_undefined_variable
  end
end

이 테스트는 이제 통과해야 합니다.

사용 가능한 어설션

지금까지 일부 사용 가능한 어설션을 살펴보았습니다. 어설션은 테스팅의 핵심 요소입니다. 계획대로 진행되고 있는지 확인하는 실제 검사를 수행합니다.

여기에는 Minitest(Rails에서 기본적으로 사용하는 테스팅 라이브러리)에서 사용할 수 있는 어설션의 일부가 나와 있습니다. [msg] 매개변수는 테스트 실패 메시지를 더 명확하게 만들기 위해 지정할 수 있는 선택적 문자열 메시지입니다.

어설션 목적
assert( test, [msg] ) test가 true인지 확인합니다.
assert_not( test, [msg] ) test가 false인지 확인합니다.
assert_equal( expected, actual, [msg] ) expected == actual이 true인지 확인합니다.
assert_not_equal( expected, actual, [msg] ) expected != actual이 true인지 확인합니다.
assert_same( expected, actual, [msg] ) expected.equal?(actual)이 true인지 확인합니다.
assert_not_same( expected, actual, [msg] ) expected.equal?(actual)이 false인지 확인합니다.
assert_nil( obj, [msg] ) obj.nil?이 true인지 확인합니다.
assert_not_nil( obj, [msg] ) obj.nil?이 false인지 확인합니다.
assert_empty( obj, [msg] ) objempty?인지 확인합니다.
assert_not_empty( obj, [msg] ) objempty?가 아닌지 확인합니다.
assert_match( regexp, string, [msg] ) 문자열이 정규식과 일치하는지 확인합니다.
assert_no_match( regexp, string, [msg] ) 문자열이 정규식과 일치하지 않는지 확인합니다.
assert_includes( collection, obj, [msg] ) objcollection에 포함되어 있는지 확인합니다.
assert_not_includes( collection, obj, [msg] ) objcollection에 포함되어 있지 않은지 확인합니다.
assert_in_delta( expected, actual, [delta], [msg] ) expectedactual 숫자가 delta 내에 있는지 확인합니다.
assert_not_in_delta( expected, actual, [delta], [msg] ) expectedactual 숫자가 delta 내에 있지 않은지 확인합니다.
assert_in_epsilon ( expected, actual, [epsilon], [msg] ) expectedactual 숫자의 상대 오차가 epsilon 미만인지 확인합니다.
assert_not_in_epsilon ( expected, actual, [epsilon], [msg] ) expectedactual 숫자의 상대 오차가 epsilon 미만이 아닌지 확인합니다.
assert_throws( symbol, [msg] ) { block } 주어진 블록이 지정된 기호를 발생시키는지 확인합니다.
assert_raises( exception1, exception2, ... ) { block } 주어진 블록이 지정된 예외 중 하나를 발생시키는지 확인합니다.
assert_instance_of( class, obj, [msg] ) objclass의 인스턴스인지 확인합니다.
assert_not_instance_of( class, obj, [msg] ) objclass의 인스턴스가 아닌지 확인합니다.
assert_kind_of( class, obj, [msg] ) objclass의 인스턴스이거나 하위 클래스인지 확인합니다.
assert_not_kind_of( class, obj, [msg] ) objclass의 인스턴스도 하위 클래스도 아닌지 확인합니다.
assert_respond_to( obj, symbol, [msg] ) objsymbol에 응답하는지 확인합니다.
assert_not_respond_to( obj, symbol, [msg] ) objsymbol에 응답하지 않는지 확인합니다.
assert_operator( obj1, operator, [obj2], [msg] ) obj1.operator(obj2)가 true인지 확인합니다.
assert_not_operator( obj1, operator, [obj2], [msg] ) obj1.operator(obj2)가 false인지 확인합니다.
assert_predicate ( obj, predicate, [msg] ) obj.predicate가 true인지 확인합니다. 예: assert_predicate str, :empty?
assert_not_predicate ( obj, predicate, [msg] ) obj.predicate가 false인지 확인합니다. 예: assert_not_predicate str, :empty?
assert_error_reported(class) { block } 오류 클래스가 보고되었는지 확인합니다. 예: assert_error_reported IOError { Rails.error.report(IOError.new("Oops")) }
assert_no_error_reported { block } 오류가 보고되지 않았는지 확인합니다. 예: assert_no_error_reported { perform_service }
flunk( [msg] ) 실패를 보장합니다. 아직 완성되지 않은 테스트를 명시적으로 표시하는 데 유용합니다.

위의 내용은 Minitest가 지원하는 어설션의 일부입니다. 전체 목록과 최신 정보는 Minitest API 문서, 특히 Minitest::Assertions를 참조하세요.

테스팅 프레임워크의 모듈식 특성 덕분에 사용자 고유의 어설션을 만들 수 있습니다. 실제로 Rails는 그렇게 합니다. 생활을 더 쉽게 만들기 위해 일부 특수화된 어설션을 포함합니다.

참고: 사용자 고유의 어설션을 만드는 것은 이 자습서에서 다루지 않는 고급 주제입니다.

Rails 특정 어설션

Rails는 minitest 프레임워크에 자체 사용자 정의 어설션을 추가했습니다:

어설션 목적
assert_difference(expressions, difference = 1, message = nil) {...} 평가된 표현식의 숫자 차이를 테스트합니다.
assert_no_difference(expressions, message = nil, &block) 전달된 블록을 실행하기 전후의 표현식 평가 결과가 변경되지 않았음을 확인합니다.
[assert_changes(expressions, message = nil, from:, to:, &block)][] 전달된 블록을 실행한 후 표현식 평가 결과가 변경되었음을 확인합니다.
assert_no_changes(expressions, message = nil, &block) 전달된 블록을 실행한 후 표현식 평가 결과가 변경되지 않았음을 확인합니다.
assert_nothing_raised { block } 주어진 블록이 어떤 예외도 발생시키지 않음을 확인합니다.
assert_recognizes(expected_options, path, extras={}, message=nil) 주어진 경로의 라우팅이 올바르게 처리되었고 구문 분석된 옵션(expectedoptions 해시에 주어진)이 경로와 일치함을 확인합니다. 즉, expectedoptions에 의해 지정된 경로를 Rails가 인식한다는 것을 확인합니다.
assert_generates(expected_path, options, defaults={}, extras = {}, message=nil) 제공된 옵션을 사용하여 제공된 경로를 생성할 수 있음을 확인합니다. 이는 assert_recognizes의 반대입니다. extras 매개변수는 쿼리 문자열에 있을 수 있는 추가 요청 매개변수의 이름과 값을 알려주는 데 사용됩니다. message 매개변수를 사용하면 어설션 실패에 대한 사용자 정의 오류 메시지를 지정할 수 있습니다.
assert_response(type, message = nil) 응답에 특정 상태 코드가 포함되어 있음을 확인합니다. :success를 지정하면 200-299 범위, :redirect를 지정하면 300-399 범위, :missing을 지정하면 404, :error를 지정하면 500-599 범위를 나타냅니다. 또한 명시적 상태 번호 또는 해당 기호를 전달할 수 있습니다. 자세한 내용은 상태 코드 전체 목록매핑을 참조하세요.
assert_redirected_to(options = {}, message=nil) 응답이 주어진 옵션과 일치하는 URL로 리디렉션되었음을 확인합니다. 루트 경로와 같은 명명된 경로 또는 Active Record 객체와 같은 것을 전달할 수도 있습니다.
assert_queries_count(count = nil, include_schema: false, &block) &blockint 수의 SQL 쿼리를 생성하는지 확인합니다.
assert_no_queries(include_schema: false, &block) &block이 SQL 쿼리를 생성하지 않는지 확인합니다.
assert_queries_match(pattern, count: nil, include_schema: false, &block) &block이 패턴과 일치하는 SQL 쿼리를 생성하는지 확인합니다.
assert_no_queries_match(pattern, &block) &block이 패턴과 일치하는 SQL 쿼리를 생성하지 않는지 확인합니다.

[assert_changes(expressions, message = nil, from:, to:, &block)]: https://api.rubyonrails.org/classes/ActiveSupport/Testing/Assertions.html#metho네, 계속해서 번역하겠습니다.

다음 장에서 이러한 어설션의 사용 예를 볼 수 있습니다.

테스트 케이스에 대한 간단한 메모

Minitest::Assertions에 정의된 기본 어설션 like assert_equal은 우리가 사용하는 테스트 케이스 클래스에서도 사용할 수 있습니다. 사실 Rails는 다음과 같은 클래스를 제공합니다:

이러한 각 클래스는 Minitest::Assertions를 포함하므로 우리의 테스트에서 모든 기본 어설션을 사용할 수 있습니다.

참고: Minitest에 대한 자세한 내용은 문서를 참조하세요.

Rails 테스트 러너

bin/rails test 명령어를 사용하여 모든 테스트를 한 번에 실행할 수 있습니다.

또는 테스트 케이스가 포함된 파일 이름을 bin/rails test 명령어에 전달하여 단일 테스트 파일을 실행할 수 있습니다.

$ bin/rails test test/models/article_test.rb
Run options: --seed 1559

# Running:

..

Finished in 0.027034s, 73.9810 runs/s, 110.9715 assertions/s.

2 runs, 3 assertions, 0 failures, 0 errors, 0 skips

이렇게 하면 테스트 케이스의 모든 테스트 메서드가 실행됩니다.

또한 -n 또는 --name 플래그와 테스트 메서드 이름을 제공하여 특정 테스트 메서드를 실행할 수 있습니다.

$ bin/rails test test/models/article_test.rb -n test_the_truth
Run options: -n test_the_truth --seed 43583

# Running:

.

Finished tests in 0.009064s, 110.3266 tests/s, 110.3266 assertions/s.

1 tests, 1 assertions, 0 failures, 0 errors, 0 skips

특정 줄의 테스트를 실행하려면 줄 번호를 제공할 수도 있습니다.

$ bin/rails test test/models/article_test.rb:6 # 특정 테스트와 줄 실행

줄 범위의 테스트를 실행할 수도 있습니다.

$ bin/rails test test/models/article_test.rb:6-20 # 6번 줄부터 20번 줄까지의 테스트 실행

디렉토리의 모든 테스트를 실행할 수도 있습니다.

$ bin/rails test test/controllers # 특정 디렉토리의 모든 테스트 실행

테스트 러너는 빨리 실패하기, 테스트 실행 결과를 마지막에 출력하기 등 많은 기능을 제공합니다. 테스트 러너 문서를 확인해 보세요:

$ bin/rails test -h
Usage:
  bin/rails test [PATHS...]

Run tests except system tests

Examples:
    You can run a single test by appending a line number to a filename:

        bin/rails test test/models/user_test.rb:27

    You can run multiple tests with in a line range by appending the line range to a filename:

        bin/rails test test/models/user_test.rb:10-20

    You can run multiple files and directories at the same time:

        bin/rails test test/controllers test/integration/login_test.rb

    By default test failures and errors are reported inline during a run.

minitest options:
    -h, --help                       Display this help.
        --no-plugins                 Bypass minitest plugin auto-loading (or set $MT_NO_PLUGINS).
    -s, --seed SEED                  Sets random seed. Also via env. Eg: SEED=n rake
    -v, --verbose                    Verbose. Show progress processing files.
    -q, --quiet                      Quiet. Show no progress processing files.
        --show-skips                 Show skipped at the end of run.
    -n, --name PATTERN               Filter run on /regexp/ or string.
        --exclude PATTERN            Exclude /regexp/ or string from run.
    -S, --skip CODES                 Skip reporting of certain types of results (eg E).

Known extensions: rails, pride
    -w, --warnings                   Run with Ruby warnings enabled
    -e, --environment ENV            Run tests in the ENV environment
    -b, --backtrace                  Show the complete backtrace
    -d, --defer-output               Output test failures and errors after the test run
    -f, --fail-fast                  Abort test run on first failure or error
    -c, --[no-]color                 Enable color in the output
        --profile [COUNT]            Enable profiling of tests and list the slowest test cases (default: 10)
    -p, --pride                      Pride. Show your testing pride!

지속적 통합(CI)에서 테스트 실행하기

CI 환경에서 모든 테스트를 실행하려면 다음 명령어만 실행하면 됩니다:

$ bin/rails test

시스템 테스트를 사용하는 경우 bin/rails test는 이를 실행하지 않습니다. 시스템 테스트도 실행하려면 별도의 CI 단계에서 bin/rails test:system을 실행하거나, 첫 번째 단계를 bin/rails test:all로 변경하여 시스템 테스트를 포함한 모든 테스트를 실행할 수 있습니다.

병렬 테스팅

병렬 테스팅을 사용하면 테스트 스위트를 병렬로 실행할 수 있습니다. 프로세스 포크가 기본 방법이지만 스레딩도 지원됩니다. 테스트를 병렬로 실행하면 전체 테스트 스위트 실행 시간이 단축됩니다.

프로세스를 사용한 병렬 테스팅

기본 병렬화 방법은 Ruby의 DRb 시스템을 사용하여 프로세스를 포크하는 것입니다. 제공된 작업자 수에 따라 프로세스가 포크됩니다. 기본 작업자 수는 실행 중인 머신의 실제 코어 수이지만 parallelize 메서드에 전달된 숫자로 변경할 수 있습니다.

병렬화를 활성화하려면 test_helper.rb에 다음을 추가하세요:

class ActiveSupport::TestCase
  parallelize(workers: 2)
end

전달된 작업자 수는 프로세스가 포크되는 횟수입니다. 로컬 테스트 스위트와 CI의 병렬화 수를 다르게 하고 싶다면 환경 변수를 사용하여 쉽게 변경할 수 있습니다:

$ PARALLEL_WORKERS=15 bin/rails test

테스트를 병렬화할 때 Active Record는 각 프로세스에 대한 데이터베이스를 자동으로 처리합니다. 데이터베이스 이름에는 작업자 번호가 접미사로 추가됩니다. 예를 들어 2개의 작업자가 있는 경우 test-database-0test-database-1이 생성됩니다.

전달된 작업자 수가 1 이하이면 프로세스가 포크되지 않고 테스트가 병렬화되지 않으며 원래의 test-database 데이터베이스를 사용합니다.

프로세스가 포크될 때 실행되는 두 개의 훅이 제공됩니다. 하나는 프로세스가 포크될 때 실행되고, 다른 하나는 포크된 프로세스가 닫히기 전에 실행됩니다. 애플리케이션이 여러 데이터베이스를 사용하거나 작업자 수에 따라 다른 작업을 수행하는 경우 이 훅이 유용할 수 있습니다.

parallelize_setup 메서드는 프로세스가 포크된 직후에 호출됩니다. parallelize_teardown 메서드는 프로세스가 닫히기 직전에 호출됩니다.

class ActiveSupport::TestCase
  parallelize_setup do |worker|
    # 데이터베이스 설정
  end

  parallelize_teardown do |worker|
    # 데이터베이스 정리
  end

  parallelize(workers: :number_of_processors)
end

이 메서드들은 스레드를 사용한 병렬 테스팅에서는 필요하지 않고 사용할 수 없습니다.

스레드를 사용한 병렬 테스팅

스레드를 사용하거나 JRuby를 사용하는 경우 스레드 기반 병렬화 옵션이 제공됩니다. 스레드 병렬화는 Minitest의 Parallel::Executor에 의해 지원됩니다.

병렬화 방법을 포크 대신 스레드로 변경하려면 test_helper.rb에 다음을 추가하세요:

class ActiveSupport::TestCase
  parallelize(workers: :number_of_processors, with: :threads)
end

JRuby 또는 TruffleRuby에서 생성된 Rails 애플리케이션은 자동으로 with: :threads 옵션을 포함합니다.

parallelize에 전달된 작업자 수는 테스트에 사용될 스레드 수를 결정합니다. 로컬 테스트 스위트와 CI의 병렬화 수를 다르게 하고 싶다면 환경 변수를 사용하여 쉽게 변경할 수 있습니다:

$ PARALLEL_WORKERS=15 bin/rails test

병렬 트랜잭션 테스팅

Rails는 테스트 케이스가 완료된 후 데이터베이스 트랜잭션으로 감싸서 실행합니다. 이를 통해 테스트 케이스가 서로 독립적이며 데이터베이스의 변경 사항이 단일 테스트 내에서만 표시됩니다.

병렬 스레드에서 실행되는 코드를 테스트하려면 테스트 케이스 클래스에서 self.use_transactional_tests = false를 설정하여 트랜잭션을 비활성화해야 합니다:

class WorkerTest < ActiveSupport::TestCase
  self.use_transactional_tests = false

  test "parallel transactions", 계속해서 번역하겠습니다.

```ruby
class WorkerTest < ActiveSupport::TestCase
  self.use_transactional_tests = false

  test "parallel transactions" do
    # 트랜잭션을 생성하는 스레드 시작
  end
end

참고: 트랜잭션 테스트를 비활성화하면 테스트가 완료된 후 데이터가 자동으로 롤백되지 않으므로 테스트에서 생성한 데이터를 직접 정리해야 합니다.

병렬화 임계값

병렬로 테스트를 실행하면 데이터베이스 설정 및 fixture 로드와 같은 오버헤드가 발생합니다. 이로 인해 Rails는 50개 미만의 테스트가 포함된 실행은 병렬화하지 않습니다.

이 임계값은 test.rb에서 구성할 수 있습니다:

config.active_support.test_parallelization_threshold = 100

또한 테스트 케이스 수준에서 병렬화를 설정할 때도 지정할 수 있습니다:

class ActiveSupport::TestCase
  parallelize threshold: 100
end

테스트 데이터베이스

대부분의 Rails 애플리케이션은 데이터베이스와 많은 상호 작용을 하므로 테스트에도 데이터베이스가 필요합니다. 효율적인 테스트를 작성하려면 이 데이터베이스를 어떻게 설정하고 샘플 데이터로 채워야 하는지 이해해야 합니다.

기본적으로 모든 Rails 애플리케이션에는 개발, 테스트, 프로덕션의 세 가지 환경이 있습니다. 각 환경의 데이터베이스는 config/database.yml에 구성됩니다.

전용 테스트 데이터베이스를 사용하면 테스트 데이터를 격리된 환경에서 설정하고 상호 작용할 수 있습니다. 이렇게 하면 테스트에서 테스트 데이터를 마음껏 변경할 수 있으며 개발 또는 프로덕션 데이터베이스에 대해 걱정할 필요가 없습니다.

테스트 데이터베이스 스키마 유지 관리

테스트를 실행하려면 테스트 데이터베이스에 현재 구조가 있어야 합니다. 테스트 헬퍼는 테스트 데이터베이스에 보류 중인 마이그레이션이 있는지 확인합니다. db/schema.rb 또는 db/structure.sql을 테스트 데이터베이스에 로드하려고 시도합니다. 마이그레이션이 아직 적용되지 않은 경우 오류가 발생합니다. 일반적으로 이는 스키마가 완전히 마이그레이션되지 않았음을 나타냅니다. 개발 데이터베이스에 대한 마이그레이션 실행(bin/rails db:migrate)으로 스키마를 최신 상태로 만들 수 있습니다.

참고: 기존 마이그레이션이 수정된 경우 테스트 데이터베이스를 다시 빌드해야 합니다. 이는 bin/rails db:test:prepare를 실행하여 수행할 수 있습니다.

Fixtures의 내막

좋은 테스트를 위해서는 테스트 데이터 설정에 대해 고민해야 합니다. Rails에서는 fixtures를 정의하고 사용자 정의하여 이 작업을 처리할 수 있습니다. 자세한 내용은 Fixtures API 문서를 참조하세요.

Fixtures란 무엇인가?

Fixtures는 샘플 데이터의 fancy한 표현입니다. Fixtures를 사용하면 테스트 데이터베이스에 테스트 실행 전에 미리 정의된 데이터를 채울 수 있습니다. Fixtures는 데이터베이스에 독립적이며 YAML로 작성됩니다. 모델별로 하나의 파일이 있습니다.

참고: Fixtures는 테스트에 필요한 모든 객체를 생성하도록 설계되지 않았으며, 일반적인 경우에 적용할 수 있는 기본 데이터에 대해서만 관리하는 것이 가장 좋습니다.

Fixtures는 test/fixtures 디렉토리에 있습니다. bin/rails generate model을 사용하여 새 모델을 생성하면 Rails가 이 디렉토리에 fixture 스텁을 자동으로 생성합니다.

YAML

YAML 형식의 fixtures는 샘플 데이터를 설명하는 인간 친화적인 방법입니다. 이러한 유형의 fixtures는 .yml 파일 확장자(예: users.yml)를 가집니다.

다음은 YAML fixture 파일의 샘플입니다:

# lo & behold! I am a YAML comment!
david:
  name: David Heinemeier Hansson
  birthday: 1979-10-15
  profession: Systems development

steve:
  name: Steve Ross Kellock
  birthday: 1974-09-27
  profession: guy with keyboard

각 fixture는 이름으로 시작하며 들여쓰기된 키/값 쌍 목록이 뒤따릅니다. 레코드는 일반적으로 빈 줄로 구분됩니다. 첫 번째 열에 #을 사용하여 fixture 파일에 주석을 추가할 수 있습니다.

associations를 사용하는 경우 두 개의 다른 fixture 간에 참조 노드를 정의할 수 있습니다. belongs_to/has_many 관계의 예는 다음과 같습니다:

# test/fixtures/categories.yml
about:
  name: About
# test/fixtures/articles.yml
first:
  title: Welcome to Rails!
  category: about
# test/fixtures/action_text/rich_texts.yml
first_content:
  record: first (Article)
  name: content
  body: <div>Hello, from <strong>a fixture</strong></div>

fixtures/articles.ymlfirst Article의 category 키가 about 값을 가지고 있고, fixtures/action_text/rich_texts.ymlfirst_content 항목의 record 키가 first (Article) 값을 가지고 있다는 점에 주목하세요. 이는 Active Record에 fixtures/categories.yml에서 찾은 Category about을 전자의 경우에, fixtures/articles.yml에서 찾은 Article first를 후자의 경우에 로드하도록 지시합니다.

참고: 이름으로 서로 참조하려면 id: 속성 대신 fixture 이름을 사용할 수 있습니다. Rails는 실행 간 일관성을 위해 기본 키를 자동 할당합니다. 이 관계 동작에 대한 자세한 내용은 Fixtures API 문서를 참조하세요.

파일 첨부 Fixtures

다른 Active Record 지원 모델과 마찬가지로 Active Storage 첨부 레코드는 ActiveRecord::Base 인스턴스에서 상속되므로 fixtures로 채울 수 있습니다.

thumbnail 첨부 파일이 있는 Article 모델과 fixture 데이터 YAML을 고려해 보겠습니다:

class Article < ApplicationRecord
  has_one_attached :thumbnail
end
# test/fixtures/articles.yml
first:
  title: An Article

image/png 인코딩 파일이 test/fixtures/files/first.png에 있다고 가정하면 다음 YAML fixture 항목은 관련 ActiveStorage::BlobActiveStorage::Attachment 레코드를 생성합니다:

# test/fixtures/active_storage/blobs.yml
first_thumbnail_blob: <%= ActiveStorage::FixtureSet.blob filename: "first.png" %>
# test/fixtures/active_storage/attachments.yml
first_thumbnail_attachment:
  name: thumbnail
  record: first (Article)
  blob: first_thumbnail_blob

ERB로 채우기

ERB를 사용하면 템플릿 내에 Ruby 코드를 포함시킬 수 있습니다. YAML fixture 형식은 Rails가 fixtures를 로드할 때 ERB로 전처리됩니다. 이를 통해 Ruby를 사용하여 일부 샘플 데이터를 생성할 수 있습니다. 예를 들어 다음 코드는 1000명의 사용자를 생성합니다:

<% 1000.times do |n| %>
  user_<%= n %>:
    username: <%= "user#{n}" %>
    email: <%= "user#{n}@example.com" %>
<% end %>

Fixtures 사용하기

Rails는 기본적으로 test/fixtures 디렉토리의 모든 fixtures를 자동으로 로드합니다. 로드 프로세스에는 다음 세 단계가 포함됩니다:

  1. 해당 테이블의 기존 데이터 제거
  2. fixture 데이터를 테이블에 로드
  3. fixture 데이터를 메서드로 덤프하여 직접 액세스할 수 있게 함

팁: 기존 데이터를 데이터베이스에서 제거하려면 Rails는 참조 무결성 트리거(외래 키, 검사 제약 조건 등)를 비활성화하려고 합니다. 테스트 환경에서 이러한 트리거를 비활성화할 수 있는 권한이 없어 성가신 권한 오류가 발생하는 경우, 데이터베이스 사용자에게 이 권한이 있는지 확인하세요. (PostgreSQL에서는 슈퍼유저만 모든 트리거를 비활성화할 수 있습니다. PostgreSQL 권한에 대해 자세히 알아보려면 여기를 참조하세요.)

Fixtures는 Active Record 객체입니다

Fixtures는 Active Record의 인스턴스입니다. 위의 3번째 단계에서 언급했듯이 테스트 케이스의 지역 범위에서 자동으로 사용할 수 있는 메서드로 객체에 직접 액세스할 수 있습니다. 예를 들면 다음과 같습니다:

# david라는 이름의 fixture를 반환합니다.
users(:david)

# david fixture의 id 속성을 반환합니다.
users(:david).id

# User 클래스에서 사용 가능한 메서드에도 액세스할 수 있습니다.
david = users(:david)
david.call(david.partner)

여러 fixture를 한 번에 가져오려면 fixture 이름 목록을 전달할 수 있습니다:

# david와 steve라는 fixture가 포함된 배열을 반환합니다.
users(:david, :steve)

모델 테스팅

모델 테스트는 애플리케이션의 다양한 모델을 테스트하는 데 사용됩니다.

Rails 모델 테스트는 test/models 디렉토리에 저장됩니다. Rails는 모델 테스트 스켈레톤을 생성하는 생성기를 제공합니다.

$ bin/rails generate test_unit:model article title:string body:text
create  test/models/article_test.rb
create  test/fixtures/articles.yml

모델 테스트에는 자체 상위 클래스가 없습니다. 대신 ActiveSupport::TestCase를 상속합니다.

시스템 테스팅

시스템 테스트를 사용하면 실제 또는 무인 브라우저에서 테스트를 실행하면서 애플리케이션에 대한 사용자 상호 작용을 테스트할 수 있습니다. 시스템 테스트는 내부적으로 Capybara를 사용합니다.

Rails 시스템 테스트를 생성하려면 test/system 디렉토리를 사용합니다. Rails는 시스템 테스트 스켈레톤을 생성하는 생성기를 제공합니다.

$ bin/rails generate system_test users
      invoke test_unit
      create test/system/users_test.rb

새로 생성된 시스템 테스트는 다음과 같습니다네, 계속해서 번역하겠습니다.

require "application_system_test_case"

class UsersTest < ApplicationSystemTestCase
  # test "visiting the index" do
  #   visit users_url
  #
  #   assert_selector "h1", text: "Users"
  # end
end

기본적으로 시스템 테스트는 Selenium 드라이버, Chrome 브라우저, 1400x1400 화면 크기로 실행됩니다. 다음 섹션에서는 기본 설정을 변경하는 방법을 설명합니다.

기본적으로 Rails는 테스트 중에 발생한 예외를 처리하고 HTML 오류 페이지로 응답합니다. 이 동작은 config.action_dispatch.show_exceptions 구성으로 제어할 수 있습니다.

기본 설정 변경하기

Rails는 시스템 테스트의 기본 설정을 매우 간단하게 변경할 수 있게 해줍니다. 모든 설정이 추상화되어 있어 테스트 작성에 집중할 수 있습니다.

새 애플리케이션 또는 스캐폴드를 생성할 때 application_system_test_case.rb 파일이 테스트 디렉토리에 생성됩니다. 시스템 테스트의 모든 구성은 이 파일에 있어야 합니다.

기본 설정을 변경하려면 시스템 테스트가 "구동되는” 드라이버를 변경할 수 있습니다. 예를 들어 Selenium에서 Cuprite로 변경하려면 먼저 Gemfilecuprite gem을 추가합니다. 그런 다음 application_system_test_case.rb 파일에서 다음과 같이 수행합니다:

require "test_helper"
require "capybara/cuprite"

class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
  driven_by :cuprite
end

driven_by에 드라이버 이름은 필수 인수입니다. driven_by에 전달할 수 있는 선택적 인수는 브라우저를 지정하는 :using(Selenium에서만 사용됨), 화면 크기를 변경하는 :screen_size, 드라이버에서 지원하는 옵션을 설정하는 :options입니다.

require "test_helper"

class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
  driven_by :selenium, using: :firefox
end

무인 브라우저를 사용하려면 Headless Chrome 또는 Headless Firefox를 사용할 수 있습니다. :using 인수에 headless_chrome 또는 headless_firefox를 추가하면 됩니다.

require "test_helper"

class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
  driven_by :selenium, using: :headless_chrome
end

원격 브라우저(예: Docker의 Headless Chrome)를 사용하려면 url을 추가하고 options에서 browserremote로 설정해야 합니다.

require "test_helper"

class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
  url = ENV.fetch("SELENIUM_REMOTE_URL", nil)
  options = if url
    { browser: :remote, url: url }
  else
    { browser: :chrome }
  end
  driven_by :selenium, using: :headless_chrome, options: options
end

이제 원격 브라우저에 연결할 수 있습니다.

$ SELENIUM_REMOTE_URL=http://localhost:4444/wd/hub bin/rails test:system

테스트 중인 애플리케이션도 원격으로 실행 중인 경우 Capybara에서 원격 서버 호출에 대한 추가 입력이 필요합니다.

require "test_helper"

class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
  def setup
    Capybara.server_host = "0.0.0.0" # 모든 인터페이스에 바인딩
    Capybara.app_host = "http://#{IPSocket.getaddress(Socket.gethostname)}" if ENV["SELENIUM_REMOTE_URL"].present?
    super
  end
  # ...
end

이제 Docker 컨테이너 또는 CI에서 실행 중인지 여부에 관계없이 원격 브라우저와 서버에 연결할 수 있습니다.

Capybara 구성에 추가 설정이 필요한 경우 이 추가 구성을 application_system_test_case.rb 파일에 추가할 수 있습니다.

Capybara 문서[https://github.com/teamcapybara/capybara#setup]에서 추가 설정을 참조하세요.

스크린샷 헬퍼

ScreenshotHelper는 테스트의 스크린샷을 캡처하는 데 도움이 되는 헬퍼입니다. 이는 테스트가 실패한 시점의 브라우저를 보거나 나중에 디버깅을 위해 스크린샷을 볼 수 있게 해줍니다.

두 개의 메서드가 제공됩니다: take_screenshottake_failed_screenshot. take_failed_screenshot은 Rails 내부에서 before_teardown 내에 자동으로 포함됩니다.

take_screenshot 헬퍼 메서드는 브라우저의 스크린샷을 찍는 데 사용할 수 있습니다.

시스템 테스트 구현하기

이제 블로그 애플리케이션에 시스템 테스트를 추가해 보겠습니다. 인덱스 페이지를 방문하고 새 블로그 기사를 생성하는 것을 보여주는 시스템 테스트를 작성하겠습니다.

스캐폴드 생성기를 사용한 경우 시스템 테스트 스켈레톤이 자동으로 생성되었습니다. 그렇지 않은 경우 시스템 테스트 스켈레톤을 먼저 생성해야 합니다.

$ bin/rails generate system_test articles

이 명령어를 실행하면 테스트 파일 자리 표시자가 생성되었어야 합니다. 이전 명령어의 출력을 보면 다음과 같습니다:

      invoke  test_unit
      create    test/system/articles_test.rb

이제 이 파일을 열고 첫 번째 어설션을 작성해 보겠습니다:

require "application_system_test_case"

class ArticlesTest < ApplicationSystemTestCase
  test "viewing the index" do
    visit articles_path
    assert_selector "h1", text: "Articles"
  end
end

이 테스트는 기사 인덱스 페이지에 h1이 있는지 확인합니다. 이 테스트는 통과해야 합니다.

시스템 테스트를 실행해 보겠습니다.

$ bin/rails test:system

참고: 기본적으로 bin/rails test를 실행하면 시스템 테스트가 실행되지 않습니다. bin/rails test:system을 실행해야 실제로 실행됩니다. bin/rails test:all을 실행하면 시스템 테스트를 포함한 모든 테스트가 실행됩니다.

기사 생성 시스템 테스트

이제 블로그에 새 기사를 생성하는 흐름을 테스트해 보겠습니다.

test "should create Article" do
  visit articles_path

  click_on "New Article"

  fill_in "Title", with: "Creating an Article"
  fill_in "Body", with: "Created this article successfully!"

  click_on "Create Article"

  assert_text "Creating an Article"
end

첫 번째 단계는 visit articles_path를 호출하는 것입니다. 이렇게 하면 기사 인덱스 페이지로 이동합니다.

그런 다음 click_on "New Article"은 인덱스 페이지에서 “New Article” 버튼을 찾아 클릭합니다. 이렇게 하면 /articles/new로 리디렉션됩니다.

그런 다음 테스트에서 기사의 제목과 본문을 지정된 텍스트로 채웁니다. 필드가 채워지면 “Create Article"을 클릭하여 새 기사를 데이터베이스에 생성하는 POST 요청을 보냅니다.

기사 인덱스 페이지로 리디렉션되면 새 기사의 제목 텍스트가 인덱스 페이지에 있는지 어설션합니다.

다중 화면 크기 테스트

데스크톱 크기 외에도 모바일 크기로 테스트하려면 ActionDispatch::SystemTestCase를 상속받는 다른 클래스를 만들 수 있습니다. 이 예에서는 /test 디렉토리에 mobile_system_test_case.rb라는 파일을 만들고 다음과 같이 구성합니다.

require "test_helper"

class MobileSystemTestCase < ActionDispatch::SystemTestCase
  driven_by :selenium, using: :chrome, screen_size: [375, 667]
end

이 구성을 사용하려면 test/systemMobileSystemTestCase를 상속받는 테스트를 만듭니다. 이제 다양한 구성으로 애플리케이션을 테스트할 수 있습니다.

require "mobile_system_test_case"

class PostsTest < MobileSystemTestCase
  test "visiting the index" do
    visit posts_url
    assert_selector "h1", text: "Posts"
  end
end

더 나아가기

시스템 테스팅의 장점은 컨트롤러, 모델, 뷰의 상호 작용을 통합 테스트하는 것과 유사하지만 훨씬 강력하고 실제 사용자가 애플리케이션을 사용하는 것처럼 테스트할 수 있다는 것입니다. 앞으로 사용자가 애플리케이션에서 수행할 수 있는 모든 작업(댓글 작성, 기사 삭제, 초안 기사 게시 등)을 테스트할 수 있습니다.

통합 테스팅

통합 테스트는 애플리케이션의 다양한 부분이 어떻게 상호 작용하는지 테스트하는 데 사용됩니다. 일반적으로 애플리케이션 내의 중요한 워크플로를 테스트하는 데 사용됩니다.

Rails 통합 테스트를 생성하려면 test/integration 디렉토리를 사용합니다. Rails는 통합 테스트 스켈레톤을 생성하는 생성기를 제공합니다.

$ bin/rails generate integration_test user_flows
      exists  test/integration/
      create  test/integration/user_flows_test.rb

새로 생성된 통합 테스트는 다음과 같습니다:

require "test_helper"

class UserFlowsTest < ActionDispatch::IntegrationTest
  # test "the truth" do
  #   assert true
  # end
end

여기서 테스트는 ActionDispatch::IntegrationTest를 상속합니다. 이를 통해 통합 테스트에 사용할 수 있는 추가 헬퍼가 제공됩니다.

기본적으로 Rails는 테스트 중에 발생한 예외를 처리하고 HTML 오류 페이지로 응답합니다. 이 동작은 config.action_dispatch.show_exceptions 구성으로 제어할 수 있습니다.

통합 테스트에 사용 가능한 헬퍼

표준 테스팅 헬퍼 외에도 ActionDispatch::IntegrationTest를 상속하면 통합 테스트 작성 시 사용할 수 있는 추가 헬퍼가 제공됩니다. 세 가지 범주의 헬퍼에 대해 간단히 소개하겠습니다.

통합 테스트 러너를 다루려면 ActionDispatch::Integration::Runner를 참조하세요.

요청을 수행할 때는 ActionDispatch::Integration::RequestHelpers를 사용할 수 있습니다.

파일을 업로드해야 하는 경우 ActionDispatch::TestProcess::FixtureFile를 참조하세요.

세션 또는 통합 테스트의 상태를 수정해야 하는 경우 [ActionDispatch::Integration::Session](https://api.ruby네, 계속해서 번역하겠습니다.

rails.org/classes/ActionDispatch/Integration/Session.html)을 참조하세요.

통합 테스트 구현하기

이제 블로그 애플리케이션에 통합 테스트를 추가해 보겠습니다. 새 블로그 기사 생성 기본 워크플로를 확인하여 모든 것이 제대로 작동하는지 확인하겠습니다.

먼저 통합 테스트 스켈레톤을 생성해 보겠습니다:

$ bin/rails generate integration_test blog_flow

테스트 파일 자리 표시자가 생성되었어야 합니다. 이전 명령어의 출력을 보면 다음과 같습니다:

      invoke  test_unit
      create    test/integration/blog_flow_test.rb

이제 그 파일을 열고 첫 번째 어설션을 작성해 보겠습니다:

require "test_helper"

class BlogFlowTest < ActionDispatch::IntegrationTest
  test "can see the welcome page" do
    get "/"
    assert_select "h1", "Welcome#index"
  end
end

Testing Views 섹션에서 살펴볼 assert_select를 사용하여 요청의 결과 HTML을 쿼리할 수 있습니다. 이는 요청 응답에서 핵심 HTML 요소와 해당 콘텐츠의 존재를 어설션하는 데 사용됩니다.

루트 경로를 방문하면 welcome/index.html.erb 뷰가 렌더링되어야 합니다. 따라서 이 어설션은 통과해야 합니다.

기사 생성 통합 테스트

이제 블로그에 새 기사를 생성하고 결과 기사를 확인하는 테스트를 작성해 보겠습니다.

test "can create an article" do
  get "/articles/new"
  assert_response :success

  post "/articles",
    params: { article: { title: "can create", body: "article successfully." } }
  assert_response :redirect
  follow_redirect!
  assert_response :success
  assert_select "p", "Title:\n  can create"
end

이 테스트를 살펴보겠습니다.

먼저 Articles 컨트롤러의 :new 액션을 호출합니다. 이 응답은 성공해야 합니다.

그 다음에 Articles 컨트롤러의 :create 액션에 POST 요청을 보냅니다:

post "/articles",
  params: { article: { title: "can create", body: "article successfully." } }
assert_response :redirect
follow_redirect!

요청 후 두 줄은 새 기사 생성 시 발생하는 리디렉션을 처리하기 위한 것입니다.

참고: 후속 요청을 하려면 반드시 follow_redirect!를 호출해야 합니다.

마지막으로 응답이 성공했고 새 기사가 페이지에 표시되는지 어설션할 수 있습니다.

더 나아가기

블로그를 방문하고 새 기사를 생성하는 매우 작은 워크플로를 테스트할 수 있었습니다. 이를 더 발전시켜 댓글 추가, 기사 삭제, 댓글 편집 등을 테스트할 수 있습니다. 통합 테스트는 애플리케이션의 모든 사용 사례를 실험할 수 있는 좋은 장소입니다.

컨트롤러에 대한 기능 테스트

Rails에서 컨트롤러의 다양한 액션을 테스트하는 것은 기능 테스트를 작성하는 형태입니다. 컨트롤러는 애플리케이션에 대한 들어오는 웹 요청을 처리하고 최종적으로 렌더링된 뷰로 응답합니다. 기능 테스트를 작성할 때는 액션이 요청을 어떻게 처리하고 예상되는 결과 또는 응답(경우에 따라 HTML 뷰)이 무엇인지 테스트합니다.

기능 테스트에 포함해야 할 내용

다음과 같은 사항을 테스트해야 합니다:

  • 웹 요청이 성공했는가?
  • 사용자가 올바른 페이지로 리디렉션되었는가?
  • 사용자가 성공적으로 인증되었는가?
  • 사용자에게 올바른 메시지가 표시되었는가?
  • 응답에 올바른 정보가 표시되었는가?

기능 테스트를 실제로 보는 가장 쉬운 방법은 스캐폴드 생성기를 사용하여 컨트롤러를 생성하는 것입니다:

$ bin/rails generate scaffold_controller article title:string body:text
...
create  app/controllers/articles_controller.rb
...
invoke  test_unit
create    test/controllers/articles_controller_test.rb
...

이렇게 하면 Article 리소스에 대한 컨트롤러 코드와 테스트가 생성됩니다. test/controllers/articles_controller_test.rb 파일을 살펴볼 수 있습니다.

이미 컨트롤러가 있고 기본 7개 액션에 대한 테스트 스켈레톤 코드만 생성하고 싶다면 다음 명령어를 사용할 수 있습니다:

$ bin/rails generate test_unit:scaffold article
...
invoke  test_unit
create    test/controllers/articles_controller_test.rb
...

articles_controller_test.rbtest_should_get_index 테스트를 살펴보겠습니다.

# articles_controller_test.rb
class ArticlesControllerTest < ActionDispatch::IntegrationTest
  test "should get index" do
    get articles_url
    assert_response :success
  end
end

test_should_get_index 테스트에서 Rails는 index 액션에 대한 요청을 시뮬레이션하고, 요청이 성공했고 올바른 응답 본문이 생성되었는지 확인합니다.

get 메서드는 웹 요청을 시작하고 결과를 @response에 채웁니다. 최대 6개의 인수를 받을 수 있습니다:

  • 요청할 컨트롤러 액션의 URI. 문자열 또는 경로 헬퍼(예: articles_url)로 지정할 수 있습니다.
  • params: 액션에 전달할 요청 매개변수가 포함된 해시(예: 쿼리 문자열 매개변수 또는 기사 변수).
  • headers: 요청과 함께 전달할 헤더를 설정합니다.
  • env: 필요에 따라 요청 환경을 사용자 정의합니다.
  • xhr: 요청이 Ajax 요청인지 여부를 나타냅니다. 요청을 Ajax로 표시하려면 true로 설정할 수 있습니다.
  • as: 다른 콘텐츠 유형으로 요청을 인코딩합니다.

이 모든 키워드 인수는 선택 사항입니다.

예: 첫 번째 Article:show 액션을 호출하고 HTTP_REFERER 헤더를 전달합니다:

get article_url(Article.first), headers: { "HTTP_REFERER" => "http://example.com/home" }

다른 예: 마지막 Article:update 액션을 호출하고 params에 새 title 텍스트를 전달하며 Ajax 요청으로 표시합니다:

patch article_url(Article.last), params: { article: { title: "updated" } }, xhr: true

마지막 예: 새 기사를 생성하기 위해 :create 액션을 호출하고 paramstitle 텍스트를 전달하며 JSON 요청으로 표시합니다:

post articles_path, params: { article: { title: "Ahoy!" } }, as: :json

참고: articles_controller_test.rbtest_should_create_article 테스트를 실행하려고 하면 새로 추가된 모델 수준 유효성 검사로 인해 실패할 것입니다.

articles_controller_test.rbtest_should_create_article 테스트를 다음과 같이 수정하면 모든 테스트가 통과할 것입니다:

test "should create article" do
  assert_difference("Article.count") do
    post articles_url, params: { article: { body: "Rails is awesome!", title: "Hello Rails" } }
  end

  assert_redirected_to article_path(Article.last)
end

이제 모든 테스트를 실행해 보면 모두 통과할 것입니다.

참고: 기본 인증 섹션의 단계를 따랐다면 모든 테스트를 통과시키려면 각 요청 헤더에 권한 부여를 추가해야 합니다:

post articles_url, params: { article: { body: "Rails is awesome!", title: "Hello Rails" } }, headers: { Authorization: ActionController::HttpAuthentication::Basic.encode_credentials("dhh", "secret") }

기본적으로 Rails는 테스트 중에 발생한 예외를 처리하고 HTML 오류 페이지로 응답합니다. 이 동작은 config.action_dispatch.show_exceptions 구성으로 제어할 수 있습니다.

기능 테스트에 사용 가능한 요청 유형

HTTP 프로토콜에 익숙하다면 get이 요청 유형 중 하나라는 것을 알고 계실 것입니다. Rails 기능 테스트에서 지원되는 요청 유형은 6가지입니다:

  • get
  • post
  • patch
  • put
  • head
  • delete

모든 요청 유형에는 사용할 수 있는 동등한 메서드가 있습니다. 일반적인 CRUD 애플리케이션에서는 get, post, put, delete를 더 자주 사용할 것입니다.

참고: 기능 테스트에서는 지정된 요청 유형이 액션에서 허용되는지 확인하지 않습니다. 우리는 결과에 더 관심이 있습니다. 이 사용 사례에 대해서는 요청 테스트가 존재합니다.

XHR(Ajax) 요청 테스트하기

Ajax 요청을 테스트하려면 get, post, patch, put, delete 메서드에 xhr: true 옵션을 지정할 수 있습니다. 예:

test "ajax request" do
  article = articles(:one)
  get article_url(article), xhr: true

  assert_equal "hello world", @response.body
  assert_equal "text/javascript", @response.media_type
end

계시록의 세 가지 해시

요청이 처리되고 나면 3개의 Hash 객체를 사용할 수 있습니다:

  • cookies - 설정된 모든 쿠키
  • flash - 플래시에 있는 모든 객체
  • session - 세션 변수에 있는 모든 객체

일반 Hash 객체와 마찬가지로 문자열 키를 참조하여 값에 액세스할 수 있습니다. 기호 이름으로도 참조할 수 있습니다. 예:

flash["gordon"]               # 또는 flash[:gordon]
session["shmession"]          # 또는 session[:shmession]
cookies["are_good_for_u"]     # 또는 cookies[:are_good_for_u]

사용 가능한 인스턴스 변수

요청 에는 기능 테스트에서 세 가지 인스턴스 변수에 액세스할 수 있습니다:

  • @controller - 요청을 처리하는 컨트롤러
  • @request - 요청 객체
  • @response - 응답 객체
class ArticlesControllerTest < ActionDispatch::IntegrationTest
  test "should get index" do
    get articles_url

    assert_equal "index", @controller.action_name
    assert_equal "application/x-www-form-urlencoded", @request.media_type
    assert_match "Articles", @response.body
  end
end

헤더 및 CGI 변수 설정

HTTP 헤더CGI 변수는 헤더로 전달할 수 있습니다:

# HTTP 헤더 설정
get articles_url, headers: { "Content-Type": "text/plain" } # 사용자 정의 네, 계속해서 번역하겠습니다.

# CGI 변수 설정
get articles_url, headers: { "HTTP_REFERER": "http://example.com/home" } # 사용자 정의 환경 변수 시뮬레이션

flash 알림 테스트하기

앞서 언급했듯이 계시록의 세 가지 해시 중 하나가 flash였습니다.

우리는 누군가가 새 Article을 성공적으로 생성할 때마다 flash 메시지를 추가하고 싶습니다.

먼저 test_should_create_article 테스트에 이 어설션을 추가해 보겠습니다:

test "should create article" do
  assert_difference("Article.count") do
    post articles_url, params: { article: { title: "Some title" } }
  end

  assert_redirected_to article_path(Article.last)
  assert_equal "Article was successfully created.", flash[:notice]
end

지금 테스트를 실행하면 실패할 것입니다:

$ bin/rails test test/controllers/articles_controller_test.rb -n test_should_create_article
Run options: -n test_should_create_article --seed 32266

# Running:

F

Finished in 0.114870s, 8.7055 runs/s, 34.8220 assertions/s.

  1) Failure:
ArticlesControllerTest#test_should_create_article [/test/controllers/articles_controller_test.rb:16]:
--- expected
+++ actual
@@ -1 +1 @@
-"Article was successfully created."
+nil

1 runs, 4 assertions, 1 failures, 0 errors, 0 skips

이제 컨트롤러에 플래시 메시지를 구현해 보겠습니다. :create 액션은 다음과 같이 보여야 합니다:

def create
  @article = Article.new(article_params)

  if @article.save
    flash[:notice] = "Article was successfully created."
    redirect_to @article
  else
    render "new"
  end
end

이제 테스트를 실행하면 통과할 것입니다:

$ bin/rails test test/controllers/articles_controller_test.rb -n test_should_create_article
Run options: -n test_should_create_article --seed 18981

# Running:

.

Finished in 0.081972s, 12.1993 runs/s, 48.7972 assertions/s.

1 runs, 4 assertions, 0 failures, 0 errors, 0 skips

종합하기

이 시점에서 Articles 컨트롤러는 :index, :new, :create 액션을 테스트합니다. 기존 데이터를 다루는 것은 어떨까요?

:show 액션에 대한 테스트를 작성해 보겠습니다:

test "should show article" do
  article = articles(:one)
  get article_url(article)
  assert_response :success
end

앞서 fixtures에 대해 논의할 때 언급했듯이 articles() 메서드를 통해 Articles fixtures에 액세스할 수 있습니다.

기존 Article을 삭제하는 것은 어떨까요?

test "should destroy article" do
  article = articles(:one)
  assert_difference("Article.count", -1) do
    delete article_url(article)
  end

  assert_redirected_to articles_path
end

기존 Article을 업데이트하는 테스트도 추가할 수 있습니다.

test "should update article" do
  article = articles(:one)

  patch article_url(article), params: { article: { title: "updated" } }

  assert_redirected_to article_path(article)
  # 업데이트된 데이터를 가져오고 제목이 업데이트되었는지 확인하기 위해 연관 관계를 다시 로드합니다.
  article.reload
  assert_equal "updated", article.title
end

이 세 가지 테스트에서 동일한 Article fixture 데이터에 액세스하고 있음을 알 수 있습니다. ActiveSupport::Callbacks에서 제공하는 setupteardown 메서드를 사용하여 이 중복을 제거할 수 있습니다.

이제 테스트는 다음과 같이 보일 것입니다. 간결성을 위해 다른 테스트는 생략했습니다.

require "test_helper"

class ArticlesControllerTest < ActionDispatch::IntegrationTest
  # 모든 테스트 전에 호출됨
  setup do
    @article = articles(:one)
  end

  # 모든 테스트 후에 호출됨
  teardown do
    # 컨트롤러가 캐시를 사용하는 경우 이후에 이를 초기화하는 것이 좋습니다.
    Rails.cache.clear
  end

  test "should show article" do
    # setup에서 생성된 @article 인스턴스 변수 사용
    get article_url(@article)
    assert_response :success
  end

  test "should destroy article" do
    assert_difference("Article.count", -1) do
      delete article_url(@article)
    end

    assert_redirected_to articles_path
  end

  test "should update article" do
    patch article_url(@article), params: { article: { title: "updated" } }

    assert_redirected_to article_path(@article)
    # 업데이트된 데이터를 가져오고 제목이 업데이트되었는지 확인하기 위해 연관 관계를 다시 로드합니다.
    @article.reload
    assert_equal "updated", @article.title
  end
end

다른 콜백과 마찬가지로 setupteardown 메서드에는 블록, 람다 또는 기호로 전달된 메서드 이름을 사용할 수 있습니다.

테스트 헬퍼

코드 중복을 방지하기 위해 사용자 정의 테스트 헬퍼를 추가할 수 있습니다. 로그인 헬퍼가 좋은 예입니다:

# test/test_helper.rb

module SignInHelper
  def sign_in_as(user)
    post sign_in_url(email: user.email, password: user.password)
  end
end

class ActionDispatch::IntegrationTest
  include SignInHelper
end
require "test_helper"

class ProfileControllerTest < ActionDispatch::IntegrationTest
  test "should show profile" do
    # 헬퍼는 이제 모든 컨트롤러 테스트 케이스에서 재사용 가능합니다.
    sign_in_as users(:david)

    get profile_url
    assert_response :success
  end
end

별도 파일 사용하기

헬퍼가 test_helper.rb를 너무 복잡하게 만드는 경우 별도 파일로 추출할 수 있습니다. 좋은 위치는 test/lib 또는 test/test_helpers입니다.

# test/test_helpers/multiple_assertions.rb
module MultipleAssertions
  def assert_multiple_of_forty_two(number)
    assert (number % 42 == 0), "expected #{number} to be a multiple of 42"
  end
end

이러한 헬퍼는 필요에 따라 명시적으로 요구되고 포함될 수 있습니다.

require "test_helper"
require "test_helpers/multiple_assertions"

class NumberTest < ActiveSupport::TestCase
  include MultipleAssertions

  test "420 is a multiple of forty two" do
    assert_multiple_of_forty_two 420
  end
end

또는 관련 상위 클래스에 직접 포함될 수 있습니다.

# test/test_helper.rb
require "test_helpers/sign_in_helper"

class ActionDispatch::IntegrationTest
  include SignInHelper
end

헬퍼 사전 로드하기

개별 테스트에서 명시적으로 요구하는 대신 test_helper.rb에서 헬퍼를 사전에 로드하는 것이 편리할 수 있습니다. 이는 글로브 패턴을 사용하여 수행할 수 있습니다.

# test/test_helper.rb
Dir[Rails.root.join("test", "test_helpers", "**", "*.rb")].each { |file| require file }

이렇게 하면 개별 테스트에서 암시적으로 액세스할 수 있습니다. 그러나 이는 필요한 파일만 로드하는 것보다 부팅 시간이 늘어나는 단점이 있습니다.

라우팅 테스팅

다른 모든 것과 마찬가지로 라우팅도 테스트할 수 있습니다. 라우팅 테스트는 test/controllers/ 디렉토리에 있거나 컨트롤러 테스트의 일부입니다.

참고: 애플리케이션에 복잡한 라우팅이 있는 경우 Rails는 테스트하는 데 유용한 여러 헬퍼를 제공합니다.

Rails에서 사용 가능한 라우팅 어설션에 대한 자세한 내용은 ActionDispatch::Assertions::RoutingAssertions API 문서를 참조하세요.

뷰 테스팅

응답의 핵심 HTML 요소와 콘텐츠를 어설션하여 뷰의 응답을 테스트하는 것은 애플리케이션 뷰를 테스트하는 일반적인 방법입니다. 라우팅 테스트와 마찬가지로 뷰 테스트는 test/controllers/ 디렉토리에 있거나 컨트롤러 테스트의 일부입니다. assert_select 메서드를 사용하면 간단하지만 강력한 구문을 사용하여 응답의 HTML 요소를 쿼리할 수 있습니다.

assert_select에는 두 가지 형식이 있습니다:

assert_select(selector, [equality], [message]) 는 선택기를 통해 선택된 요소에 대해 equality 조건이 충족되는지 확인합니다. 선택기는 CSS 선택기 표현식(문자열) 또는 대체 값이 있는 표현식일 수 있습니다.

assert_select(element, selector, [equality], [message]) 는 element(Nokogiri::XML::Node 또는 Nokogiri::XML::NodeSet의 인스턴스)와 해당 자손에 대해 선택기를 통해 선택된 모든 요소에서 equality 조건이 충족되는지 확인합니다.

예를 들어 응답의 제목 요소 내용을 확인할 수 있습니다:

assert_select "title", "Welcome to Rails Testing Guide"

중첩된 assert_select 블록을 사용하여 더 깊이 조사할 수도 있습니다.

다음 예에서 내부 assert_select "li.menu_item"은 외부 블록에서 선택된 요소 컬렉션 내에서 실행됩니다:

assert_select "ul.navigation" do
  assert_select "li.menu_item"
end

선택된 요소 컬렉션을 반복하여 assert_select를 개별적으로 호출할 수 있습니다.

예를 들어 응답에 두 개의 정렬된 목록이 있고 각각 네 개의 중첩된 목록 요소가 있는 경우 다음 테스트가 모두 통과합니다.

assert_select "ol" do |elements|
  elements.each do |element|
    assert_select element, "li", 4
  end
end

assert_select "ol" do
  assert_select "li", 8
end

이 어설션은 매우 강력합니다. 고급 사용법은 문서를 참조하세요.

추가 뷰 기반 어설션

뷰 테스팅에 주로 사용되는 추가 어설션이 있습니다:

어설션 목적
assert_select_email 이메일 본문에 대한 어설션을 허용합니다.
assert_select_encoded 인코딩된 HTML에 대한 어설션을 허용합니다. 각 요소의 내용을 디코딩한 다음 블록을 호출하여 이를 수행합니다.
css_select(selector) 또는 css_select(element, selector) selector에 의해 선택된 모든 요소의 배열을 반환합니다. 두 번째 변형에서는 먼저 기본 element를 일치시키고 selector 표현식을 해당 자식에서 일치시킵니다. 일치하는 항목이 없으면 두 변형 모두 빈 배열을 반환합니다.

assert_select_email의 예:

assert_select_email do
  assert_select "small", "Please click the 'Un네, 계속해서 번역하겠습니다.

assert_select_email do
  assert_select "small", "Please click the 'Unsubscribe' link if you want to opt-out."
end

뷰 부분 테스팅

부분 템플릿 - 일반적으로 "부분"이라고 불리는 - 은 렌더링 프로세스를 더 관리 가능한 청크로 분할하는 또 다른 장치입니다. 부분을 사용하면 템플릿의 코드 조각을 별도의 파일로 추출하고 템플릿 전체에서 재사용할 수 있습니다.

뷰 테스트는 부분이 예상대로 렌더링되는지 테스트할 수 있는 기회를 제공합니다. 뷰 부분 테스트는 test/views/ 디렉토리에 있으며 ActionView::TestCase를 상속합니다.

부분을 렌더링하려면 템플릿에서와 같이 render를 호출합니다. 콘텐츠는 테스트 로컬 #rendered 메서드를 통해 사용할 수 있습니다:

class ArticlePartialTest < ActionView::TestCase
  test "renders a link to itself" do
    article = Article.create! title: "Hello, world"

    render "articles/article", article: article

    assert_includes rendered, article.title
  end
end

ActionView::TestCase에서 상속받은 테스트는 assert_select추가 뷰 기반 어설션에 액세스할 수 있습니다:

test "renders a link to itself" do
  article = Article.create! title: "Hello, world"

  render "articles/article", article: article

  assert_select "a[href=?]", article_url(article), text: article.title
end

rails-dom-testing과 통합하려면 ActionView::TestCase에서 상속받는 테스트에서 document_root_element 메서드를 선언하여 렌더링된 콘텐츠를 Nokogiri::XML::Node 인스턴스로 반환해야 합니다:

test "renders a link to itself" do
  article = Article.create! title: "Hello, world"

  render "articles/article", article: article
  anchor = document_root_element.at("a")

  assert_equal article.name, anchor.text
  assert_equal article_url(article), anchor["href"]
end

애플리케이션이 Ruby >= 3.0 이상을 사용하고 Nokogiri >= 1.14.0 이상에 의존하며 Minitest >= >5.18.0 이상에 의존하는 경우 document_root_elementRuby의 패턴 매칭을 지원합니다:

test "renders a link to itself" do
  article = Article.create! title: "Hello, world"

  render "articles/article", article: article
  anchor = document_root_element.at("a")
  url = article_url(article)

  assert_pattern do
    anchor => { content: "Hello, world", attributes: [{ name: "href", value: url }] }
  end
end

Functional and System Testing에서 사용되는 동일한 Capybara 지원 어설션에 액세스하려면 ActionView::TestCase를 상속받는 기본 클래스를 정의하고 document_root_elementpage 메서드로 변환할 수 있습니다:

# test/view_partial_test_case.rb

require "test_helper"
require "capybara/minitest"

class ViewPartialTestCase < ActionView::TestCase
  include Capybara::Minitest::Assertions

  def page
    Capybara.string(rendered)
  end
end

# test/views/article_partial_test.rb

require "view_partial_test_case"

class ArticlePartialTest < ViewPartialTestCase
  test "renders a link to itself" do
    article = Article.create! title: "Hello, world"

    render "articles/article", article: article

    assert_link article.title, href: article_url(article)
  end
end

Action View 버전 7.1부터 #rendered 헬퍼 메서드는 뷰 부분의 렌더링된 콘텐츠를 구문 분석할 수 있는 객체를 반환합니다.

#rendered 메서드가 반환하는 String 콘텐츠를 객체로 변환하려면 .register_parser를 호출하여 구문 분석기를 정의합니다. .register_parser :rss를 호출하면 #rendered.rss 헬퍼 메서드가 정의됩니다. 예를 들어 렌더링된 RSS 콘텐츠를 객체로 구문 분석하려면 RSS::Parser.parse를 호출하여 등록하면 됩니다:

register_parser :rss, -> rendered { RSS::Parser.parse(rendered) }

test "renders RSS" do
  article = Article.create!(title: "Hello, world")

  render formats: :rss, partial: article

  assert_equal "Hello, world", rendered.rss.items.last.title
end

기본적으로 ActionView::TestCase는 다음과 같은 구문 분석기를 정의합니다:

test "renders HTML" do
  article = Article.create!(title: "Hello, world")

  render partial: "articles/article", locals: { article: article }

  assert_pattern { rendered.html.at("main h1") => { content: "Hello, world" } }
end

test "renders JSON" do
  article = Article.create!(title: "Hello, world")

  render formats: :json, partial: "articles/article", locals: { article: article }

  assert_pattern { rendered.json => { title: "Hello, world" } }
end

헬퍼 테스팅

헬퍼는 뷰에서 사용할 수 있는 메서드가 포함된 단순한 모듈입니다.

헬퍼를 테스트하려면 헬퍼 메서드의 출력이 예상대로 일치하는지 확인하면 됩니다. 헬퍼 관련 테스트는 test/helpers 디렉토리에 있습니다.

다음과 같은 헬퍼가 있다고 가정해 보겠습니다:

module UsersHelper
  def link_to_user(user)
    link_to "#{user.first_name} #{user.last_name}", user
  end
end

이 메서드의 출력을 다음과 같이 테스트할 수 있습니다:

class UsersHelperTest < ActionView::TestCase
  test "should return the user's full name" do
    user = users(:david)

    assert_dom_equal %{<a href="/user/#{user.id}">David Heinemeier Hansson</a>}, link_to_user(user)
  end
end

또한 테스트 클래스가 ActionView::TestCase를 확장하므로 link_to 또는 pluralize와 같은 Rails 헬퍼 메서드에 액세스할 수 있습니다.

메일러 테스팅

메일러 클래스를 테스트하려면 철저한 작업을 수행하기 위한 특정 도구가 필요합니다.

우편부 관리하기

메일러 클래스 - 다른 모든 Rails 애플리케이션 부분과 마찬가지로 - 예상대로 작동하는지 테스트해야 합니다.

메일러 클래스를 테스트하는 목표는 다음과 같습니다:

  • 이메일이 처리되고 있는지(생성 및 전송)
  • 이메일 내용이 올바른지(제목, 발신자, 본문 등)
  • 올바른 이메일이 올바른 시기에 전송되고 있는지

양면성

메일러 테스팅에는 두 가지 측면이 있습니다. 단위 테스트에서는 입력을 엄격하게 제어하고 출력을 알려진 값과 비교합니다. 기능 테스트에서는 메일러가 올바르게 사용되고 있는지 테스트하는 것이 목표이지 메일러의 세부 사항을 테스트하는 것은 아닙니다.

단위 테스팅

메일러가 예상대로 작동하는지 테스트하려면 단위 테스트를 사용하여 실제 결과를 미리 작성된 예제와 비교할 수 있습니다.

복수의 복수

메일러 단위 테스팅의 목적으로 fixtures는 출력의 예제를 제공하는 데 사용됩니다. 이는 Active Record 데이터가 아닌 예제 이메일이므로 다른 fixtures와 별도의 하위 디렉토리에 보관됩니다. 디렉토리 이름은 메일러 이름과 직접 대응됩니다. 따라서 UserMailer라는 메일러의 경우 fixtures는 test/fixtures/user_mailer 디렉토리에 있습니다.

메일러를 생성한 경우 생성기는 메일러 작업에 대한 고정 fixtures를 생성하지 않습니다. 위에 설명한 대로 직접 만들어야 합니다.

기본 테스트 케이스

UserMailer라는 이름의 메일러에 대한 단위 테스트의 예는 다음과 같습니다. invite 작업을 사용하여 친구를 초대하는 이메일을 보내는 것을 테스트합니다. 생성기가 생성한 기본 테스트의 수정된 버전입니다.

require "test_helper"

class UserMailerTest < ActionMailer::TestCase
  test "invite" do
    # 이메일을 생성하고 추가 어설션을 위해 저장합니다.
    email = UserMailer.create_invite("me@example.com",
                                     "friend@example.com", Time.now)

    # 이메일을 보내고 대기열에 추가되었는지 테스트합니다.
    assert_emails 1 do
      email.deliver_now
    end

    # 보낸 이메일의 본문에 예상되는 내용이 포함되어 있는지 테스트합니다.
    assert_equal ["me@example.com"], email.from
    assert_equal ["friend@example.com"], email.to
    assert_equal "You have been invited by me@example.com", email.subject
    assert_equal read_fixture("invite").join, email.body.to_s
  end
end

이 테스트에서는 이메일을 생성하고 email 변수에 결과 객체를 저장합니다. 그런 다음 실제로 전송되었는지(첫 번째 어설션) 확인하고 이메일에 예상되는 내용이 포함되어 있는지(두 번째 일괄 어설션) 확인합니다. read_fixture 헬퍼는 고정 파일의 내용을 읽는 데 사용됩니다.

참고: 이메일에 HTML 또는 텍스트 부분이 하나만 있는 경우 email.body.to_s를 사용합니다. 둘 다 있는 경우 email.text_part.body.to_s 또는 email.html_part.body.to_s를 사용하여 특정 부분을 테스트할 수 있습니다.

다음은 invite 고정의 내용입니다:

Hi friend@example.com,

You have been invited.

Cheers!

이제 메일러에 대한 테스트를 작성하는 방법에 대해 조금 더 자세히 알아보겠습니다. config/environments/test.rbActionMailer::Base.delivery_method = :test 줄은 테스트 모드로 배달 방법을 설정하므로 실제로 이메일이 전송되지 않습니다(사용자에게 스팸을 보내지 않도록 유용함). 대신 이메일이 ActionMailer::Base.deliveries 배열에 추가됩니다.

참고: ActionMailer::Base.deliveries 배열은 ActionMailer::TestCaseActionDispatch::IntegrationTest 테스트에서만 자네, 계속해서 번역하겠습니다.

참고: ActionMailer::Base.deliveries 배열은 ActionMailer::TestCaseActionDispatch::IntegrationTest 테스트에서만 자동으로 재설정됩니다. 이러한 테스트 케이스 외부에서 초기화하려면 수동으로 ActionMailer::Base.deliveries.clear를 호출해야 합니다.

대기열에 있는 이메일 테스트하기

assert_enqueued_email_with 어설션을 사용하면 예상된 메일러 메서드 인수 및/또는 매개변수화된 메일러 매개변수로 이메일이 대기열에 추가되었음을 확인할 수 있습니다. 이를 통해 deliver_later 메서드로 대기열에 추가된 모든 이메일을 일치시킬 수 있습니다.

기본 테스트 케이스와 마찬가지로 email 변수에 반환된 객체를 저장합니다. 다음 예에서는 인수 전달을 포함한 변형을 보여줍니다.

이 예에서는 올바른 인수로 이메일이 대기열에 추가되었음을 어설션합니다:

require "test_helper"

class UserMailerTest < ActionMailer::TestCase
  test "invite" do
    # 이메일을 생성하고 추가 어설션을 위해 저장합니다.
    email = UserMailer.create_invite("me@example.com", "friend@example.com")

    # 올바른 인수로 이메일이 대기열에 추가되었음을 테스트합니다.
    assert_enqueued_email_with UserMailer, :create_invite, args: ["me@example.com", "friend@example.com"] do
      email.deliver_later
    end
  end
end

이 예에서는 인수 이름을 사용하여 올바른 메일러 메서드 인수로 이메일이 대기열에 추가되었음을 어설션합니다:

require "test_helper"

class UserMailerTest < ActionMailer::TestCase
  test "invite" do
    # 이메일을 생성하고 추가 어설션을 위해 저장합니다.
    email = UserMailer.create_invite(from: "me@example.com", to: "friend@example.com")

    # 이름이 지정된 인수로 이메일이 대기열에 추가되었음을 테스트합니다.
    assert_enqueued_email_with UserMailer, :create_invite, args: [{ from: "me@example.com",
                                                                    to: "friend@example.com" }] do
      email.deliver_later
    end
  end
end

이 예에서는 매개변수화된 메일러가 올바른 매개변수와 인수로 대기열에 추가되었음을 어설션합니다. 메일러 매개변수는 params로, 메일러 메서드 인수는 args로 전달됩니다:

require "test_helper"

class UserMailerTest < ActionMailer::TestCase
  test "invite" do
    # 이메일을 생성하고 추가 어설션을 위해 저장합니다.
    email = UserMailer.with(all: "good").create_invite("me@example.com", "friend@example.com")

    # 올바른 메일러 매개변수와 인수로 이메일이 대기열에 추가되었음을 테스트합니다.
    assert_enqueued_email_with UserMailer, :create_invite, params: { all: "good" },
                                                           args: ["me@example.com", "friend@example.com"] do
      email.deliver_later
    end
  end
end

이 예에서는 매개변수화된 메일러가 올바른 매개변수로 대기열에 추가되었음을 테스트하는 다른 방법을 보여줍니다:

require "test_helper"

class UserMailerTest < ActionMailer::TestCase
  test "invite" do
    # 이메일을 생성하고 추가 어설션을 위해 저장합니다.
    email = UserMailer.with(to: "friend@example.com").create_invite

    # 올바른 메일러 매개변수로 이메일이 대기열에 추가되었음을 테스트합니다.
    assert_enqueued_email_with UserMailer.with(to: "friend@example.com"), :create_invite do
      email.deliver_later
    end
  end
end

기능 및 시스템 테스팅

단위 테스팅을 통해 이메일의 속성을 테스트할 수 있지만, 기능 및 시스템 테스팅을 통해 사용자 상호 작용이 적절하게 이메일 전송을 트리거하는지 테스트할 수 있습니다. 예를 들어 친구 초대 작업이 적절하게 이메일을 보내는지 확인할 수 있습니다:

# 통합 테스트
require "test_helper"

class UsersControllerTest < ActionDispatch::IntegrationTest
  test "invite friend" do
    # ActionMailer::Base.deliveries의 차이를 어설션합니다.
    assert_emails 1 do
      post invite_friend_url, params: { email: "friend@example.com" }
    end
  end
end
# 시스템 테스트
require "test_helper"

class UsersTest < ActionDispatch::SystemTestCase
  driven_by :selenium, using: :headless_chrome

  test "inviting a friend" do
    visit invite_users_url
    fill_in "Email", with: "friend@example.com"
    assert_emails 1 do
      click_on "Invite"
    end
  end
end

참고: assert_emails 메서드는 특정 배달 방법에 묶이지 않으며 deliver_now 또는 deliver_later 메서드로 전송된 이메일에 모두 작동합니다. 이메일이 대기열에 추가되었음을 명시적으로 어설션하려면 assert_enqueued_email_with(위의 예) 또는 assert_enqueued_emails 메서드를 사용할 수 있습니다. 자세한 내용은 여기에서 확인할 수 있습니다.

작업 테스팅

작업은 격리(작업 동작 중심) 및 컨텍스트(호출 코드 동작 중심)에서 테스트할 수 있습니다.

작업을 격리하여 테스팅하기

작업을 생성하면 관련 테스트 파일이 test/jobs 디렉토리에 생성됩니다.

청구 작업에 대한 예제 테스트는 다음과 같습니다:

require "test_helper"

class BillingJobTest < ActiveJob::TestCase
  test "account is charged" do
    perform_enqueued_jobs do
      BillingJob.perform_later(account, product)
    end
    assert account.reload.charged_for?(product)
  end
end

테스트에 사용되는 기본 큐 어댑터는 perform_enqueued_jobs가 호출될 때까지 작업을 수행하지 않습니다. 또한 각 테스트 실행 전에 모든 작업을 지워 테스트가 서로 영향을 미치지 않도록 합니다.

이 테스트는 perform_enqueued_jobsperform_later를 사용하여 perform_now를 사용하지 않습니다. 이렇게 하면 재시도가 구성된 경우 재시도 실패가 테스트에 의해 포착되고 다시 대기열에 추가되지 않습니다.

컨텍스트에서 작업 테스팅하기

작업이 올바르게 대기열에 추가되는지 테스팅하는 것이 좋습니다. 예를 들어 컨트롤러 작업에 의해 수행될 수 있습니다. ActiveJob::TestHelper는 이를 돕기 위해 여러 메서드를 제공하며, 그중 하나가 assert_enqueued_with입니다.

계정 모델 메서드를 테스트하는 다음 예를 참조하세요:

require "test_helper"

class AccountTest < ActiveSupport::TestCase
  include ActiveJob::TestHelper

  test "#charge_for enqueues billing job" do
    assert_enqueued_with(job: BillingJob) do
      account.charge_for(product)
    end

    assert_not account.reload.charged_for?(product)

    perform_enqueued_jobs

    assert account.reload.charged_for?(product)
  end
end

예외 발생 테스팅

특정 경우에 작업이 예외를 발생시키는지 테스팅하는 것은 까다로울 수 있습니다. 특히 재시도가 구성된 경우 더욱 그렇습니다. perform_enqueued_jobs 헬퍼는 작업에서 예외가 발생하면 테스트를 실패시키므로 예외가 발생할 때 테스트가 성공하려면 작업의 perform 메서드를 직접 호출해야 합니다.

require "test_helper"

class BillingJobTest < ActiveJob::TestCase
  test "does not charge accounts with insufficient funds" do
    assert_raises(InsufficientFundsError) do
      BillingJob.new(empty_account, product).perform
    end
    refute account.reload.charged_for?(product)
  end
end

이 방법은 일반적으로 권장되지 않습니다. 프레임워크의 일부를 우회하기 때문입니다(예: 인수 직렬화).

Action Cable 테스팅

Action Cable은 애플리케이션 내부의 다양한 수준에서 사용되므로 채널, 연결 클래스 자체 및 다른 엔터티가 올바른 메시지를 브로드캐스트하는지 테스트해야 합니다.

연결 테스트 케이스

기본적으로 Rails 애플리케이션을 새로 생성하면 기본 연결 클래스(ApplicationCable::Connection)에 대한 테스트가 test/channels/application_cable 디렉토리에 생성됩니다.

연결 테스트는 연결의 식별자가 올바르게 할당되는지 또는 부적절한 연결 요청이 거부되는지 확인하는 것을 목표로 합니다. 다음은 예입니다:

class ApplicationCable::ConnectionTest < ActionCable::Connection::TestCase
  test "connects with params" do
    # `connect` 메서드를 호출하여 연결 열기를 시뮬레이션합니다.
    connect params: { user_id: 42 }

    # 테스트에서 `connection` 객체에 액세스할 수 있습니다.
    assert_equal connection.user_id, "42"
  end

  test "rejects connection without params" do
    # `assert_reject_connection` 매처를 사용하여 연결이 거부되었음을 확인합니다.
    assert_reject_connection { connect }
  end
end

요청 쿠키도 통합 테스트와 동일한 방식으로 지정할 수 있습니다:

test "connects with cookies" do
  cookies.signed[:user_id] = "42"

  connect

  assert_equal connection.user_id, "42"
end

ActionCable::Connection::TestCase API 문서에서 자세한 내용을 확인할 수 있습니다.

채널 테스트 케이스

채널을 생성하면 관련 테스트가 test/channels 디렉토리에 자동으로 생성됩니다. 다음은 채팅 채널에 대한 예제 테스트입니다:

require "test_helper"

class ChatChannelTest < ActionCable::Channel::TestCase
  test "subscribes and stream for room" do
    # `subscribe` 호출로 구독 생성을 시뮬레이션합니다.
    subscribe room: "15"

    # 테스트에서 `subscription` 객체에 액세스할 수 있습니다.
    assert subscription.confirmed?
    assert_has_stream "chat_15"
  end
end

이 테스트는 매우 간단하며 채널이 특정 스트림에 연결되었는지만 어설션합니다.

기본 연네, 계속해서 번역하겠습니다.

기본 연결 식별자도 지정할 수 있습니다. 다음은 웹 알림 채널에 대한 예제 테스트입니다:

require "test_helper"

class WebNotificationsChannelTest < ActionCable::Channel::TestCase
  test "subscribes and stream for user" do
    stub_connection current_user: users(:john)

    subscribe

    assert_has_stream_for users(:john)
  end
end

ActionCable::Channel::TestCase API 문서에서 자세한 내용을 확인할 수 있습니다.

사용자 정의 어설션 및 다른 구성 요소 내부의 브로드캐스트 테스팅

Action Cable에는 테스트의 간결성을 높이기 위해 사용할 수 있는 사용자 정의 어설션이 많이 포함되어 있습니다. 사용 가능한 모든 어설션 목록은 ActionCable::TestHelper API 문서를 참조하세요.

다른 구성 요소(예: 컨트롤러 내부) 내에서 올바른 메시지가 브로드캐스트되었는지 확인하는 것이 좋습니다. 이 경우 Action Cable이 제공하는 사용자 정의 어설션이 매우 유용합니다. 예를 들어 모델 내에서:

require "test_helper"

class ProductTest < ActionCable::TestCase
  test "broadcast status after charge" do
    assert_broadcast_on("products:#{product.id}", type: "charged") do
      product.charge(account)
    end
  end
end

Channel.broadcast_to로 브로드캐스트된 것을 테스트하려면 Channel.broadcasting_for를 사용하여 기본 스트림 이름을 생성해야 합니다:

# app/jobs/chat_relay_job.rb
class ChatRelayJob < ApplicationJob
  def perform(room, message)
    ChatChannel.broadcast_to room, text: message
  end
end
# test/jobs/chat_relay_job_test.rb
require "test_helper"

class ChatRelayJobTest < ActiveJob::TestCase
  include ActionCable::TestHelper

  test "broadcast message to room" do
    room = rooms(:all)

    assert_broadcast_on(ChatChannel.broadcasting_for(room), text: "Hi!") do
      ChatRelayJob.perform_now(room, "Hi!")
    end
  end
end

열심히 로드하기

일반적으로 애플리케이션은 development 또는 test 환경에서 빨리 실행되도록 열심히 로드되지 않지만 production 환경에서는 그렇습니다.

그러나 프로젝트에 CI가 있는 경우 CI에서 열심히 로드하는 것이 쉽습니다.

CI는 일반적으로 테스트 스위트가 실행되고 있음을 나타내는 환경 변수를 설정합니다. 예를 들어 CI일 수 있습니다:

# config/environments/test.rb
config.eager_load = ENV["CI"].present?

Rails 7부터 새로 생성된 애플리케이션은 기본적으로 이렇게 구성됩니다.

독립 테스트 스위트

프로젝트에 지속적인 통합이 없는 경우에도 Rails.application.eager_load!를 호출하여 테스트 스위트에서 열심히 로드할 수 있습니다.

Minitest

require "test_helper"

class ZeitwerkComplianceTest < ActiveSupport::TestCase
  test "eager loads all files without errors" do
    assert_nothing_raised { Rails.application.eager_load! }
  end
end

RSpec

require "rails_helper"

RSpec.describe "Zeitwerk compliance" do
  it "eager loads all files without errors" do
    expect { Rails.application.eager_load! }.not_to raise_error
  end
end

추가 테스팅 리소스

시간 종속 코드 테스팅

Rails는 시간에 민감한 코드가 예상대로 작동하도록 하는 데 도움이 되는 기본 제공 헬퍼 메서드를 제공합니다.

다음 예에서는 travel_to 헬퍼를 사용합니다:

# 사용자가 등록 후 1개월 후에 선물을 받을 자격이 있습니다.
user = User.create(name: "Gaurish", activation_date: Date.new(2004, 10, 24))
assert_not user.applicable_for_gifting?

travel_to Date.new(2004, 11, 24) do
  # `travel_to` 블록 내에서 `Date.current`가 스텁됩니다.
  assert_equal Date.new(2004, 10, 24), user.activation_date
  assert user.applicable_for_gifting?
end

# 변경 사항은 `travel_to` 블록 내에서만 표시되었습니다.
assert_equal Date.new(2004, 10, 24), user.activation_date

사용 가능한 시간 헬퍼에 대한 자세한 내용은 ActiveSupport::Testing::TimeHelpers API 참조를 참조하세요.