RSpec은 BDD프로세스에서 쓰는 훌륭한 툴입니다. (BDD 어플리케이션의 개발과정을 확인하는 사람이 읽을 수 있는 명세서를 적는 일을 말합니다.)

웹에서는 RSpec를 어떻게 사용하는지를 설명하고 _무엇을_ 할 수 있는 지 설명한 전체 오버뷰를 찾을 수 있습니다. 하지만 RSpec으로 좋은 테스트 스위트를 만드는 지를 설명한 글을 찾기란 쉽지 않습니다.

Better Specs 은 대부분의 가이드라인의 누락 된 부분을 모으고자 노력했습니다. - 이건 개발자들의 수년간의 경험에서 배운 방법이죠.

함수를 설명하는 방법

어떤 함수를 설명하려는지 명확하게 하세요. 예를들어, 클래스 함수를 참조할 때는 .(아니면 ::) 를 접두어로, 인스턴스 함수를 참조할 때는 #를 접두어로 사용하는 루비 문서 규칙을 사용하세요.

bad

describe 'the authenticate method for User' do
describe 'if the user is an admin' do

good

describe '.authenticate' do
describe '#admin?' do

이 가이드라인에 대해 토론하기 →

Contexts 사용하기

Contexts는 테스트를 명확하고 잘 조직되게하는 강력한 방법입니다. 장기적으로 이 방법은 테스트를 읽기 편하게 만듭니다.

bad

it 'has 200 status code if logged in' do
  expect(response).to respond_with 200
end
it 'has 401 status code if not logged in' do
  expect(response).to respond_with 401
end

good

context 'when logged in' do
  it { is_expected.to respond_with 200 }
end
context 'when logged out' do
  it { is_expected.to respond_with 401 }
end

Contexts는 "when"이나 "with"로 설명을 시작하세요.

이 가이드라인에 대해 토론하기 →

설명을 짧게 유지하기

명세의 설명은 40문자를 넘으면 안됩니다. 40문자를 넘을때는 context를 사용해 쪼개야 합니다.

bad

it 'has 422 status code if an unexpected params will be added' do

good

context 'when not valid' do
  it { is_expected.to respond_with 422 }
end

예제에서 it { is_expected.to respond_with 422 } 조건문으로 바꿈으로써 스테이더스 코드에 관한 설명을 제거했습니다. rspec filename를 쳐서 이 테스트를 실행해보면 읽을 수 있는 출력을 얻을 것입니다.

Formatted Output

when not valid
  it should respond with 422

이 가이드라인에 대해 토론하기 →

단일 조건 테스트

'단일 조건'에 관한 팁은 '각 테스트는 한가지만 확인해야 한다.'로 더 널리 알려저 있습니다. 이것은 에러를 찾을 때 도움이 되고, 실패하는 테스트를 바로 찾을수 있게 하고, 코드를 읽기 편하게 합니다.

독립된 유닛 스팩에서, 일반적으로 각 예제는 단(!) 하나의 행동만 테스트 하길 바랄 것 입니다. 같은 예제 안에서 여러 예상치가 나온다면 여러 행동으로 나누어야 할것 같다는 신호입니다.

어쨋든 분리되지 않은 테스트에서(예를 들어 디비나 외부 서비스와 연동하거나 끝에서 끝까지 테스트 하는 경우), 분리하기만하면 같은 셋업을 여러번 하게 되어 테스트가 무거워지는 현상이 나타납니다. 이런 종류의 무거운 테스트에선 굳이 나누지 않아도 괜찮을 것 같아요.

good (isolated)

it { is_expected.to respond_with_content_type(:json) }
it { is_expected.to assign_to(:resource) }

Good (not isolated)

it 'creates a resource' do
  expect(response).to respond_with_content_type(:json)
  expect(response).to assign_to(:resource)
end

이 가이드라인에 대해 토론하기 →

모든 가능한 케이스를 테스트하기

테스트는 하는게 좋습니다만, 모든 케이스를 테스트 하지 않으면, 유용하지 않게 됩니다. 유효한 경우와 유효하지 않은 경우를 모두 테스트 하세요. 예를들어 이런 액션이 있다고 해봅시다.

