RSpec 입문 그 1번 「RSpec의 기본적인 구조나 편리한 기능을 이해하자!」

Jinsu Kim·2021년 2월 27일
5

Rails-Rspec

목록 보기
1/3

일본 ruby & rails에서는 유명하신 Junichi Ito 씨가 쓴 Rspec 테스트 글이다.
Rspec을 공부하기 위해서는 너무나도 좋은 글인 것 같아. Juinchi Ito씨에게 양해를 구한 뒤 허락을 받고 번역하였습니다. 읽어보시고 이상한 곳이 있으면 댓글로 언제든지 말씀해주시면 감사드리겠습니다!

실제 일본어로 되어 있는 사이트는 아래와 같다! 일본어가 가능하신분은 일본어로 읽는 것을 추천드립니다!
https://qiita.com/jnchito/items/42193d066bd61c740612

자 그럼 RSpec에 대해 시작해보겠습니다! 아래 부터가 번역입니다!(가끔 저의 의견이 들어가있기 때문에 그 점에 대해서는 참고 부탁드립니다^^)

📝 개요

RSpec을 검색해보면 '어렵다', '이해하기 어렵다' 이런 댓글을 가끔 볼 수 있다.
확실히 좀 독특한 문법을 가지고 있고, 기능도 상당히 많기 때문에 그런 댓글들도 이해가 됩니다.

하지만 RSpec에 한정하지 않고 어떤 프레임 워크에서도 동일하겠지만 익숙해지면 술술 쓸 수 있으며, 실제로 Jinichi Ito씨는 "RSpec 편리하네" 라고 생각하면서 테스트 코드를 작성하고 있다고 합니다. 저도 이 글을 정리하고 테스트를 직접해보니 엄청나게 편리하고 눈에 잘들어왔습니다!

이글에서는 Junichi Ito 씨가 생각하기에 '최소한 여기 만이라도 알고 있으면 괜찮아!' "라는 RSpec 구문과 실제로 사용하면서 편리했던 기능들이 정리되어 있습니다.

구체적으로는 이 글에서 설명할 내용들입니다.

  1. describe / it / expect의 역할
  2. 중첩 된 describe
  3. context
  4. before
  5. let / let! / subject
  6. shared_examples
  7. shared_context
  8. pending와 skip의 구분

🧑‍💻대상 독자

  • "RSpec라고 왠지 무서워" 라고 생각하는 RSpec 초보자
  • RSpec을 여러 번 사용해 보았지만, 내용을 몰라요"라고 생각하는 RSpec 경험자
  • Ruby 관계의 프로젝트에 참가해 상사에게 "RSpec 테스트를 써"라고 들어 곤란해하고 있는 다른 테스트 프레임워크 경험자

📋 RSpec & Ruby 버젼

  • RSpec 3.1.0
  • Ruby 2.1.3

Rspec에 대해서는 아래의 사이트를 참고해주세요
(향후 이 사이트에 대해서도 번역 예정입니다)
https://qiita.com/yusabana/items/db44b81bdddf6ed0e9f5

Rails에서 RSpec3 환경을 구축하는 방법
2019.7.22을 기점으로 Junichi Ito 씨가 Youtube를 만든 것 같습니다! 일본어 버젼이지만 일본어가 가능하신 분은 아래의 사이트를 들어가셔서 설명을 듣는 것도 좋을 것 같습니다.
[2019 년판] Rails 아니 Rspec3 환경을 구축하는 방법 - YouTube

📝 describe / it /sepect의 역할의 이해

가장 단순한 RSpec 테스트는 아래와 같이 된다.

RSpec.describe '사칙연산' do
  it '1 + 1은 2가 된다' do
    expect(1 + 1).to eq 2
  end
end

📋 describe(RSpec.describe)는 테스트 그룹화를 선언합니다.
여기에서는 「사칙 연산에 대한 테스트를 작성 할꺼에요」 라고 선언하고 있습니다.
describe는 일본어로하면 "묘사하다", "말하다[서술하다]"라는 뜻이다.
describe는 일본어로하면 "~을한다", "~을 설명하는" "~을 설명하는"이라는 뜻입니다.

📋 it는 테스트를 example이라는 단위로 정리 역할을 합니다.
it do ... end중 expectation(기대 값과 실제 값의 비교)이 모든 통과하면 example은 통과 한 것으로 됩니다.

📋 expect(X).to eq Y로 기술하는 것이 expectation입니다.
expectation에 expect는 "기대하다"라는 의미가 있기 때문에 expect(X).to eq Y는 "X가 Y와 같다는 것을 기대하다"라고 읽을 수 있습니다.
따라서 expect(1 + 1).to eq 2 1 + 1이 2가되는 것을 기대하다 라는 테스트입니다.

❗❓ to와 eq부분은 matcher라고 불리는 기능이지만, 이번 기사에서는 깊이 설명하지 않고 넘어가기 때문에 지금은 기대 값과 실제 값이 같은지 확인하고 있구나~ 라는 것만 이해할 수 있으면 괜찮습니다.

📜 참고 : should가 expect에 갱신된 이유에 대해서

RSpec 2.10 이전은 should를 사용해 다음과 같이 expectation을 쓰고 있었다.

(1 + 2).should eq 3
또는
(1 + 2).should == 3

