Ruby 애플리케이션에 RSpec을 활용해 테스트를 작성하는 방법을 배우고 싶으신가요?
그렇다면 정확히 잘 찾아오셨습니다!
이 튜토리얼에서는 RSpec으로 테스트를 작성하는 방법을 단계별로 자세히 알려드립니다.
테스트를 작성해야 하는 이유
테스트 코드가 중요한 이유는 다음과 같습니다.
오류에 대한 안전망을 구축합니다 (특히 리팩토링 시 유용)
테스트 스위트가 없다면 뭔가 깨질까 봐 두려워서 기존 코드를 건드리기조차 어렵습니다.
반면 테스트가 있으면 자신감 있게 코드를 수정할 수 있습니다.
코드 문서화 역할을 합니다
테스트는 애플리케이션이 무엇을 해야 하는지 설명해 주는 살아있는 문서입니다.
피드백 루프를 제공합니다
TDD(테스트 주도 개발)를 하면 다음에 무엇에 집중해야 할지 알려주는 피드백 루프를 얻게 됩니다. 집중력이 쉽게 흐트러지는 개발자에게 특히 유용합니다.
코드가 기대한 결과를 내는지 확인할 수 있습니다
이 부분이 특히 중요합니다!
복잡한 로직을 작성할 때는 직접 만든 하나의 예시뿐 아니라 다양한 입력값에서도 올바르게 동작하는지 확인해야 합니다.
테스트는 엣지 케이스(경계 사례)를 발견하고 이를 문서화하는 데 큰 도움이 됩니다.
Ruby 개발자 취업에 도움이 됩니다
대부분의 채용 과정에서 테스트 작성 능력은 큰 강점이 되며, 합격 가능성을 높여줍니다.
RSpec 시작하기
RSpec의 동작 방식을 이해하기 위해 단계별 예제를 함께 살펴보겠습니다.
팩토리얼(factorial) 값을 구하는 간단한 애플리케이션을 만들어 보겠습니다.
첫 번째 단계는 다음과 같습니다.
require 'rspec/autorun'
describe Factorial do
# ...
end
위 코드가 첫 RSpec 테스트를 작성하기 위한 시작점입니다.
먼저 rspec 젬(gem)을 require 해야 하고, 그다음 describe 블록을 만들어 모든 테스트를 그룹화하고 어떤 클래스를 테스트하는지 RSpec에 알려줍니다.
다음은 it 블록입니다.
describe Factorial do
it "does something" do
# ...
end
end
it 블록은 테스트의 이름이며, 테스트 자체의 모든 구성 요소를 묶는 역할도 합니다.
구성 요소는 다음 세 가지입니다.
- Setup (준비)
- Exercise (실행)
- Verify (검증)
Setup 단계에서는 필요한 객체들을 생성합니다. 즉, 준비 단계입니다.
그다음 테스트하려는 메서드를 호출해 반환값을 받습니다.
마지막으로 기대값(expectation, RSpec) 또는 단언문(assertion, Minitest)으로 결과를 검증합니다.
RSpec 테스트 예제
팩토리얼 메서드를 작성하려면 먼저 유효한 값들을 온라인 검색이나 손계산을 통해 확인해야 합니다.
그런 다음 그 값들을 테스트에 활용합니다.
예를 들어 다음과 같습니다.
describe Factorial do
it "finds the factorial of 5" do
calculator = Factorial.new
expect(calculator.factorial_of(5)).to eq(120)
end
end
이 코드를 일반 Ruby 프로그램처럼 실행하면 다음과 같은 오류가 발생합니다.
uninitialized constant Factorial (NameError)
아직 Factorial 클래스가 없기 때문에 당연한 결과입니다.
클래스를 만들어 보겠습니다.
class Factorial
end
이번에는 다음 오류가 나타납니다.
undefined method 'factorial_of'
factorial_of 메서드를 생성해서 해결해 보겠습니다.
class Factorial
def factorial_of
end
end
코드를 다시 실행하면:
wrong number of arguments (given 1, expected 0)
또 다른 오류네요! 그런데 이건 오히려 좋은 신호입니다. 🙂
오류는 짜증 낼 대상이 아니라 소중한 피드백입니다.
이제 factorial_of 메서드에 인자를 하나 추가합니다.
class Factorial
def factorial_of(n)
end
end
이번에는 테스트 실패(failure)가 발생합니다.
expected: 120
got: nil
바로 지금이 우리가 원하던 지점입니다!
다음 과제는 메서드를 실제로 구현하는 것입니다.
class Factorial
def factorial_of(n)
(1..n).inject(:*)
end
end
이제 드디어 첫 번째 통과하는 테스트를 만나게 됩니다.
.
Finished in 0.00315 seconds (files took 0.09083 seconds to load)
1 example, 0 failures
이것이 바로 TDD(테스트 주도 개발)입니다.
테스트를 먼저 작성하고, 테스트가 다음에 무엇을 해야 하는지 안내하도록 하는 방식입니다.
RSpec let 메서드
여러 테스트를 작성하면서 동일한 객체를 재사용하고 싶다면 let 구문으로 객체를 정의할 수 있습니다.
사용 방법은 다음과 같습니다.
describe Factorial do
let(:calculator) { Factorial.new }
it "finds the factorial of 5" do
expect(calculator.factorial_of(5)).to eq(120)
end
end
이제 같은 describe 블록 안의 모든 테스트에서 calculator를 재사용할 수 있습니다.
let에 대해 꼭 알아두어야 할 점은 "지연(lazy) 평가"된다는 것입니다.
무슨 의미일까요?
객체는 처음 사용되는 시점에 비로소 생성됩니다.
객체 생성에 데이터베이스 레코드 생성이나 파일 쓰기 같은 부수 효과(side-effect)가 있다면 이 차이가 중요해집니다.
가급적 이런 부수 효과는 피하는 것이 좋지만, 불가피하다면 let! 메서드를 사용하세요.
예시:
let!(:user) { User.create("rspec@rubyguides.com") }
let!은 지연 평가되지 않으므로, 객체가 테스트 실행 전에 미리 생성됩니다.
subject 메서드 활용법
let의 변형인 subject도 있습니다.
유일한 차이점은 subject는 하나만 정의할 수 있으며, 테스트 대상이 되는 핵심 객체의 인스턴스를 담는다는 점입니다.
사실 RSpec은 이미 다음과 같은 기본 subject를 자동으로 생성해 줍니다.
subject { Factorial.new }
이것을 "암묵적 subject(implicit subject)"라고 부릅니다.
다음과 같이 사용할 수 있습니다.
describe Factorial do
it "finds the factorial of 5" do
expect(subject.factorial_of(5)).to eq(120)
end
end
subject에 이름을 붙일 수도 있습니다.
subject(:calculator) { Factorial.new }
동작 방식은 let과 동일하지만, 한 줄짜리 기대식(one-line expectation)을 사용할 수 있게 해 줍니다.
it { should be_empty }
모든 테스트 전에 코드 실행하기
RSpec에는 모든 테스트 전후, 혹은 테스트 그룹 전후에 특정 코드를 실행할 수 있는 훅(hook)이 마련되어 있습니다.
예를 들어 다음과 같습니다.
describe Shop do
before(:all) { Shop.prepare_database }
after (:all) { Shop.cleanup_database }
end
각 예제(RSpec에서 example = 테스트)마다 실행하고 싶다면 :all 대신 :each를 사용하면 됩니다.
테스트 서브그룹 만들기
애플리케이션의 여러 시나리오를 테스트할 때는 관련된 테스트끼리 묶어두면 편리합니다.
RSpec에서는 context 블록을 사용해 이를 구현할 수 있습니다.
예시를 살펴보겠습니다.
describe Course do
context "when user is logged in" do
it "displays the course lessons" do
end
it "displays the course description" do
end
end
context "when user it NOT logged in" do
it "redirects to login page" do
end
it "it shows a message" do
end
end
end
테스트 임시 비활성화하기
디버깅 목적으로 특정 테스트를 임시로 비활성화할 수 있습니다.
방법은 간단합니다. 비활성화하려는 테스트의 it을 xit으로 바꾸기만 하면 됩니다.
예시:
xit "eats lots of bacon" do
end
작업이 끝나면 x를 제거하는 것을 잊지 마세요!
이름으로 테스트 필터링해서 실행하기
테스트를 비활성화하는 대신, -e 플래그로 실행하고 싶은 테스트만 골라서 실행할 수도 있습니다.
예시:
> ruby person.rb -e bacon
필터링은 테스트 이름을 기준으로 하므로, 위 예제는 이름에 "bacon"이라는 단어가 포함된 모든 테스트와 일치합니다.
RSpec 기대식(expectation)과 매처(matcher)
앞서 계속 사용했던 이 예제를 기억하시나요?
expect(calculator.factorial_of(5)).to eq(120)
그런데 eq(120) 부분은 무엇일까요?
120은 우리가 기대하는 값이고,
eq는 소위 '매처(matcher)'라고 부르는 것입니다.
매처는 RSpec이 메서드의 출력값과 기대값을 비교하는 방식입니다.
eq의 경우 RSpec은 == 연산자를 사용합니다.
하지만 그 외에도 다양한 매처를 사용할 수 있습니다.
예를 들어 be_something 매처가 있습니다.
expect(nil).to be_nil
여기서 something은 테스트 결과에 대해 호출될 술어(predicate) 메서드(예: empty?)입니다.
그 밖에 유용한 매처들:
- include (배열 및 해시용)
- start_with
- end_with
- match (정규표현식 매칭용)
- be_between
- have_key / have_value (해시용)
- be_instance_of
- respond_to
- have_attributes (인스턴스 변수 테스트용)
특별히 주의가 필요한 매처는 raise_error입니다.
이 매처를 사용하려면 기대식을 반드시 블록으로 감싸야 하기 때문입니다.
예를 들어 다음과 같습니다.
expect{ :x.count }.to raise_error(NoMethodError)
change 매처도 같은 방식으로 동작합니다.
expect{ stock.increment }.to change(stock, :value).by(100)
RSpec 포매터(formatter)
RSpec의 기본 출력 형식은 "progress" 포맷입니다.
이 형식에서는 통과한 테스트마다 점(.) 하나가 표시되고, 실패한 테스트(기대값과 실제값이 불일치)는 F, 오류는 E로 표시됩니다.
하지만 다른 포맷 옵션도 사용할 수 있습니다.
목록은 다음과 같습니다.
- progress
- documentation
- json
- html
-f 플래그로 활성화할 수 있습니다.
> ruby factorial.rb -f d
Person
eats lots of healthy food
writes many articles
Finished in 0.00154 seconds (files took 0.09898 seconds to load)
2 examples, 0 failures
documentation 포맷은 테스트 설명(description)을 활용해 출력을 생성합니다.
느린 테스트 찾기
RSpec에는 테스트 성능을 프로파일링할 수 있는 아주 유용한 옵션이 내장되어 있습니다.
--profile 플래그만 붙이면 각 테스트가 실행되는 데 걸리는 시간을 확인할 수 있고, 지나치게 느린 테스트를 찾아 개선할 수 있습니다.
예시:
> ruby factorial.rb --profile
Factorial finds the factorial of 5
0.00043 seconds
마치며
이번 글에서는 RSpec 테스트 프레임워크를 사용해 테스트를 작성하는 방법을 배웠습니다.
이제 직접 나만의 테스트를 작성해 볼 차례입니다!