Shim Won

January 27, 2015 1:45 am

번역 Minitest, RSpec 경험담

지난 6개월 동안 RSpec을 진지하게 사용했었습니다. 그래서 이제 RSpec 과 Minitest를 비교하는 허세 글(blurrrgh poast)을 쓸 때인 것 같습니다. 저는 Minitest를 지난 몇 년간 사용했고, RSpec은 6개월밖에 안 썼습니다. 읽을 때 염두에 넣어 주세요.

기억해 두세요, 당신이 테스트하고 있다면 어떤 테스트 프레임워크를 쓰던 상관 없습니다. 이 글은 두 프레임워크에 대한 저의 경험담입니다. 다시 말해서 이건 그냥 의견입니다.

시작하기 전에!!!

저는 정말로 모든 테스트 프레임워크는 기본적으로 같다고 믿습니다. 어떤 프레임워크도 다른 데에서 안되는 일을 할 수 있진 않아요. 그냥 코드니까요. 그럼 무엇이 테스트 프레임워크를 차별화할까요? 저는 제일 다른 점은 유저 인터페이스라 생각합니다. 그래서 실제로 프레임워크간의 유저인터페이스를 비교해보려 합니다.

RSpec에서 좋은 점들

단언컨데, RSpec에서 가장 좋아하는 것은 실패하면, 어떻게 그 테스트만 실행하는지 출력 밑에 찍어주는 것 입니다. 그 줄을 복붙해서 한 테스트만 실행 할 수 있죠. 모르는 분들을 위해 예를 보여드릴게요.

describe "something" do
  it "works" do
    expect(10).to equal(11)
  end
  it "really works" do
    expect(11).to equal(11)
  end
end

이걸 실행하면 출력은 이런 느낌입니다.

[aaron@TC tlm.com (master)]$ rspec code/fail_spec.rb 
F.
Failures:
  1) something works
     Failure/Error: expect(10).to eq(11)
       expected: 11
            got: 10
       (compared using ==)
     # ./code/fail_spec.rb:3:in `block (2 levels) in <top (required)>'
Finished in 0.00504 seconds (files took 0.20501 seconds to load)
2 examples, 1 failure
Failed examples:
rspec ./code/fail_spec.rb:2 # something works
[aaron@TC tlm.com (master)]$ 

실패한 한 테스트만 다시 실행하려면 그냥 이렇게 한 줄만 복붙하면 됩니다.

재실행하는 움짤있던 자리

생각하거나 타이핑 할 필요도 없이, 복붙 실행했습니다. “놀면서 배우는” 기능이라 생각하곤 합니다.

RSpec에서 또 좋은 점은 색을 입힌 아웃풋 포메터가 들어있다 점입니다. 그래서 rspec --color를 하면 색이 들어간 출력을 얻을 수 있습니다. 저는 몇년간 Minitest를 컬러없는 출력으로 사용했었습니다만, RSpec에 와서 색이 들어간 출력을 정말 좋아하게 됐습니다. 이건 출력에서 좀 더 중요한 부분인 선언(assertion) 실패와 스택 트레이스을 빠르게 볼 수 있게 도와줍니다.(하지만 내 흰색 배경의 터미널에서 잘 매치되지않는 경우가 있기 때문에 RSpec 코어 팀은 전부 검은색 화면을 사용하지 않나 생각합니다.)

Minitest에서 좋은 점들

Minitest에서 가장 좋은 점은 Minitest의 테스트는 그냥 루비 클래스라는 점입니다. 여기 예제 태스트는 아까 보여준 RSpec 예제와 비슷합니다.

require 'minitest/autorun'
class Something < Minitest::Test
  def test_works
    assert_equal 11, 10
  end
  def test_really_works
    assert_equal 11, 11
  end
end

test_works 메소드가 선언된 곳을 정확히 알 수 있기 때문에 좋습니다. 루비의 다른 클래스와 다를게 없어서 새로 배울 것도 없습니다. 저는 CTags를 많이 쓰기 때문에, 에디터안에서 테스트 메소드나 클래스로 갈 수 있어서 좋습니다. 일반 루비 클래스를 사용하는 다른 좋은 점은 테스트를 리팩터링 할 때 나타납니다. 메소드 클래스 모듈을 추출하고, 상속을 변경하는 등의 그냥 일반적인 리팩터링 스킬을 쓸 수 있습니다. 라이브러리 파일에서 사용한 리팩터링 테크닉을 테스트 파일에서도 똑같이 사용할 수 있습니다. 저는 이게 Minitest가 돋보이는 부분이라 생각합니다.

