본문 바로가기

Ruby On Rails/RSpec

RSpec 설정 이야기

1. 개요

  1. 이 글은 RSpec으로 test를 편하게 사용했으면 하는 바람에 작성하였습니다. 이 글에서 다룰 내용들은 먼저 RSpec이 추구하는 바에 대해 간략히 설명하고 설정과 관련하여 알고 있어야 하는 내용들을 적을 생각입니다. 또한 각 spec들을 어떻게 작성하면 좋을지에 대해서 하나하나 다루겠습니다. 특히, 통합 테스트와 비즈니스 로직에 대한 내용에 집중할 것입니다.

2. RSpec이란

구현 테스트보단 행동 테스트에 초점을 맞추고 개발자가 편하게 테스트를 할 수 있게 지원해주는 API입니다.

3. RSpec 전체 구조

1. 기본 구조

- 이 것이 기본 구조입니다. 다른 spec을 추가하고 싶다면 spec 아래에 만들어주시면 되겠습니다.

2. gem

group :test do   
  gem 'rspec-rails'   
  gem 'database_cleaner'   
  gem 'shoulda-matchers'   
  gem 'factory_bot_rails'   
  gem 'faker'   
  gem 'capybara'   
  gem 'webdrivers' 
end 
group :development do   
  gem 'spring'   
  gem 'spring-watcher-listen', '~> 2.0.0'   
  gem 'spring-commands-rspec'   
  gem 'guard-rspec' 
end
  1. database_cleaner는 모든 spec에서 쓰이지만, 특히 service spec에서 중요하게 쓰입니다.
  2. shoulda-matchers는 model spec에서 중요하게 쓰입니다.
  3. factorybot은 가상 객체를 만들어서 실제 모델처럼 사용할 수 있게 지원합니다.
  4. faker는 인스턴스 변수를 쉽게 생성해줍니다.
  5. capybara는 백엔드 api에서 js를 테스트하기 어려운 환경을 지원해주는 젬입니다. js 이벤트를 발생시키는 메서드를 제공해줍니다.
  6. webdrivers는 capybara를 사용할 때, 웹 브라우저에서 테스트를 볼 수 있도록 지원해줍니다.
  7. spring은 test할 때 설정을 preload 해줍니다.
  8. guard 서버를 켜고 코드를 작성하면 작성된 코드를 자동적으로 테스트를 진행시켜줍니다.

3. spec/support/….rb 설정

 1. ./capybara.rb

Capybara.default_max_wait_time = 5 
Capybara.default_driver = :selenium_chrome

 2. ./database_cleaner.rb

DatabaseCleaner[:active_record].strategy = :transaction
DatabaseCleaner[:active_record, { :connection => :db2 }]
DatabaseCleaner[:active_record, { :connection => :db3 }]

RSpec.configure do |config|
  config.before(:suite) do
    DatabaseCleaner.clean_with(:truncation)
  end
  
  config.before(:each) do
    DatabaseCleaner.strategy = :transaction
    DatabaseCleaner.start
  end

  config.before(:each, :js => true) do
    DatabaseCleaner.strategy = :truncation
  end

  config.after(:each) do
    DatabaseCleaner.clean
  end
end

 

 3. ./devise.rb

RSpec.configure do |config|
  config.include Devise::TestHelpers, :type => :controller
end

 4. ./factory_bot.rb

RSpec.configure do |config|
  config.include FactoryBot::Syntax::Methods
end

 5. ./helpers.rb

Dir[Rails.root.join('spec', 'support', 'helpers', '**', '*.rb')].each { |f| require f }
RSpec.configure do |config|
  config.include Feature::SessionHelper, type: :feature
end

 6. ./shoulda_matchers.rb

Shoulda::Matchers.configure do |config|
  config.integrate do |with|
    with.test_framework :rspec
    with.library :rails
  end
end

4. spec/rails_helper.rb

  1. 모든 설정을 support 아래에서 하고 실제 테스트를 할 spec에서는 이 파일만 require 해서 각 spec에서 별다른 설정을 따로 진행하지 않고 테스트해야 될 코드에만 집중하도록 해줘야 합니다.