Destroy action

before_filter :find_owned_resources
before_filter :find_resource

def destroy
  render 'show'
  @consumption.destroy
end

흔히 보는 에러로 리소스가 잘 제거되었는지만 테스트하고 있습니다. 하지만 최소한 두개의 경계 케이스가 존재합니다: 리소스를 찾지 못했을 때와 소유하지 않았을 때. 모든 일반적으로 가능한 입력을 생각해보고 테스트 해야합니다.

bad

it 'shows the resource'

good

describe '#destroy' do

  context 'when resource is found' do
    it 'responds with 200'
    it 'shows the resource'
  end

  context 'when resource is not found' do
    it 'responds with 404'
  end

  context 'when resource is not owned' do
    it 'responds with 404'
  end
end

이 가이드라인에 대해 토론하기 →

Expect vs Should 문법

새 프로젝트에서는 항상 expect 문법만 사용합니다.

bad

it 'creates a resource' do
  response.should respond_with_content_type(:json)
end

good

it 'creates a resource' do
  expect(response).to respond_with_content_type(:json)
end

Rspec을 새로운 문법만 허용하도록 설정할수있습니다. 모든 코드에서 2의 문법을 사용 못하도록 말이죠.

good

# spec_helper.rb
RSpec.configure do |config|
  # ...
  config.expect_with :rspec do |c|
    c.syntax = :expect
  end
end

한 줄의 expectation이나 묵시적 subject를 사용할 경우 is_expected.to를 사용해야합니다.

bad

context 'when not valid' do
  it { should respond_with 422 }
end

good

context 'when not valid' do
  it { is_expected.to respond_with 422 }
end

오래된 프로젝트에선 transpec 을 사용해 새로운 문법으로 변환할 수 있습니다.

새Rspec expectation 문법에 대한 정보가 더필요하시면 여기여기를 보세요.

이 가이드라인에 대해 토론하기 →

Subject 사용하기

만약 같은 subject에 대해 여러 테스트를 하고있다면, subject{} 를 이용해 반복을 제거(DRY) 합니다.

bad

it { expect(assigns('message')).to match /it was born in Belville/ }

good

subject { assigns('message') }
it { is_expected.to match /it was born in Billville/ }

RSpec은 명시적인 subject도 지원합니다.

Good

subject(:hero) { Hero.first }
it "carries a sword" do
  expect(hero.equipment).to include "sword"
end

rspec subject에 대해 좀 더 알아보기.

이 가이드라인에 대해 토론하기 →

let 과 let! 사용하기

만약 before 인스턴스 변수를 만들지 않고 변수를 할당해야 한다면, let을 사용합시다. let 사용하면 변수를 테스트에서 처음 사용 될때만 lazy load하고 테스트 명세가 끝날때까지 캐슁할 수 있습니다. 이 스택오버 플로우 글에서 let에 대해 설명해 놓았네요.

bad

describe '#type_id' do
  before { @resource = FactoryGirl.create :device }
  before { @type     = Type.find @resource.type_id }

  it 'sets the type_id field' do
    expect(@resource.type_id).to equal(@type.id)
  end
end

good

describe '#type_id' do
  let(:resource) { FactoryGirl.create :device }
  let(:type)     { Type.find resource.type_id }

  it 'sets the type_id field' do
    expect(resource.type_id).to equal(type.id)
  end
end

액션을 초기화하기 위해 let을 사용하면 명세을 테스트할때 lazy load됩니다.

good

context 'when updates a not existing property value' do
  let(:properties) { { id: Settings.resource_id, value: 'on'} }

  def update
    resource.properties = properties
  end

  it 'raises a not found error' do
    expect { update }.to raise_error Mongoid::Errors::DocumentNotFound
  end
end

블록이 정의 될때 변수가 정의되길 원한다면let!을 사용하세요. 이것은 쿼리나 스코프같은 데이터베이스 테스트를 할때 유용합니다.

let이 무엇인지에 대한 예제입니다.

good

# this:
let(:foo) { Foo.new }

# is very nearly equivalent to this:
def foo
  @foo ||= Foo.new
end

rspec let에 대해 더 알아보기.