should가 아니라 exepct가 사용되어지게 된 이유는 should라면 드물게 불일치가 발생한다는것을 알게되었기 때문이다.
should는 메타 프로그래밍(흑마술)을 사용하고 있기 때문에 문제가 발생하곤했다.
더 자세한 이유를 알고 싶은 분께서는 아래와 같은 사이트를 참고해주세요
RSpec's New Expectation Syntax (영어)

📝 복수의 example

describe 중에는 복수의 example (it do ... end)를 쓸수 있다.

RSpec.describe '사칙연산' do
  it '1 + 1 은 2 가 되는 것(or 1 + 1은 2가 되는지 테스트)' do
    expect(1 + 1).to eq 2
  end
  it '10 - 1 은 9가 되는 것(or 10 -1은 9가 되는지 테스트' do
    expect(10 - 1).to eq 9
  end
end

적절히 그룹화 하면 그 describe 블록은 이 기능을 테스트하고 있구나 라고 테스트 코드를 읽는 사람이 이해하기 쉬워진다(다른 사람이 읽기 좋은 코드는 최고의 코드!)

📝 복수의 expectation

1개의 example 중에 복수의 expectation을 쓰는 것도 가능하다.

RSpec.describe '사칙연산' do
  it '모두 테스트 가능하다' do
    expect(1 + 2).to eq 3
    expect(10 - 1).to eq 9
    expect(4 * 8).to eq 32
    expect(40 / 5).to eq 8
  end
end

단지 이렇게 코드를 쓰면 도중에 테스트가 실패했을 때에 몇번 째 줄에 expectation이 패스 했는지 안했는지 예측할 수 없다.
때문에 원칙으로서는 1개의 example에 한개의 expectation을 쓰는 것이 테스트의 보수성이 좋아진다.(물론 원칙이기 때문에 필요하다면 복수로 써도 괜찮다)

📝 중첩된(nest) describe

describe는 몇개라도 쓸수있고 중첩시키는 것도 가능하다.
기본적인 역할은 네스트의 그룹화이기 때문에 다음과 같이 그룹화 하는 것도 가능하다.

RSpec.describe '사칙연산' do
  describe '덧셈' do
    it '1 + 1 은 2가 되는지 테스트' do
      expect(1 + 1).to eq 2
    end
  end
  
  describe '뺄셈' do
    it '10 - 1 은 9가 되는지 테스트' do
      expect(10 - 1).to eq 9
    end
  end
end

적절히 그룹화 하면 「describe 블록은 이 기능을 테스트 하고 있구나」라는 것을 테스트 코드를 읽는 사람이 이해하기 쉬워진다.
📜 가장 위쪽의 describe 이외는 RSpec.을 생략가능하다.

📝 context 와 before으로 좀더 편리하게

좀 더 실천적인 예를 사용하여 contextbefore의 사용방법을 봐보자

class User
  def initialize(name:, age:)
    @name = name
    @age = age
  end
  def greet
    if @age <= 12
      "저는 #{@name}입니다."
    else
      "저는#{@name}입니다."
    end
  end
end

위의 코드에 대해 아래가 테스트 코드이다.

RSpec.describe User do
  describe '#greet' do
    it '12살 이하의 경우 히라가나로 대답하는지 테스트' do
      user = User.new(name: 'ジンス(진수)', age: 12)
      expect(user.greet).to eq '저는ジンス입니다.'
    end
    
    it '13살 이상의 경우 영어로 、한자로 대답하는지 테스트' do
      user = User.new(name: '辰樹(진수)', age: 13)
      expect(user.greet).to eq '저는辰樹입니다'
    end
  end
end

위의 코드라도 문제없이 작동하지만 조금더 RSpec을 사용하여 테스트를 리팩터링 해보자

📜 참고 : 처음의 한발과 좀 다른 점
describe에는 describe User 처럼 문자열이 아닌 클래스로 전달하는 것도 가능하다.
또, 인스턴스 메소드의 greet 메소드를 테스트 해보자라는 의미로 describe '#greet' 처럼 쓰는 것도 자주 있다.

📝 context에 조건별로 그룹화 한다

✏️ RSpec에서는 describe이외에도 context라고 하는 기능으로 테스트를 그룹화하는 것도 가능하다

  • describe와 context는 기능적으로 같지만, context는 조건을 나누거나 할 때 쓰는것이 많다
  • context의 의미는 문맥, 상황의 의미이다.

여기서는 12살 이하의 경우와 13살 이상의 경우라는 2개의 조건 그룹으로 나눠보았습니다.

RSpec.describe User do
  describe '#greet' do
    context '12살 이하의 경우' do
      it '히라가나로 대답하는지 테스트' do
        user = User.new(name: 'ジンス(진수)', age: 12)
        expect(user.greet).to eq '저는ジンス입니다.'
      end
    end
    
    context '13살 이상의 경우' do
      it '한자로 대답하는지 테스트' do
        user = User.new(name: '辰樹(진수)', age: 13)
        expect(user.greet).to eq '저는辰樹입니다'
      end
    end
  end
end

describe와 같이 context에 적절히 그룹화 하면
"이 context 블록은 이런 조건의 경우를 테스트하는 구나!"
라고 테스트 코드를 읽는 사람이 이해하기 쉽다

📝 before을 사용하여 공통적으로 사전준비를 한다

  • ✏️before do ... end에 감싸져 있는 부분은 example의 실행전에 매번 불려집니다.
  • ✏️before 블록안에서는 테스트를 실행하기 전에 공통처리나 데이터 셋업 등을 하는 대 많이 활용한다.