ENV['RAILS_ENV'] ||= 'test'
require File.expand_path('../../config/environment', __FILE__)
abort("The Rails environment is running in production mode!") if Rails.env.production?

require 'spec_helper'
require 'rspec/rails'

Dir[Rails.root.join('spec', 'support', '**', '*.rb')].each { |f| require f }
begin
  ActiveRecord::Migration.maintain_test_schema!
rescue ActiveRecord::PendingMigrationError => e
  puts e.to_s.strip
  exit 1
end
RSpec.configure do |config|
  config.fixture_path = "#{::Rails.root}/spec/fixtures"
  config.use_transactional_fixtures = false
  config.infer_spec_type_from_file_location!
  config.filter_rails_from_backtrace!
end

5. spec/spec_helper.rb

RSpec.configure do |config|
  config.expect_with :rspec do |expectations|
    expectations.include_chain_clauses_in_custom_matcher_descriptions = true
  end
  config.mock_with :rspec do |mocks|
    mocks.verify_partial_doubles = true
  end
  config.shared_context_metadata_behavior = :apply_to_host_groups
end

6. Guardfile

  1. 자동 테스트에 대한 설정입니다. 가드 서버를 켜놓는다면, 코드의 변경을 감지해서 자동으로 테스트를 진행합니다. 만약 진행이 되지 않는다면, Guard::RSpec::Dsl 안의 코드를 확인하시고 내가 자동 테스트를 진행하고 싶은 부분을 Guardfile에 추가해주시면 되겠습니다. 아래에서는 각 spec은 실제 service, model 등과 이름이 같은 것끼리 매칭이 되도록 설정이 되어있습니다. 다만 controller 같은 경우 feature spec과 매칭이 되도록 수정을 해놓았습니다.
  2. bundle exec guard init rspec 
    1. 이 명령어를 입력하면 Guardfile이 생성됩니다.
guard :rspec, cmd: "spring rspec -f doc" do
  require "guard/rspec/dsl"
  dsl = Guard::RSpec::Dsl.new(self)

  # Feel free to open issues for suggestions and improvements

  # RSpec files
  rspec = dsl.rspec
  watch(rspec.spec_helper) { rspec.spec_dir }
  watch(rspec.spec_support) { rspec.spec_dir }
  watch(rspec.spec_files)

  # Ruby files
  ruby = dsl.ruby
  dsl.watch_spec_files_for(ruby.lib_files)

  # Rails files
  rails = dsl.rails(view_extensions: %w(erb haml slim))
  dsl.watch_spec_files_for(rails.app_files)
  dsl.watch_spec_files_for(rails.views)

  watch(rails.controllers) do |m|
    [
      rspec.spec.call("routing/#{m[1]}_routing"),
      # rspec.spec.call("controllers/#{m[1]}_controller"),
      rspec.spec.call("features/#{m[1]}"),
      rspec.spec.call("acceptance/#{m[1]}")
    ]
  end

  # Rails config changes
  watch(rails.spec_helper) { rspec.spec_dir }
  watch(rails.routes) { "#{rspec.spec_dir}/routing" }
  watch(rails.app_controller) { "#{rspec.spec_dir}/features" }

  # Capybara features specs
  watch(rails.view_dirs) { |m| rspec.spec.call("features/#{m[1]}") }
  watch(rails.layouts) { |m| rspec.spec.call("features/#{m[1]}") }

  # Turnip features and steps
  watch(%r{^spec/acceptance/(.+)\.feature$})
  watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) do |m|
    Dir[File.join("**/#{m[1]}.feature")][0] || "spec/acceptance"
  end
end
  1.  