RSpec에서 싫은 점

RSpec은 테스트를 적기 위한 DSL입니다. 저는 아마 이게 RSpec의 가장 큰 약점이라 생각합니다. 저느 프로그래머니, 코드를 읽을 수 있어서, 테스트 코드가 “영어처럼 읽히는지”는 별로 중요하지 않습니다.

저는 DSL의 가치를 이해할 수 없습니다, 특히 리팩터링하고 싶은 4000줄 가까이되는 스팩파일에서는요. 어떻게 리팩토링 해야 합니까? 메소드를 추출할 수 있겠죠.

describe "something" do
  def helper
    11
  end
  it "works" do
    expect(10).to eq(helper)
  end
  it "really works" do
    expect(11).to eq(helper)
  end
end

근데 어디에 helper 메소드가 정의 되었나요? 메소드의 가시성(visibility)은 뭐죠? 모듈에 넣을 수 있나요? 상속을 쓸 수 있나요? 어디서 호출할 수 있나요? 추출 된 메소드에서 super를 호출할 수 있나요? 할 수있다면 super는 어디로 가나요? 이런 것들은 내가 3000개의 테스트 실패와 읽어야 할 진짜 긴 스팩 파일이 있을 때 생각하고 싶은 것들이 아닙니다. 그런 경우 “RSpec이 어떻게 작동하는지 궁금하네?”에서 “내 고양이가 뭐하는지 궁금하네?”로 바뀌죠.

제가 이해하기론, RSpec 에서 describe을 호출하는 것은 본질 적으로 class의 선언입니다. 하지만 정말 그런 경우, 왜 루비 클래스를 사용하지 않나요? 그러면 메소드 가시성, 모듈, 상속등을 추정하지 않아도 되는데요. 라이브러리 코드에서 제가 사용하는 것은 말할 필요도 없이 루비 클래스 입니다. 왜 테스트를 위해 다른 언어를 사용해야 하죠?

describe를 중첩하는 것은 상속하는 것처럼 보입니다만, before 블록같은 특정한 것에 대해서만 그렇습니다. 이 코드를 실행한다면,

describe "something" do
  before do
    puts "hi!"
  end
  it "works" do
    expect(10).to eq(helper)
  end
  it "really works" do
    expect(11).to eq(helper)
  end
  context "another thing" do
    before do
      puts "hello!"
    end
    it "really really works" do
      expect(11).to eq(helper)
    end
  end
  def helper
    11
  end
end

“hi”가 3번 각각 한번씩 3번 출력되고, “hello!”가 한 번 출력하는 것을 보게 됩니다. 중첩된 context에서 “hello!”를 “hi!”가 출력하게 하고 싶다면 어떨까요? 일반적인 상속의 경우, super를 호출하도록 변경할 겁니다. 새로운 것을 배우지 않고 방금전에 말한 걸하려면 어떻게 해야하는지 잘모르겠습니다.^1 (아마도 let이 필요할 것 같지만 잘 모르겠네요.) 지금으로썬, 스펙 밖에 있는 클래스들을 리팩토링하고, 그다음엔 그 스펙에서 객체를 불러오는 식으로 대처하고 있습니다. 본질적으로 테스트에서의 “조합(composition)”이지요.

또 나에게 에러처럼 보이는 것은, RSpec테스트를 리팩터링하려고 복붙을 하면 어떤 줄을 바꿔도 한 테스트만 실행하기가 더이상 안된다는 점입니다. 새 줄을 찾을 필요가 있게됩니다. RSpec에 -e 플래그로 테스트의 이름을 넣어 사용할 순 있겠지만, 그렇게하면 “복붙 실행이라는” 아주 좋아하는 부분이 없어져 버리죠.

어쨋든 나는 이제 RSpec그만 깔껍니다. 제가 보기엔, RSpec은 “테스트를 적는 언어”와 “그냥 루비”사이에서 어떤 정채성 혼란이 있어 보입니다. 이게 RSpec의 가장 큰 약점입니다.