이 가이드라인에 대해 토론하기 →

Mock을 하느냐 마느냐

논쟁은 진행중입니다. 가능한한 (과도하게) mock을 사용하지 말고 실제 행동을 테스트해야 합니다. 실제 케이스를 테스트 하는것은 흐름을 변경 할때 유용합니다.

good

# simulate a not found resource
context "when not found" do
  before { allow(Resource).to receive(:where).with(created_from: params[:id]).and_return(false) }
  it { is_expected.to respond_with 404 }
end

mock은 스팩을 빠르게 하지만 사용하기 힘듭니다. 그래서 잘 사용하려면 mock을 잘 이해해야 합니다. 이 글을 읽어 보세요.

이 가이드라인에 대해 토론하기 →

필요한 데이터만 만들기

만약 중형(혹은 소형) 프로젝트에서 일한 경험밖에 없다면, 테스트 스위트는 무거워 질 수 있습니다. 이 문제를 해결하기 위해, 필요이상의 데이터를 로드하지 않는 것은 중요합니다. 또한 여러 데이터가 필요하다고 하면 아마도 생각이 틀렸을 가능성이 있습니다.

good

describe "User" do
  describe ".top" do
    before { FactoryGirl.create_list(:user, 3) }
    it { expect(User.top(2)).to have(2).item }
  end
end

이 가이드라인에 대해 토론하기 →

픽스쳐대신 팩토리 사용하기

이것은 오래된 이야기입니다만, 여전히 언급할 가치가 있습니다. 픽스쳐를 사용하지마세요. 왜냐하면 픽스쳐는 사용하기 어렵기 때문입니다. 대신 팩토리를 사용하세요. 팩토리를 사용하면 새로운 데이터를 생성할 때 코드를 줄일 수 있습니다.

bad

user = User.create(
  name: 'Genoveffa',
  surname: 'Piccolina',
  city: 'Billyville',
  birth: '17 Agoust 1982',
  active: true
)

good

user = FactoryGirl.create :user

주의! 베스트 프락티스에서 유닛테스트에 대해 이야기할때는 픽스쳐도 팩토리도 사용하지 않았습니다. 복잡하게 팩토리나 픽스쳐 셋업을 하는데 시간낭비하지 않으면, 라이브러리에 로직을 추가하는 시간을 벌 수 있습니다. 이 글을 읽어보세요.

Factory Girl에 대해 더 알아보기.

이 가이드라인에 대해 토론하기 →

읽기 쉬운 matcher 사용하기

읽기 쉬운 matcher를 사용하세요. rspec matcher를 보세요. 두번 보세요.

bad

lambda { model.save! }.to raise_error Mongoid::Errors::DocumentNotFound

good

expect { model.save! }.to raise_error Mongoid::Errors::DocumentNotFound

이 가이드라인에 대해 토론하기 →

Shared Examples

테스트를 만드는것은 좋은 일이며 매일 조금씩 더 자신감을 갖게합니다. 그러나 결국 중복코드가 여기저기 발생하는 것을 보게 되는데요. shared example을 이용해서 테스트 스위트의 중복을 제거하세요.

bad

describe 'GET /devices' do
  let!(:resource) { FactoryGirl.create :device, created_from: user.id }
  let(:uri) { '/devices' }

  context 'when shows all resources' do
    let!(:not_owned) { FactoryGirl.create factory }

    it 'shows all owned resources' do
      page.driver.get uri
      expect(page.status_code).to be(200)
      contains_owned_resource resource
      does_not_contain_resource not_owned
    end
  end

  describe '?start=:uri' do
    it 'shows the next page' do
      page.driver.get uri, start: resource.uri
      expect(page.status_code).to be(200)
      contains_resource resources.first
      expect(page).to_not have_content resource.id.to_s
    end
  end
end

good

describe 'GET /devices' do

  let!(:resource) { FactoryGirl.create :device, created_from: user.id }
  let(:uri)       { '/devices' }

  it_behaves_like 'a listable resource'
  it_behaves_like 'a paginable resource'
  it_behaves_like 'a searchable resource'
  it_behaves_like 'a filterable list'
end