4. 중요한 설정

  1. local mysql
    1. test 환경은 보통 로컬 db에서 진행합니다. user는 root에서 접근을 하고 비밀번호는 없게 설정하는 방법도 있습니다.
    2. 설치
      1. brew install mysql
    3. mysql 비밀번호 없이 test 환경에 접속하기
      1. ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '';
        1. local에서 mysql에 접속 후 이 쿼리를 날려줄 것.
    4. 이슈들
      1. 테스트하시는 중에 테스트 서버 설정이 개발 서버 설정과 달라서 에러가 나는 경우가 있습니다. 그때마다 어떤 에러였는지와 해결책을 댓글로 남겨주시면 여기에 하나씩 추가하겠습니다.
      2. vi /etc/my.cnf
        1. 만약 my.cnf 파일이 없으시다면 새로 만드셔서 추가하시면 되겠습니다.
        2. 이 경로는 private이므로 sudo로 접근하셔야 합니다.
      3. group에 대한 이슈(SELECT list is not in GROUP BY clause 블라블라)
        1. my.cnf
          1. [mysqld]
          2. sql_mode=NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES
  2. test 환경 DB 생성 후 스키마 로드(혹은 마이그레이션 해줄 것)
    1. rails db:drop RAILS_ENV=test
    2. rails db:create RAILS_ENV=test
    3. rails db:schema:load RAILS_ENV=test
  3. RSpec 설정에 대한 설명
    1. 설정 구조
      1. spec/support/…rb
        1. support 하위에서 설정과 관련된 .rb 파일들을 적어주고 spec.rb 파일들에서는 rails_helper.rb 만 require 해서 사용할 수 있도록 해줍니다.
    2. 설정 방법
RSpec.configure do |config|
  config.include FactoryBot::Syntax::Methods
end

1) 일반적으로 테스트를 위한 gem이나 다른 설정 파일을 RSpec 설정에 등록할 때 이러한 방식을 따릅니다. 물론 다른 방식으로 등록하는 경우도 있는데, 그러한 경우는 해당 gem reference에 어떻게 등록해야 하는지 나와있습니다.

 

2) 여기에서 .configure란 RSpec static method에 접근을 해서 yield를 발생시켜 RSpec 코어에 있는 Configuration을 생성합니다. 만약 Configuration class가 존재한다면 인스턴스변수에 저장이 되어있고, 이 인스턴스변수를 사용합니다. (ruby에서 yield를 발생시킨다는 의미는 그 함수에 연관된 메서드를 호출하는 것으로 생각하시면 이해가 편합니다.)

이렇게 support 디렉터리 아래에 설정을 쭉 해주면 rails_helper.rb 에서 이 모든 설정 데이터를 import해줘서 다른 테스트 파일에서 rails_helper.rb 파일만 import하면 테스트를 편하게 할 수 있도록 설정하였습니다.

 

       3. database_cleaner.rb

1) database와 연관 지어 테스트를 할 장소는 business logic이 쓰인 services에서 입니다. 그리고 이 서비스 안에서 많은 db resources를 사용하게 되는데요. 그래서 db 설정을 정확하고 테스트하기 편하게 하기 위해 여러 글들을 읽고 테스트 설정에 대한 테스트를 진행했었습니다.

DatabaseCleaner[:active_record].strategy = :transaction
  1. 이렇게 전략을 transaction으로 주어야 it 블록 안에 들어가는 데이터들을 실제 db가 관리하듯 관리해줍니다. 즉, transaction을 주지 않으면 테스트 db에서는 테스트를 재시작할 때, PK가 항상 1부터 1씩 증가합니다. 이건 데이터가 있거나 없거나 상관하지 않습니다. 그래서 이렇게 전략을 주게 되면 테스트를 여러 번 하더라도 PK의 중복 에러 발생을 막을 수 있습니다.
DatabaseCleaner[:active_record, { :connection => :db2 }]
DatabaseCleaner[:active_record, { :connection => :db3 }]
  1. 보통 프로젝트에서 하나의 database만 사용하지 않고 여러 개의 db를 사용합니다. 그러므로 다른 데이터베이스를 사용하고 있다면 그 데이터베이스에 대한 설정을 따로 해줘야만 합니다. 물론 하나의 db만 사용한다면 이 설정은 해줄 이유가 없습니다. 이렇게 다른 데이터베이스를 연결시켜야 한다면 위와 같은 방식으로 connection 다음에 데이터베이스 이름을 넣어주시면 되겠습니다.

 

 

'Ruby On Rails > RSpec' 카테고리의 다른 글

FactoryBot 작성방법  (0) 2021.02.06
helper spec  (0) 2021.02.06
각 Spec 기본 구조  (0) 2021.02.06
RSpec 실행 명령어  (0) 2021.01.30
RSpec 구조에 대한 이야기  (0) 2021.01.10