아래의 코드는 억지스럽지만 샘플 코드로서 참고 합시다.
아래의 코드에서는 name: 'Jinsu'가 중복되어지고 있기 때문에 DRY해보자

📜 Dry : Don't Repeat Yourself  | 똑같은 일을 두번 반복하지 않는다.
RSpec.describe User do
  describe '#greet' do
    before do
      @params = { name: 'Jinsu' }
    end
    
    context '12살 이하의 경우' do
      it '히라가나로 대답하는지 테스트' do
        user = User.new(@params.mergeage: 12))
        expect(user.greet).to eq '저는ジンス입니다.'
      end
    end
    
    context '13살 이상의 경우' do
      it '한자로 대답하는지 테스트' do
        user = User.new(@params.mergeage: 13))
        expect(user.greet).to eq '저는辰樹입니다'
      end
    end
  end
end

위의 예를 보면 이해가 되는 것처럼 로컬 변수가 아닌 인스턴스 변수에 데이터를 셋업하고 있다.
이것은 before 블록과 it 블록안에 변수의 scope가 다르기 때문입니다.

여기서 나오는 before대신 앞으로 나올 let이라는 기능을 쓰도록 노력하자!

📝 중첩된(Nested) describe나 context 중에 before 사용방법

✏️ before은 describe나 context 별로 준비하는 것이 가능하다
describe나 context가 중첩되어 있는 경우는 부자 관계에 대응해 before이 순서대로 불려진다

위의 코드의 예를 바꿔보자


RSpec.describe User do
  describe '#greet' do
    before do
      @params = { name: 'Jinsu' }
    end
    
    context '12살 이하의 경우' do
      before do
        @params.merge!(age: 12)
      end
      
      it '한글로 대답하는지 테스트' do
        user = User.new(@params)
        expect(user.greet).to eq '저는 Jinsu 입니다.'
      end
    end
    
    context '13살 이하의 경우' do
      before do
        @params.merge!(age: 13)
      end
      it '영어로 대답하는지 테스트' do
        user = User.new(@params)
        expect(user.greet).to eq 'My name is Jinsu'
      end
    end
  end
end

혹시 모르니 before이 각각에 어떻게 불려지는지 확인해 주세요!

위의 예의 경우 @params = { name: 'Jinsu' }의 부분은 12살 이하의 경우 에도 13살 이상의 경우 에도 불려진다.

한편 @params.merge!(age: 12)@params.merge!(age: 13)은 각각 12살 이하의 경우와 13살 이상의 경우 만 불려진다.(불려지는 순서는 물론 부모, 자식 순이다)


🔆 여기 까지가 기본 RSpec에 관한 기본적인 내용입니다! 🔆
더 자세하게 배우고 싶은 분은 밑에 응용 부분을 계속해서 읽어주세요!


응용(좀도 고도한 테크닉)

📝 인스턴스 변수 대신에 let을 사용하자

지금 까지 설명했던 코드에서는 인스턴스변수의 @params를 사용해왔었다.
그러나 RSpec에는 이 인스턴스 변수를 let이라는 기능으로 바꾸는 것이 가능하다.

실제로 let으로 바꾸어 코드를 봐보자

<RSpec.describe User do
  describe '#greet' do
    let(:params) { { name: 'Jinsu' } }
    
    context '12살 이하의 경우' do
      before do
        params.merge!(age: 12)
      end
      it '한글로 대답하는지 테스트' do
        user = User.new(params)
        expect(user.greet).to eq '저는 Jinsu 입니다'
      end
    end
    
    context '13살 이상의 경우' do
      before do
        params.merge!(age: 13)
      end
      it '영어로 대답하는지 테스트' do
        user = User.new(params)
        expect(user.greet).to eq 'My name is Jinsu'
      end
    end
  end
end

✏️ let(:foo) { ... } 처럼 코드를 작성하면 { ... }중의 값이 foo로서 참조 가능다는 것이 let의 기본적인 사용 방법이다.

단지 위의 예에서는 { { name: 'Jinsu'} }{ }가 2번이나 나오기 때문에 복잡하게 되어져있다.
바깥쪽의 { }은 Ruby의 블록으로 안쪽의 { }은 해쉬 리터럴 이다.

이해하기 어려운 분은 아래의 예처럼 써보면 이해하기 쉬울수도 있겠네요!

# let(:params) { { name: 'Jinsu' } } 와 같은 의미의 코드
let(:params) do
  hash = {}
  hash[:name] = 'Jinsu'
  hash
end

참고로 여러분 중에는 인스턴스변수 대신에 let을 쓰면 어떤 메리트가 있나? 라고 생각하는 사람이 많을지도 모르겠네요. 아래의 글을 읽어나가면서 let을 사용하면 좋은점에 대해서 설명하겠습니다.

📝 user를 let으로 하자

인스턴스 변수가 아닌 지역변수(Local Valiable)을 let으로 바꾸는것도 좋습니다.

userlet으로 바꿔봅시다.