경험에 의하면, shared example은 주로 컨트롤러에 사용됩니다. 모델은 서로 꽤나 다르게 생겨서, (보통은) 많은 부분을 공유하지 않습니다.

rspec shared examples에 대해 더 알아보기.

이 가이드라인에 대해 토론하기 →

보이는것을 테스트 하기

모델과 어플리케이션의 행동를 깊게 (통합) 테스트합니다. 컨트롤러를 테스트 하느라 쓸데없는 복정성을 넣지 마세요.

내가 처음 어플을 테스트했을때는 컨트롤러를 테스트했지만, 지금은 하지 않습니다. 지금 나는 RSpec과 Capybara를 사용한 통합 테스트만 만듭니다. 왜나구요? 왜냐면 진짜로 보이는 것을 테스트해야한다고 믿고 있고 컨트롤러를 테스트 하는것은 불필요한 단계라 생각하기 때문입니다. 하다보면 테스트들은 모델과 통합테스트로 들어가고, 그것들은 쉽게 shared examples로 묶어지고, 깨끗하고 읽기 편한 테스트 스위트를 만들게 될 것 입니다.

이 사안은 루비 커뮤니티에서 아직 토론중이며 양측 다 주장을 보충해줄 좋은 근거들을 가지고 있습니다. 컨트롤러를 테스트 할 필요가 있다고 주장하는 측은 인테그레이션 테스트로 모든 기능을 커버할수없고 느리다고 주장합니다.

둘 다 아닙니다.당신은 쉽게 모든 기능을 커버할 수 있으시고 (안 그러신가요?) Guard같은 자동화 툴을 이용해 한파일만 테스트 할 수 있습니다. 이렇게하면 흐름을 끊기지 않는 범위내에서 필요한 사양만 테스트할 수 있게 됩니다.

이 가이드라인에 대해 토론하기 →

should를 쓰지 않기

테스트를 설명할때 should를 사용하지 마세요. 현제 시제의 3인칭 시점으로 작성합시다. 새로나온 expectation 를 사용하시면 더욱 좋습니다.

bad

it 'should not change timings' do
  consumption.occur_at.should == valid.occur_at
end

good

it 'does not change timings' do
  expect(consumption.occur_at).to equal(valid.occur_at)
end

should를 사용하지 않도록 강제할때 사용할 수 있는 should_not잼을 확인해보세요. 그리고 이미 작성된 rspec에서 "should."로 시작되는 예문을 지워주는 the should_clean잼도 확인 해보세요.

이 가이드라인에 대해 토론하기 →

guard로하는 테스트 자동화

앱을 변경할때마다 테스트 스위트를 돌리는 일은 성가실수 있습니다. 그건 매우 시간이 오래걸리고 흐름을 끊을 수 있습니다. Guard를 사용하면 갱신된 사양, 모델, 컨트롤러, 파일에 대해서만 테스트를 수행하도록 테스트 스위트를 자동화 할 수 있습니다.

good

bundle exec guard
기본적인 리로딩 룰이 적혀있는 샘플 Guardfile 입니다.

good