Minitest에서 싫은 점

RSpec이 한가지 Minitest보다 나은게 있냐고 물으면, 아마 커맨드 라인 UI가 Minitest보다 훌륭하다고 할 겁니다. 여기 예제를 보세요.

이렇게 한번에 한 테스트를 실행할 수 있습니다.

$ ruby code/fail_test.rb -n Something#test_works

이 명령은 Something에서 test_works 메소드를 실행하게 합니다. 나에게 문제가 되는 것은 테스트 실패 출력에서 이걸 하는 방법이 명확하게 안보인다는 점입니다. 전체 케이스를 실행하면 출력은 이렇게 나옵니다.

[aaron@TC tlm.com (master)]$ ruby code/fail_test.rb 
Run options: --seed 5990
# Running:
.F
Finished in 0.002047s, 977.0396 runs/s, 977.0396 assertions/s.
  1) Failure:
Something#test_works [code/fail_test.rb:5]:
Expected: 11
  Actual: 10
2 runs, 2 assertions, 1 failures, 0 errors, 0 skips
[aaron@TC tlm.com (master)]$

이거 보세요. 어떻게 한 개의 테스트를 실행할지 알 수 있나요? RSpec에선 문제 없죠. RSpec에선 처음 사용했을 때에도 테스트를 한 개만 실행하는 방법을 알 수 있었습니다. 어떤 문서도 읽지 않았고, 그냥 테스트를 실행했는데 정보가 거기 있었을 뿐이죠. RSpec에서 오신 분은 아마 이렇게 할 수 있지 않을까 생각할 수 도 있습니다.

$ ruby code/fail_test.rb:5

물론 루비를 커맨드 라인에서 사용하는 거라서 동작하진 않습니다.

여기에 테스트를 한 개만 하려 할 때 제가 어떻게 하는지 동영상으로 찍어보았습니다.

데모..가 있던 자리

RSpec의 출력과 비교해 이 방식의 결점은 커맨드 라인 인터페이스를 알아야하고 몇자 적어야할 필요가 있다는 점입니다. 아마 “몇자잖아”라고 생각할 지도 모르지만, 매번 더 많은 시간을 들여 고쳐야 할 실수를 할 수 있는 무언가를 타이핑해야 할 필요가 있어요. 정말 “복붙 실행”의 장점은 과소평가 될 수 없다고 생각합니다. (네, Minitest와 RSpec에 autotest같은 툴이 있는건 압니다만, 그런건 어떨 때에는 사용할 수 없거나 사용하지 않습니다.)^1

Minitest에 원하는 또 한가지는 RSpec에서 그러는 것처럼 컬러 포메터가 포함되었으면 좋겠습니다. RSpec에서 컬러 아웃풋을 사용하고나니, 정말 돌아가고 싶지 않아요. 컬러 포메터는 정말 테스트가 실패 했을 때 어떤 것이 중요한지 파악하는데 도움이 됩니다.

결론

양쪽의 프레임워크 다 제가 보기에 좋은 점과 싫은 점이 있습니다. 디버깅과 리팩터링 스킬을 테스트에서 사용하는 것은 저한테는 꽤나 중요한 일입니다. 특히 많은 레거시 코드를 다룰때에는요. 이게 제가 개인 프로젝트할 때에는 Minitest만 사용하는 이유입니다. 하지만, RSpec에서도 리팩터링과 디버깅이 나아질거라고 확신하고, 나아지고 있습니다. 나중에 뭘 배웠는지 공유할게요.

지금, 당신이 뭘쓰는지는 상관없습니다. 테스트를 하고 있는 한 저는 만족합니다. 프로 개발자는 둘 중 하나는 할 수 있어야 합니다. 왜냐하면, 기본적으로 저 둘은 같은 일을 (코드를 테스트) 하거든요.

1: RSpec의 개발자인 @myronmarston님이 여기에 대해 답변 했었습니다. 이 글도 읽어 보세요. 2: 레거시 프로젝트라면 새로 넣는게 어려우니 “기본적으로 테스트 프레임워크에 포함되어있는가?”가 중요하다는 이야기를 하려고 하는 것 같아요.