RSpec.describe User do
  describe '#greet' do
    let(:user) { User.new(params) }
    let(:params) { { name: 'Jinsu' } }
    
    context '12살 이하의 경우' do
      before do
        params.merge!(age: 12)
      end
      it '한글로 대답하는지 테스트' do
        expect(user.greet).to eq '저는 Jinsu 입니다'
      end
    end
    
    context '13살 이상의 경우' do
      before do
        params.merge!(age: 13)
      end
      it '영어로 대답하는지 테스트' do
        expect(user.greet).to eq 'My name is Jinsu'
      end
    end
  end
end

중복하고 있던 user = User.new(params) 부분을 공통화 시켰습니다✨✨

📝 let의 메리트를 활용하여 age도 let으로 바꿔놓자

위의 코드에서는 before 블록안에 params.merge!(age: 12)같은 코드를 쓰는 것은 별로 좋지 않다(보기 좋지 않다 or cool하지 않다)

이왕이면, before 부분도 let으로 바꿔서 코드를 세련되고 깔끔하게 수정해봅시다.

RSpec.describe User do
  describe '#greet' do
    let(:user) { User.new(params) }
    let(:params) { { name: 'Jinsu', age: age } }
    
    context '12살 이하의 경우' do
      let(:age) { 12 }
      it '한글로 대답하는지 테스트' do
        expect(user.greet).to eq '저는 Jinsu 입니다.'
      end
    end
    
    context '13살 이상의 경우' do
      let(:age) { 13 }
      it '영어로 대답하는지 테스트' do
        expect(user.greet).to eq 'My name is Jinsu'
      end
    end
  end
end

자, 여기서 방금 사용해봤던 let의 메리트가 나타나 있습니다.

letbefore + 인스턴스 변수를 사용할 때와 다르게 느긋한 계산법이 되어집니다.
즉, let은 필요한 순간까지 호출되지 않는다!

📜 느긋한 계산법(wikipidia)

✏️컴퓨터 프로그래밍에서 느긋한 계산법은 계산의 결과값이 필요할 때까지 계산을 늦추는 기법이다.

위의 코드를 예를 들면 아래와 같은 순서로 호출 된다.

  1. expect(user.greet).to가 불려진다 => user는 뭐야?
  2. let(:user) { User.new(params) }가 불려진다. => params는 뭐야?
  3. let(:params) { { name: 'Jinsu', age: age } }가 불려진다. => age는 뭐야?
  4. let(:age) { 12 }(또는 13) 가 불려진다
  5. 결과로서 expect(User.new(name: 'Jinsu', age: 12).greet).to를 호출한 것이 된다.

이것을 before + 인스턴스 변수로 표현하려고 하면 꽤 귀찮게 작업해야 할 것이라고 생각한다.

그렇기 때문에 let의 느긋한 계산법되어진다는 특징을 잘 활용하면 효율성 높게 테스트 코드를 쓰는것이 가능하다.

2015.5.11 추가(향후 한국어 번역 예정)

let의 이점을 RSpec의 개발자가 설명한 기사가 있어 Ito씨가 번역하였다고 한다.
일본어가 가능하신분은 참고로 해주세요

📬 RSpec의 let을 사용하는것은 언제인가?(일본어 번역)

📝 subject를 사용하여 테스트 대상의 오브젝트를 1곳에 정리

테스트 대상의 오브젝트(또는 메소드의 실행 결과)가 명확히 1개로 정해져 있는 경우는 subject 라는 기능을 사용하여 테스트 코드를 DRY하게 작성할 수 있다.

예를 들어 이전의 코드의 예에서는 어느쪽에 example도 user.greet의 실행결과를 테스트하고 있습니다. 거기서 user.greet를 subject에 끌어올려, example 중에서 삭제해봅시다.

RSpec.describe User do
  describe '#greet' do
    let(:user) { User.new(params) }
    let(:params) { { name: 'Jinsu', age: age } }
    subject { user.greet }
    context '12살 이하의 경우' do
      let(:age) { 12 }
      it '한글로 대답하는지 테스트' do
        is_expected.to eq '저는 Jinsu 입니다.'
      end
    end
    context '13살 이상의 경우' do
      let(:age) { 13 }
      it '영어로 대답하는지 테스트' do
        is_expected.to eq 'My name is Jinsu'
      end
    end
  end
end

subject { user.greet }를 선언했기 때문에 지금까지 expect(user.greet).to eq '저는 Jinsu 입니다.'로 쓴 부분이 is_expected.to eq '저는 Jinsu 입니다' 바뀌었습니다.

더해서 it에 전달하는 문자열 한글로 대답하는지 테스트를 생략해보자

RSpec.describe User do
  describe '#greet' do
    let(:user) { User.new(params) }
    let(:params) { { name: 'Jinsu', age: age } }
    
    subject { user.greet }
    
    context '12살 이하의 경우' do
      let(:age) { 12 }
      it { is_expected.to eq '저는 Jinsu 입니다.' }
    end
    
    context '13살 이상의 경우' do
      let(:age) { 13 }
      it { is_expected.to eq 'My name is Jinsu' }
    end
  end
end