guard 'rspec', cli: '--drb --format Fuubar --color', version: 2 do
  # 모든 갱신된 사양 파일에 대해서 실행
  watch(%r{^spec/.+_spec\.rb$})
  # lib/폴더안의 파일이 변경되었을때 lib사양을 실행
  watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
  # 모델이 변경 되었을때 관련된 모델사양을 실행
  watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
  # 뷰가 변경 되었을때 관련된 뷰사양을 실행
  watch(%r{^app/(.*)(\.erb|\.haml)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" }
  # 컨트롤러가 변경되었을때 관련된 통합 사양을 실행
  watch(%r{^app/controllers/(.+)\.rb}) { |m| "spec/requests/#{m[1]}_spec.rb" }
  # 어플리케이션 컨트롤러가 변경 되었을때 모든 통합테스트를 실행
  watch('app/controllers/application_controller.rb') { "spec/requests" }
end

Guard는 좋은 툴이지만 보통 모든 요구사항을 충족하지 못합니다. TDD로 일하면서 단축키설정해서 원할때 실행하는게 가장 좋을때도 있습니다. 그리고 푸쉬하기 전에 rake로 전체 코드를 테스트 하는 거죠.여기에 vim의 단축키 설정이 있습니다.

guard-rspec에 대해 더 알아보기.

이 가이드라인에 대해 토론하기 →

레일즈를 미리 로드해두어 빠른 테스트하기

레일즈에서 테스트를 실행할때는 전채 레일즈 앱을 로딩합니다. 이것은 시간이 걸리고 일의 흐름을 끊을 수 있습니다. 이 문제를 해결하기 위해선 ZeusSpinSpork같은 것을 사용할 수 있습니다. Spork는 (보통) 변경되지 않은 컨트롤러, 모델, 뷰, 팩토리와 자주 변경하는 파일 같은 모든 라이브러리를 미리 로드합니다.

여기에 Spork용 spec helperGuardfile 설정이 있습니다. 이 설정을 사용하면 미리 로드된 (initializers같은) 파일이 변경되면 전체 어플을 리로드 시키고, 단일 테스트를 매우 매우 빠르게 실행 할 수 있습니다.

Spork는 굉장히 많은 부분을 몽키 패치하기 때문에 쓰다보면 파일이 리로드 되지 않는 이유를 파악하기 위해 몇시간을 허비하는 일이 있습니다. 만약에 Spin 이나 다른 솔루션의 예제가 있으시다면 알려주세요.

Zeus를 사용하기 위한 Guardfile 설정은 여기 있습니다. spec_helper는 수정하실 필요가 없지만, 테스트를 하시기 전에 콘솔에서 `zeus start`를 실행하셔야합니다.

모든면에서 Zeus는Spork보다 덜 공격적인 방법을 사용합니다. 단점이라면 요구사항이 꽤 엄격한 편입니다. Ruby 1.9.3이상(backported GC를 쓸수 있는 루비 2.0 이상을 권장합니다) FSEvents 나 inotify를 사용할 수 있는 OS를 필요로 합니다.

많은 사람들이 Spork에서 다른 솔루션으로 옮겼습니다. 이 솔루션들은 좋은 설계로 앞선 문제의 나은 해결책이 되고, 의도적으로 필요한 부분만 로딩하고있습니다. 더 알고싶으시면 관련 토론을 읽어보세요.

이 가이드라인에 대해 토론하기 →

HTTP 리퀘스트를 mock하기

가끔 외부서비스를 억세스할 필요할 때가 있습니다. 이런 경우 실제 서비스에 의존시키지 말고, webmock같은 솔루션을 이용해 stub해야 합니다.

good

context "with unauthorized access" do
  let(:uri) { 'http://api.lelylan.com/types' }
  before    { stub_request(:get, uri).to_return(status: 401, body: fixture('401.json')) }
  it "gets a not authorized notification" do
    page.driver.get uri
    expect(page).to have_content 'Access denied'
  end
end

webmockVCR에 대해 더 알아보기. 둘을 어떻게 같이 쓰는 지 설명한 좋은 프리젠테이션 도 있습니다.

이 가이드라인에 대해 토론하기 →

유용한 formatter

formatter를 사용하면 테스트 스위트에관한 유용한 정보를 얻을수있습니다. 개인적으로는 fuubar가 굉장히 좋았습니다. 실행하시 위해선 gem을 설치하고 Guardfile에 fuubar를 기본 formatter로 설정하면 됩니다.

good

# Gemfile
group :development, :test do
  gem 'fuubar'

good

# .rspec
--drb
--format Fuubar
--color

fuubar에 대해 더 알아보기.

이 가이드라인에 대해 토론하기 →

Styleguide

We are seeking for the best guidelines to write "nice to read" specs. Right now a good starting point is for sure the Mongoid test suite. It uses a clean style and easy-to-read specs, following most of the guidelines described here.

Improving Better Specs

This is an open source project. If something is missing or incorrect just file an issue to discuss the topic. Also check the following issues:

  • Multilanguage (file an issue if you want to translate this guide)

Credits

This project was created by and released as open-source thanks to Lelylan, a new platform to monitor and control your devices through a simple, open and robust REST API.

If you like what I'm doing offer me a coffe