(본 게시물은 저작권의 문제 발생시 출판사의 요청에 의해 삭제될 수 있습니다.)
단위 테스트는 작은 코드 덩어리, 즉 일반적인 각 메서드와 메서드에 포함된 각 줄을 대상으로 테스트를 수행한다. 시스템 전체를 하나의 테스트 대상으로 보는 다른 테스트와는 대조된다.
소프트웨어는 결국 계층 구조를 이루고 있기 때문에 테스트 대상을 작게 잡아야 한다. 특정 위치에 있는 코드들은 그 아래 계층의 코드가 정상으로 작동해야만 문제없이 작동한다. 아래 계층의 코드에 버그가 있다면 그 위의 계층 역시 이에 영향을 받을 수 있다.
단위 테스트는 프로그래머가 더 좋은 코드를 작성하는 데 도움을 준다. 이는 코드를 실제로 작성하기 전에 테스트에 대해 생각해 보게 함으로써 자연스럽게 더 좋은, 더 느슨한 설계를 유도한다. 또한 코드를 작성하는 동안 코드가 얼마나 정확한지 그때그때 결과를 확인할 수 있어서 도움이 된다. 코드를 작성한 이후에도 코드가 여전히 동작하는지 확인할 수 있고 다른 사람들이 코드를 어떻게 사용하는지 이해할 수 있도록 도와준다.
로마 숫자(Roman number) 클래스를 테스트한다고 가정해보자. 아래의 예제는 특정 숫자를 표현하는 객체를 생성하고 그 객체를 로마 숫자 표기법으로 출력한다.
# 이 코드는 버그가 있다.
class Roman
MAX_ROMAN = 4000
def initialize(value)
if value <= 0 | | value > MAX_ROMAN
fail "Roman values must be > 0 and <= #{MAX_ROMAN}"
end
@value = value
end
FACTORS = [ [ "m", 1000], [ "cm", 900], ["d", 500], ["cd", 400],
[ "c", 100], ["xc", 90], ["l", 50], ["xl", 40],
[ "x", 10], [ "ix", 9], ["v", 5], [ "iv", 4],
[ "i", 1]]
def to_s
value = @value
roman = ""
for code, factor in FACTORS
count, value = value.divmod(factor)
roman < code unless count.zero?
end
roman
end
end
위의 코드를 테스트하기 위해 다음과 같은 코드를 하나 더 작성했다.
require_relative 'romanbug'
r = Roman.new(1)
fail " 'i' exprected" unless r.to_s == "i"
r = Roman.new(9)
fail " 'ix' expected" unless r.to_s == "ix"
위와 같은 테스트코드는 테스트 개수가 늘어나면 너무 복잡해져 관리가 어려울 수 있다. 테스트 과정을 구조화 하기 위한 다양한 단위 테스트 프레임워크가 등작했고 루비 배포판에는 이미 라이언 데이비스(Ryan Davis)의 MiniTest가 포함되어 있다.
Minitest는 이전의 Test::Unit과 거의 같지만 필수적이지 않은 테스트케이스 러너, GUI 지원 등이 제거되었다. 이 블로그에 포스팅되는 테스트는 Test::Unit 래퍼를 사용한다.
테스트 프레임워크
테스트 프레임워크 기본적인 기능 세가지
1) 개별 테스트를 표현하는 방법을 제공
2) 테스트를 구조화하는 프레임워크 제공
3) 테스트를 수행하기 위한 유연한 방법 제공
단언(assertion) == 기대 결과
테스트 안에서 if 문을 길게 나열하는 것을 피하기 위해 테스트 프레임워크는 같은 일을 하는 데 단언(assertion)을 사용한다. 단언문 형식은 다양하지만 이것들을 따르는 패턴은 기본적으로 같다. 단언문은 기대되는 결과를 적을 수 있는 방법과 실제 결과를 전달하는 방법을 제공한다. 실제 결과와 기대한 결과가 다르다면, 단언문은 멋진 문구와 함께 이 사실을 실패(failure)로 기록한다.
위의 예제인 Roman 클레스 테스트를 다시 작성한 예제이다. 단지 assert_equal 메서드만을 살펴보라.
require_relative 'romanbug'
require 'test/unit'
class TestRoman < Test::Unit::TestCase
def test_simple
assert_equal("i", Roman.new(1).to_s)
assert_equal("ix", Roman.new(9).to_s)
end
end
실행결과
Run options:
# Running tests:
.
Finished tests in 0.0000372s, 296.6479 tests/s, 593.2958 assertions/s.
1 tests, 2 assertions, 0 failures, 0 erros, 0 skips
생성자는 넘겨받은 숫자를 로마 숫자로 표현할 수 있는지 확인하고, 만일 표현할 수 없다면 예외를 던진다. 이런 예외를 테스트해보자.
require_relative 'roman3'
require 'test/unit'
class TestRoman < Test::Unit::TestCase
NUMBERS = {1 => "i", 2 => "ii", 3 => "iii", 4 => "iv", 5 => "v", 9 => "ix" }
def test_simple
NUMBERS.each do | arabic, roman |
r = Roman.new(arabic)
assert_equal(roman, r.to_s)
end
end
def test_range
# 아래 두 줄에서는 예외가 발상하지 않는다.
Roman.new(1)
Roman.new(4999)
# 아래 두줄에서는 예외가 발생한다.
assert_raises(RuntimeError) { Roman.new(0) }
assert_raises(RuntimeError) { Roman.new(5000) }
end
end
실행결과
Run options:
#Running tests:
..
Finished tests in 0.003058s, blabla
2 tests, 8 assertions, 0 failures, 0errors, 0 skips
assert_euqal의 부정 단언문은 refute_equal이다. 각 단언문에 넘겨지는 마지막 매개 변수는 모든 실패 메시지 이전에 출력할 메시지다. 실패 메시지는 거의가 확인할 수 있기 때문에 이 매개변수를 사용하지 않지만, 실패 메시지가 필요한 한가지 예외는 refute_nil이다.
require 'test/unit'
class ATestThatFails < Test::Unit::TestCase
def test_user_created
user = User.find(1)
refute_nil(user, "User with ID=1 should exist")
end
end
실행결과
Run options:
# Running tests:
F
Finished test
1) Failure:
ATestThatFails#test_user_created [prog.rb:11]:
User with ID=1 should exist.
Excepted nil to not be nil.
1 tests, 1 assertions, 1 failures, 0 erros, 0 skips
끄읕.