루비 테스트 프레임워크 RSpec 2.14 매쳐(Matchers)
루비에서는 테스트를 하기 위해 minitest와 BDD 프레임워크인 RSpec이 많이 사용됩니다. 유닛 테스트에 친숙하신 분들은 minitest를 선호하지만, 좀 더 설명적인 테스트가 가능한 RSpec도 많이 사용되고 있습니다. 이 글에서는 RSpec 테스트에서 사용할 수 있는 빌트인 테스트 매쳐(Matcher)들을 간단히 소개합니다.
테스트 준비
일반적으로 테스트 파일은 프로그램 파일과 별개로 작성됩니다. 여기서는 편의상 pry 위에서 바로 테스트를 진행합니다. 따라서 이 글의 내용은 irb나 pry를 통해서 따라해볼 수 있습니다. 필요한 경우 먼저 rspec과 pry 패키지를 설치합니다
$ gem install rspec pry
다음으로 pry를 실행합니다
$ pry
[1] pry(main)>
다음으로 Matecher를 사용하기 위해 rspec과 RSpec::Matchers를 읽어들입니다.
require 'rspec'
include RSpec::Matchers
테스트 기초
테스트는 기본적으로 아래와 같은 형식으로 쓰여집니다
expect(actual).to be(expected)
여기서 actual에는 실제로 테스트하고자 하는 객체가 들어가며, expected에는 예상하는 값이 들어갑니다. 그리고 be는 actual과 expected를 비교하기 위한 매쳐를 지정합니다
또한 단언문과 마찬가지로 테스트가 실패하면 예외를 발생시킵니다. 즉, 리턴값이 true라고 해서 테스트가 성공을 의미하는 게 아니며, 거꾸로 false라고 해서 실패를 의미하는 게 아닙니다. 테스트가 실패하면 RSpec::Expectations::ExpectationNotMetError 예외가 발생합니다.
동일값 테스트 매쳐
가장 기본적이고 가장 많이 쓰이는 매쳐는 실제값과 예상되는 값이 같은 지를 비교하는 매쳐입니다
# Equivalence
expect("프로그래밍 언어 루비").to eq("프로그래밍 언어 루비")
expect("프로그래밍 언어 루비").to eql("프로그래밍 언어 루비")
expect("프로그래밍 언어 루비").to be == "프로그래밍 언어 루비"
동일성 테스트 매쳐
eq가 실제값과 예상값이 같은지를 평가하는 매쳐라면, equal은 비교하는 두 객체가 같은 객체인지를 테스트합니다. 루비에선 같은 값을 가진 심볼은 항상 같은 객체입니다. 하지만 같은 값을 가지고 있더라도 별개로 생성된 문자열은 다른 문자열입니다. 따라서 같은 값을 가진 심볼은 동일성 테스트를 통과하지만, 아래 문자열 테스트는 실패합니다
# Identity
expect(:Hello_world).to equal(:Hello_world)
expect(:Hello_world).to be(:Hello_world)
expect("Hello, World").to equal("Hello, World")
연산자 매쳐
동일값 테스트만큼 많이 사용되는 매쳐는 실제값과 예상값을 비교하는 연산자 매쳐입니다. 기본적으로 동일값을 비교하는 ==을 비롯해 >, >=, <=, < 등을 사용할 수 있습니다.
# Comparisons
expect(100 + 1).to be > 100
expect(100 - 1).to be < 100
match와 =~는 특정 문자열이 주어진 정규표현식에 매치되는 지 여부를 테스트합니다.
expect("나랏말 싸미 듕국과 달라").to match(/듕국/)
expect("나랏말 싸미 듕국과 달라").to be =~ /듕국/
숫자 범위 매쳐
be_within.of 매쳐는 실제값이 예상값 +- 특정 범위에 포함되는 지 여부를 테스트합니다.
expect(100).to be_within(5).of(100 + 3)
expect(100).to be_within(5).of(100 + 6)
타입 매쳐
어떤 객체가 어떤 클래스로부터 만들어졌는지 테스트할 수 있습니다.
# Types and classes
expect("Ruby").to be_instance_of(String)
expect("Ruby").to be_an_instance_of(String)
expect("Ruby").to be_a(String)
expect("Ruby").to be_kind_of(String)
참거짓 매쳐
어떤 객체가 참인지 거짓인지 테스트할 수 있습니다.
# Truthiness and existentialism
expect("").to be_true
expect(nil).to be_false
expect(true).to be
expect(7).to be
expect(nil).not_to be
expect(nil).to be_nil
변화 매쳐
change 매쳐는 expect에 주어진 블록이 실행되었을 때 change에 주어진 블록의 평가 결과가 변화하는 지를 테스트합니다. 변화했다면 테스트가 통과합니다. 필요에 따라서 얼마만큼 변화했는지를 테스트할 수 있는 by나 expect 블록을 실행시키기 전의 값을 테스트할 수 있는 from, 평가한 이후의 값을 테스트 할 수 있는 to 메소드와 함께 사용될 수 있습니다
arr = []
expect{arr << 1}.to change{arr.count}
expect{arr << 1}.to change{arr.count}.by(1)
str = "Hello, world."
expect{ str.upcase! }.to change{ str }.from("Hello, world.").to("HELLO, WORLD.")
예외 메쳐
expect에 주어진 블록의 평가 결과가 예외를 일으키는 지 테스트합니다. expect가 블록을 받는다는 데 주의가 필요합니다.
# Expecting errors
expect{ raise }.to raise_error
expect{ non_existance }.to raise_error
expect { non_existance }.to raise_error(NameError)
Have 매쳐
컬렉션 객체가 몇 개의 항목을 가지고 있는 지 테스트합니다.
arr = "Hello".split("")
expect(arr).to have(5).items
expect(arr).to have_exactly(5).items
expect(arr).to have_at_least(1).items
expect(arr).to have_at_most(10).items
have_key 매쳐
해시에 특정 키가 포함되어있는 지 테스트합니다.
expect({name: "nacyot", blog: "blog.nacyot.com"}).to have_key(:name)
포함 매쳐
객체에 특정한 값이나 키가 포함되어있는 지 테스트합니다.
expect([1, 2, 3]).to be_include(1)
expect({name: "nacyot", blog: "blog.nacyot.com"}).to be_include(:name)
expect("Hello").to be_include("H")
start_with / end_with 매쳐
객체의 처음이나 끝이 특정 항목으로 시작하거나 끝나는지 테스트합니다
expect("Hello, world").to start_with("Hello")
expect("Hello, world").to end_with("world")
expect([1, 2, 3, 4, 5]).to start_with(1, 2)
expect([1, 2, 3, 4, 5]).to end_with(5)
커버 매쳐
범위 객체가 특정 값을 포함하는 지 테스트합니다.
expect(100..1000).to cover(101)
expect(100..1000).not_to cover(99)
respond_to 매쳐
객체가 특정 메시지를 받을 수 있는 지 테스트 합니다.
expect("Hello").to respond_to(:upcase)
satisfy 매쳐
블록의 평가 결과가 참인지를 평가합니다. 이 때 expect에 주어진 값이 평가 대상이 되며 satisfy 블록의 인자로 넘겨집니다. 이 블록이 참이면 테스트가 통과합니다.
expect(false).to satisfy{|value| !value}
Yielding
메소드 내에서 yield하는 부분에 대해서 테스트합니다.
expect {|b| 5.tap(&b)}.to yield_control
expect { |b| 5.tap(&b) }.to yield_with_args(5)
expect { |b| 5.tap(&b) }.to yield_with_args(Fixnum)
expect { |b| "a string".tap(&b) }.to yield_with_args(/str/)
expect { |b| [1, 2, 3].each(&b) }.to yield_successive_args(1, 2, 3)
expect { |b| { :a => 1, :b => 2 }.each(&b) }.to yield_successive_args([:a, 1], [:b, 2])
Predicate matchers
참거짓을 리턴하는 메소드를 사용해 테스트합니다. 이 때 매쳐 이름은 be_메소드이름이 됩니다.
expect(7).not_to be_zero # 7.zero?
결론
여기까지 소개한 매쳐들이 RSpec 빌트인 매쳐로 테스트하는데 가장 많이 사용됩니다. 조금 기교스러운 매쳐들도 있어서 익숙해지는데 시간이 걸릴지도 모르겠습니다만, 기본적인 원리만 이해하고 빌트인 매쳐들만 활용해도 대부분의 테스트를 하는 데는 크게 무리가 없습니다. 좀 더 자세한 사항은 Relish의 Rspec 문서에서 확인하실 수 있습니다. 또한 아직 베타이지만 조만간 업데이트 예정인 메이저 버전 3에서 추가되는 매쳐도 확인할 필요가 있습니다. 역시 공식문서에서 확인할 수 있습니다.