it { is_expected.to eq '저는 Jinsu 입니다'는 "it is expected to eq '저는 Jinsu 입니다'"와 자연스러운 영어스럽게 읽는것이 가능하다.

참고로 subject는 한국어로는주어주제이라는 의미이다.(+ 대상)
이런 방법이 가능하지는않지만 user.greet { is_expected.to eq '저는 Jinsu 입니다.'라고 생각하면 「subject = 테스트의 주어」라고 번역하는 것도 가능 한 것 같습니다.

📝 리팩터링하여 테스트 코드를 완성!

여기까지는 params user을 나누었지만 나누는 메리트도 없기 때문에 params의 내용을 인라인화해버립시다.

RSpec.describe User do
  describe '#greet' do
    let(:user) { User.new(name: 'Jinsu', age: age) }
    subject { user.greet }
    
    context '12歳以下の場合' do
      let(:age) { 12 }
      it { is_expected.to eq '저는 Jinsu입니다.' }
    end
    context '13歳以上の場合' do
      let(:age) { 13 }
      it { is_expected.to eq 'My name is Jinsu' }
    end
  end
end

이걸로 일단 테스트는 완성입니다.

💥 주의 : 기교적인 테스트 코드는 피하자

여기까지 여러가지 테크닉을 소개해왔지만 무리하게 let이나 subject를 이용할 필요는 없습니다.

완전히 DRY한 테스트 코드를 목표로 하여 이런 기능들을 이용하면 과도히 기교적으로 되어버리기 때문에 오히려 테스트 코드가 읽기 어려워지기될 우려가 있습니다.
때문에 테스트 코드는 DRY 보다도 읽기 쉬운 코드를 중요하게 생각하여주세요.
어플리케이션쪽의 코드와는 다르게 약간의 중복은 허용하도록 합시다.

이러한 생각(개념)은 앞으로 소개할 shared_examplesshared_context라고 하는 테크닉에 관해서도 마찬가지입니다!

📝 it나 context에 전달하는 문자열은 영어로 쓸까? 일본어로 쓸까?

지금까지 일본인을 대상으로 일본어를 써왔지만 RSpec은 itcontext에 전달하는 문자열을 영어로 쓰면 영어테스트 도큐먼트 처럼 보인다.

예를들어 아래의 예시 같은 느낌입니다.

RSpec.describe User do
  describe '#greet' do
    let(:user) { User.new(params) }
    let(:params) { { name: 'Jinsu', age: age } }
    
    context 'when 12 years old or younger' do
      let(:age) { 12 }
      it 'greets in Hangeul' do
        expect(user.greet).to eq '저는 Jinsu입니다.'
      end
    end
    
    context 'when 13 years old or older' do
      let(:age) { 13 }
      it 'greets in English' do
        expect(user.greet).to eq '僕はたろうです。'
      end
    end
  end
end

context에는 when~ (~일 때) 나 with(가 있는 경우), without~(~가 없는 경우)라고 하는 설명의 문자열을 전달하면 조건별로 그룹화하고 있는 것이 명확해진다.

it의 뒤에는 테스트 하려고하는 메소드의 동작을 표현하는 동사를 연결하여 사양을 영문화해주세요.

위에 설명 했던(영문화) 하는 것이 된다면 이상적이지만 실제문제, 생각한 대로 영문을 파다다닥 해서 쓰는 것은 어렵다고 생각한다.

때문에 일본인(한국인) 밖에 읽지 못하는 테스트 코드 라면 일본어(or 한국어)로 쓰는것도 문제없다가 나는 생각하고 있습니다.

딱 맞는(적절한) 영문을 생각하느라 시간을 잡아먹는다면 그 시간에 테스트 코드를 쓰는 것이 시간을 유용하게 이용하는 것이 더 좋다고 생각한다.

게다가 열심히 영문으로 쓰려고해도 문법을 엉망진창 작성해 버려 다른 사람이 이해하지 못하는 리스크가 나올지도 ...

때문에 영어가 힘든 사람은 무리하지 말고 일본어(한국어)로 작성해버립시다.

📝 it의 ailas = example와 specify

여기 까지는 it 'xxx'의 형식에 테스트를 썼지만 RSpec에는 it 이외에 완전 똑같은 역할의 examplespecify가 있다.

이 3개의 메소드는 Alias의 관계가 있기 때문에 이하의 테스트 코드는 내부적으로는 같은 의미입니다.

it '1 + 1 은 2가 되는지 테스트' do
  expect(1 + 1).to eq 2
end

specify '1 + 1 은 2가 되는지 테스트' do
  expect(1 + 1).to eq 2
end

example '1 + 1 은 2가 되는지 테스트' do
  expect(1 + 1).to eq 2
end

왜 3종류나 같은 메소드가 있는가 하면 자연스러운 영문을 만들기 위해서 입니다.

# 이건(it)은 유저명을 반환한다.
it 'returns user name' do
  # ...
end

# 회사는 사원을 갖는 것을 사양으로서 명기(똑똑히씀 = specify) 한다
specify 'Company has employees' do
  # ...
end

# fizz_buzz 메소드의 실행 예(example)
example '#fizz_buzz' do
  # ...
end

단지 이 기사에는 영어가 아니라 일본어를 사용해가고 있기 때문에, 특별히 나누지 않고 it를 사용해 나가겠습니다.

it 1 + 1dms 2가 된다 보다도 specify '1 + 1은 2가 된다'example '1 + 1은 2가 된다.' 이쪽이 약간 자연스러운 것 같기도?...ㅎㅎ

📝 Rspec의고도한 기능

letsubject 까지 잘 다룰 수 있다면 충분히 초급 레벨을 클리아 하고 있다고 생각합니다. 다만 그것에 더해서 이런 테크닉도 알고 있다면 상황에 따라서 도움이 될지도 모릅니다

📋 example의 재이용: shared_examples와 it_behaves_like

위에 만들었던 테스트 코드에 조금 더 테스트 패턴을 더해봅시다.
12살과13살 뿐만이 아닌 더 어린 아이도(0살) 이나 좀더 큰 어른 (100살)에도 인사를 해줍시다.

RSpec.describe User do
  describe '#greet' do
    let(:user) { User.new(name: 'Jinsu', age: age) }
    subject { user.greet }

    context '0살의 경우' do
      let(:age) { 0 }
      it { is_expected.to eq '저는 Jinsu 에요.' }
    end
    context '12살의 경우' do
      let(:age) { 12 }
      it { is_expected.to eq '저는 Jinsu 에요.' }
    end

    context '13살의 경우' do
      let(:age) { 13 }
      it { is_expected.to eq '저는 Jinsu 입니다.' }
    end
    context '100살의 경우' do
      let(:age) { 100 }
      it { is_expected.to eq '저는 Jinsu 입니다.' }
    end
  end
end

코드를 자세히 본다면 알거라고 생각합니다만, 같은 example을 2회씩 등록하고 있습니다.
이 경우는 shared_examplesit_behaves_like라고 하는 기능을 사용하면 example을 재이용할 수가 있습니다.

RSpec.describe User do
  describe '#greet' do
    let(:user) { User.new(name: 'Jinsu', age: age) }
    subject { user.greet }

    shared_examples '아이의 인사' do
      it { is_expected.to eq '저는 Jinsu 에요' }
    end
    context '0살의 경우' do
      let(:age) { 0 }
      it_behaves_like '아이의 인사'
    end
    context '12살의 경우' do
      let(:age) { 12 }
      it_behaves_like '아이의 인사'
    end

    shared_examples '어른의 인사' do
      it { is_expected.to eq '저는 Jinsu 입니다.' }
    end
    context '13살의 경우' do
      let(:age) { 13 }
      it_behaves_like '어른의 인사'
    end
    context '100살의 경우' do
      let(:age) { 100 }
      it_behaves_like '어른의 인사'
    end
  end
end

shared_examples 'foo' do ... end 를 재이용하고싶은 example을 정의하고 it_behaves_like 'foo'에 정의한 example을 불러오는 이미지 입니다.

참고로 shared_examplesit_behaves_like는 일본어로 하면 각각 「공유 되어지고 있는 example」과 「~처럼 동작(행동)하는 것」와 같이 번역할 수 있습니다.

📋 context의 재이용: shared_context와 include_context

User 클래스에 새로운 메소드 child?를 추가해 봅시다.

class User
  
  def initialize(name:, age:)
    @name = name
    @age = age
  end
  
  def greet
    if child?
      "저는 #{@name} 에요。"
    else
      "저는 #{@name}입니다"
    end
  end
  
  def child?
    @age <= 12
  end
end

모처럼 이기 때문에 greet메소드 만이 아닌 child?메소드도 테스트해 둡시다.

RSpec.describe User do
  describe '#greet' do
    let(:user) { User.new(name: 'Jinsu', age: age) }
    subject { user.greet }
    
    context '12살의 경우' do
      let(:age) { 12 }
      it { is_expected.to eq '나는 Jinsu 에요' }
    end
    
    context '13살의 경우' do
      let(:age) { 13 }
      it { is_expected.to eq '저는 Jinsu 입니다' }
    end
  end

  describe '#child?' do
    let(:user) { User.new(name: 'Jinsu', age: age) }
    subject { user.child? }
    
    context '12살의 경우' do
      let(:age) { 12 }
      it { is_expected.to eq true }
    end
    
    context '13살의 경우' do
      let(:age) { 13 }
      it { is_expected.to eq false }
    end
  end
end

테스트 코드를 써보면 어느쪽의 테스트도 12살이하의 경우와 13살의 이상의 경우에 같은 context가 등록되어져 있습니다.

이런 경우에 shared_contextinclude_context를 사용하면 context를 재이용할 수 있습니다.

RSpec.describe User do
  let(:user) { User.new(name: 'Jinsu', age: age) }
  
  # 여기 부분에 주목해 주세요!!
  shared_context '12살의경우' do
    let(:age) { 12 }
  end
  shared_context '13살의경우' do
    let(:age) { 13 }
  end
  # 여기 까지 추가 되었습니다.

  describe '#greet' do
    subject { user.greet }
    context '12살 이하의 경우' do
      include_context '12살의경우'
      it { is_expected.to eq '나는 Jinsu 에요' }
    end
    context '13살 이하의 경우' do
      include_context '13살의경우'
      it { is_expected.to eq '저는 Jinsu 입니다' }
    end
  end

  describe '#child?' do
    subject { user.child? }
    context '12살 이하의 경우' do
      include_context '12살의 경우'
      it { is_expected.to eq true }
    end
    context '13살 이하의 경우' do
      include_context '13살의 경우'
      it { is_expected.to eq false }
    end
  end
end

shared_examples의 때와 같이 shared_context 'foo' do ... end에 재이용하고 싶은 context를 정의 하여 include_context 'foo'에 정의한 context를 불러오는 이미지 이다.

📋 느긋한 계산법의 let과 이전에 실행되어진 let!

let를 잘 사용하게 되면 편리한 기능이지만 느긋한 계산법이라는 특징이 구멍이 되어 이해하기 어려운 테스트의 실패를 부르는 경우도 있습니다.

예를 들어 Rails에 다음과 같은 Blog 모델의 테스트를 쓰면 테스트가 실패합니다.

RSpec.describe Blog do
  let(:blog) { Blog.create(title: 'RSpec필승법', content: '나중에 쓴다') }
  it '블로그 취득이 가능한지 테스트' do
    expect(Blog.first).to eq blog
  end
end

이 테스트가 실패하는 이유를 아시나요???

expect(Blog.first).to eq blog의 부분에 주목하여 주세요.
Blog.first를 불러온 시점에는 아직 let(:blog)가 실행되어져 있지 않기 때문에, 레코드가 데이터베이스에 보존되어 있지 않습니다.

때문에 Blog.firstnil를 반환해버려 테스트가 실패하게된다.
(TMI지만 저도 테스트 할 때 이 글을 읽지 않아서 몇시간 동안 원인을 찾았던 기억이 있네요...)

nilblog를 비교하려고하는 순간에 레코드가 데이터베이스에 보전되어집니다.

이 문제를 회피하기 위한 방법중 하나는 before블록안에 명시적인 let(:blog)를 불러오는 것입니다.

명시적으로 불러오는 것으로 example의 실행전에 레코드를 데이터베이스에 보존하는 것이 가능합니다.

RSpec.describe Blog do
  let(:blog) { Blog.create(title: 'RSpec필승법', content: '나중에 작성') }
  before do
    blog # 여기에 데이터베이스에 레코드를 보존한다
  end
  
  it '블로그가 취득가능한지 테스트' do
    expect(Blog.first).to eq blog
  end
end

그러나 이것을 하지않아도 before가 같은 동작을 하는 let!이 있습니다.
let!을 사용하면 example의 실행전에 let!에 정의된 값이 만들어지도록 됩니다.

RSpec.describe Blog do
  let!(:blog) { Blog.create(title: 'RSpec필승법', content: '나중에 작성') }
  
  it '블로그가 취득가능한지 테스트' do
    expect(Blog.first).to eq blog
  end
end

때문에 let의 느긋한 계산법이 테스트 실패의 원인이 되는 경우 대신에 let!을 사용하면 편리하다
(정말 꿀팁이네요!)

📋 어떻게 해도 동작하지 않는 테스트의 보존마크를 붙인다 :pending

당연히! 패스해야할것이 왜인지 어떻게~ 해도 테스트가 패스(성공) 하지 않아... 이럴 때는 pending에 보류마크를 붙여 놉시다.

RSpec.describe '상세한 클래스' do
  it '상세한 테스트' do
    expect(1 + 2).to eq 3

    pending '앞에는 왜인지 테스트가 실패하기 때문에 나중에 고친다'
    # 패스하지 않는 expectation(실행되어짐)
    expect(foo).to eq bar
  end
end

pending의 특이한 점은, 거기서 실행을 중단하는 것이 아니라 그대로 실행을 이어간다는 것입니다.
그렇게 테스트가 실패하면 성공이든 실패도 아닌 「pending(보류)」로서 마크되어진다.
혹시 마지막까지 테스트가 패스된 경우는 pending이 되지않고 「왜 패스 되는거지!!」라고 RSpec에게 혼나면서 테스트가 실패된다.

좀 삐뚫어진 수단(기능)이지만 「언젠가 보니 고쳤다」라고 하는 괴기현상을 피하는 것이 되기 때문에 의외로 편리한 기능일지도 모릅니다.

📋 문답무용에 테스트의 실행을 멈춘다 :skip

한편 정말로 거기서 테스트의 실행을 멈추고 싶은 경우에는 skip을 사용합시다.

RSpec.describe '무언가에 이유로 실행하고 싶지 않은 클래스' do
  it '실행하고 싶지 않은 테스트' do
    expect(1 + 2).to eq 3

    skip '우선 여기서 실행을 보류'
    # 여기부터는 실행되지 않는다.
    expect(foo).to eq bar
  end
end

pending과는 다르게 skip는 지정해논곳 부터 다음은 실행하지 않고 테스트를 pending으로서 마크해논다.
라고는 하지만 개인적으로 skip을 사용한 유스케이스가 별로 떠오르지는 않는다.
등장빈도는 pending보다도 계속해서 적지 않을까 생각합니다.(참고 해주세요)

📋 손쉽게 example 전체를 skip시키는 :xit

example 전체를 손쉽게 skip 하고 싶을 때는 itxit에 변경하면 그 example은 실행되어지지 않게 됩니다.(pending 처럼 취급됩니다)

RSpec.describe '무언가에 이유로 실행하기 싫은 클래스' do
  xit '실행하기 싫은 테스트' do
    expect(1 + 2).to eq 3

    expect(foo).to eq bar
  end
end

xit만이 아닌 xspecifyxexample도 동시에 example 전체를 skip한다.

📋그룹화 전체를 정리하여 skip한다 :xdescribe /xcontext

it뿐만이 아닌, describecontext에도 x를 붙이는 것이 가능하다

# 그룹 전체를 skip 한다
xdescribe '사칙연산' do
  it '1 + 1 은 2 가 되는지 테스트' do
    expect(1 + 1).to eq 2
  end
  it '10 - 1 은 9 가 되는지 테스트' do
    expect(10 - 1).to eq 9
  end
end

# 그룹 전체를 skip 한다
xcontext '관리자인 경우' do
  it '사원정보 편집 가능' do
    # ...
  end
  it '사원정보 삭제 가능' do
    # ...
  end
end

📋테스트는 나중에 쓴다: 중심이 없는 it

it 'something' do ... enddo ... end를 생략하면 그런 경우에도 pending의 테스트로서 마크 되어진다.

이것은 메소드를 구현할때에 테스트 패턴을 생각하면서 그 대로 도큐멘트화 (Rspec화) 가능하기 때문에 꽤 편리합니다.

예를 들어 「User 클래스에 good_bye 메소드를 추가하고 싶다~」라고 생각하는 경우는 구현에 들어가기에 앞서 이런식으로 RSpec상에 기능을 설계하는 것이 가능하다( 구현전에 ToDo 리스트라고 간과하는 것도 좋을지도 모릅니다)

RSpec.describe User do
  describe '#good_bye' do
    context '12살 이하의 경우' do
      it '평범히 잘가라고 인사하는지 테스트'
    end
    context '13歳以上の場合' do
      it '정중하게 잘가라고 인사하는지 테스트'
    end
  end
end

pending의 테스트가 없어졌을 때 good_bye메소드의 구현이 종료되었을 때입니다.

📜이것이 TDD?

구현전에 테스트를 쓰기 때문에 TDD(테스트 주도 개발) 엄밀히 이야기 하면 좀 다르다고 생각합니다.
우선 이것은 pending이기 때문에 레드(실패)가 되지는 않는다.(색으로 이야기 하면 노랑이네요)
또 TDD에는 구현보다도 테스트 코드(테스트가 중심)를 먼저 쓸 필요가 있지만, 여기에서는 어느쪽을 먼저 쓸지에 대해서는 특별히 정하고 있지 않습니다.
때문에, TDD 본래의 프로세스와는 좀 다릅니다.

참고로 저의 경우 TDD는 사용하고 싶을 때 쓴다 주의 입니다.
실무에 코드를 쓰는 경우는 이하의 3개의 패턴중 하나가 됩니다.

  • 먼저 구현을 하고 난뒤 테스트 코드를 작성한다.
  • 먼저 테스트 코드를 작성한 뒤 코드를 구현한다(TDD)
  • 구현 후에 수작업과 눈으로 확인한 뒤 테스트 코드는 작성하지 않는다.

테스트의 원리주의(기본 이념)(누가 뭐래도 TDD)를 선택하여 오히려 개발효율이 나쁘게 되는 경우도 많이 있기 때문에 비용 효율이 가장 높은 방법을 매번 선택하고 있는 느낌입니다.

📝정리

여기서에 기사에서는 RSpec의 기본적인 문법이나 자주 쓰이는 편리한 기능을 소개하였습니다.
초심자용의 내용에서 상급자용의 내용까지 안번에 설명했기 때문에 읽고 있는 여러분도 꽤 고생했을지도 모르겠습니다.
그러나 이만큼의 내용을 알아둔다면 RSpec의 상급자가 쓴 코드도 거의다 이해가 될죄 않을까 라고 생각합니다.

RSpec에는 많은 기능이 있고, 팟하고 봤을 때 복잡하고 이상한 문법도 많습니다.
그러나 적재적소에 (좀 이상하지만) 편리한 기능을 사용하여 과도히 기교적인 테스트 코드를 피할 수 있도록 한다면 RSpec의 맛있는 부분(좋은 부분)을 활용한 읽기쉽고 보수하기 쉬운 테스트 코드를 작성할 수 있을 것입니다.

앞으로 RSpec를 다루는 사람도, 몇번이고 사용해본적이 있는 사람도, 이번 기사를 참고로 하여 RSpec의 좋은 부분을 활용 가능하도록 된다면 매우 기쁠것입니다.

그 외 RSpec에 관련한 사이트 들은 아래와 같이 참고 사이트에 등록해놓았습니다.
시간이 되시는 분들은 계속해서 읽어주시면 많은 도움이 될거라고 생각합니다!


🔖 참고 사이트

RSpec의 공식 도큐멘트

📰 RSpec Core 3.1

아래의 내용은 아직 번역하지 않아 일본어로 되어있습니다. 일본어가 가능하신분은 일본어로 읽어보시고 계속해서 번역해나갈 것이니 기다려주세요.

📰 공개했습니다! => 사용할 RSpec 입문 2 "사용 빈도가 높은 녹차를 잘 다루는"
모의 사용(예정)

📰 공개했습니다! => 사용할 RSpec 입문 · 그 3 "처음부터 알 모의를 사용한 테스트 작성"
자주 사용하는 Capybara의 DSL(예정)

📰 공개했습니다! => 사용할 RSpec 입문 · 그 4 "어떤 브라우저 작업도 자유 자재! 역방향 Capybara 대사전"(예정)

profile
Ruby와 js로 첫 커리어를 시작하였고 4년차 엔진니어입니다! 현재 Rails, vim에 관심이 많습니다!

3개의 댓글

comment-user-thumbnail
2022년 3월 21일

좋은 글 감사합니다.

답글 달기
comment-user-thumbnail
2022년 3월 21일

오래된 글이긴 하지만... 혹시 다음 편들도 번역 생각이 있으실까요? 👀 RSpec 공부하는데 많은 도움이 되어서 ㅎㅎ 계속 번역해주싵다면 정말 감사할 것 같습니다.

1개의 답글