제공 : 한빛 네트워크
저자 : Bill Walton
역자 : 노재현
원문 : Cookin" with Ruby on Rails - Designing for Testability
[이전 기사 보기]
Cooking with Ruby on Rails - Designing for Testability (1)
Cooking with Ruby on Rails - Designing for Testability (2)
Cooking with Ruby on Rails - Designing for Testability (3)
CB : 좋았어. 그럼 이제 우리가 만든 테스트를 이용해서 개발을 해보자고. 내가 자네가 보스를 설득시켜 줬으면 하는 개발방법이 바로 실패하는 테스트가 나올때까지 새로운 코드를 작성하는 것을 미루는 방법이야. 우선 카테고리 모델부터 시작해보자고. Rails에서는 모델이 적절히 작동하는지 확인하기 위해서 유닛 테스트를 사용했었어. 적절히 라는 의미는 CRUD 기능이 잘 작동하는 것을 의미하는 거야. 여태까지 우리가 작성했던 테스트가 그걸 확인하는 거지. 그리고 다음 으로 적절히가 의미하는 건 애플리케이션이 데이터를 데이터베이스에 쓰기 전에 실행할 수 있는 검증(validation) 에 대한거야. 그리고 마지막으로 우리가 모델에 추가하게 되는 함수가 정상적으로 작동하는지 확인하는 거지.
전에 Category 테이블의 레코드는 항상 이름을 가져야 하고 이름의 길이는 100글자를 넘어선 안된다고 했었어. 그리고 이전에 그 규칙이 적용되지 않고 있다는 것도 발견했지. 이름 필드에 기본 값으로 ""을 주지 않았다고 하더라고 고객이 스페이스 바를 쳐넣었더라도 같은 버그가 발생했었을 거야. 그래서 검증 기능과 더불어서 고객이 스페이스 바를 넣는지를 확인할 수 있는 함수도 만들어야 겠어. 하지만 테스트가 우리가 무엇이 필요한지 알려주도록 해보자고.
그럼 카테고리 테스트에 새로운 함수를 하나 추가하되 적절한 이름일 경우에만 저장될 수 있도록 테스트 하는 함수를 만들어 보자고. 우선 이름이 없는 값을 저장하려고 시도해서 실패가 나도록 하고 다음으로 제대로 이름을 넣어서 성공적으로 저장되도록 할 거야.
def test_must_have_a_valid_name
rec_with_no_name = Category.new
assert_equal(false, rec_with_no_name.save)
rec_with_name = Category.new(:name => "something new")
assert rec_with_name.save
end
이제 실행해 보면
ruby testunitcategory_test.rb
그림 31.
Paul : Rails가 실패했다고 나오네요. 우린 실패하기를 바라고 있었잖아요? 그죠? 제가 제대로 따라가고 있는건가요?
CB : 문제 없어. 우리는 테스트가 실패하기를 바라고 있었던게 아니고 저장이 실패하기를 바라고 있었지. 사실 이미 테스트가 실패할 거라는 걸 알고 있었지. 시스템이 정상적으로 작동했다면 Category.save 함수는 저장에 실패했을거고 nil을 리턴하면서 테스트는 성공했을거야. 테스트가 실패했음을 알리면서 우리는 시스템이 정상적으로 작동하지 않고 있다는 걸 알 수 있게 된거지. 하지만 데이터베이스가 기본 값으로 공백 문자열을 사용하고 있기때문에 이렇게 되면 안되면서도 저장이 성공을 하게 된거야. 그러니 좀 이따 스키마를 바꿔서 데이터베이스가 기본 값으로 공백 문자열을 넣지 않도록 해야지. 그 전에 추가적으로 해야 하는 일이 있는데 고객이 사이트에 방문해서 이름에 스페이스 바로 공백 문자열을 만들 수 없게 하는거야. 이제 이름 값으로 특정값이 입력되었는지 않았는지를 확인할 수 있는 검증 코드를 만들어 보자고. Category 모델에 한 줄을 추가해야 해. appmodelscategory.rb 파일을 열고 다음을 추가해 줘
validates_presense_of :name
이제 테스트 케이스가 뭐라고 하는지 한 번 보자고
ruby testunitcategory_test.rb
그림 32.
Paul : 아직 안 고쳐졌나본데요.
CB : 잘 보라고 Paul. 이전과 같은 실패가 아니야. 하지만 자책하지는 말라고 내가 아직 제대로 보는 방법을 말해주지는 않았으니 말이야. "Started" 바로 다음 줄 보여? 첫 번째 칸에 "F" 라고 쓰여져 있는 건 Rails 가 실행한 테스트 함수중에서 첫 번째 테스트 함수가 실패했음을 말하는 거야. 그 다음의 테스트 함수들은 정상적으로 실행이 되었다는 걸 의미하기도 하지. 왜냐면 성공의 경우에는 "." 으로 출력하거든. 바로 이전의 테스트에서는 두 번째 테스트가 실패했었고 첫 번째와 세 번째 테스트는 성공했었는데 말이야. 모든 테스트가 다 실행되고 나면 정확하게 어디서 어떤 에러가 났는지가 화면에 출력이 되게 되어 있어. 우리가 검증(validation)기능을 추가했기 때문에 test_create_and_destroy 테스트 함수에서 실패가 났고 21번째 줄에서 났다라고 화면에 나타났어. 그럼 한 번 21번째 줄을 보자고.
그림 33.
CB : 실패 메시지를 더 보면 "<3> 이라는 값을 예상했는데 <2> 라는 값이 나왔다" 라는 걸 알 수 있어. 그건 바로 첫 번째 인자는 3인데, 두 번째 인자가 2 였다는 걸 의미하지. 실패의 원인은 바로 new_rec 레코드가 우리가 새로 추가한 검증 기능 때문에 저장되지 못했기 때문이야. 검증 기능이 이름 필드에 아무 값도 없이 저장되는 걸 막았기 때문이지. 이제 우리가 할 일은 저장하기 전에 이름을 지정해 주는 것 뿐이지.
new_rec.name = "for validation"
이제 다시 실행해 보자.
ruby testunitcategory_test.rb
그림 34.
CB : 짜잔.
Paul : 테스트를 실행해서 문제점을 찾는 기능은 상당히 괜찮네요. 그런데 아직 고객이 스페이스 바를 입력해서 이름 필드에 공백 문자를 넣으려고 할때와 이름이 아주 긴 경우에 대해서는 테스트를 해보지 않았어요.
CB : 아아 알고 있어. 단지 좀 현재까지의 성과를 자축해 보고 싶었던 거야. 이제 스페이스 바 문제를 해결하기 위해서 test_must_have_a_valid_name 이라는 테스트 함수를 추가해 보자.
rec_with_blank_name = Category.new(:name => " ")
assert_equal(false, rec_with_blank_name.save)
다시 테스트를 실행해 보면 다음과 같지.
그림 35.
Paul : 와 이제 validation_presense_of 검증 기능이 스페이스 바를 완벽하게 막아내고 있네요.
CB : 이제 긴 이름 값을 테스트 하는 것만 남았지? 다음 테스트 함수를 category_test.rb 함수에 추가하고
def test_long_names
partial_name = ""
"a".upto("y") {|letter| partial_name << letter}
rec_with_borderline_name = Category.new(:name => (partial_name * 4))
assert_equal(100, rec_with_borderline_name.name.size)
assert rec_with_borderline_name.save
rec_with_too_long_name = Category.new(:name => ((partial_name * 4) << "z"))
assert_equal(101, rec_with_too_long_name.name.size)
assert_equal(false, rec_with_too_long_name.save)
end
실행하면
그림 36.
CB : 테스트가 실패했네. 그럼 이제 코드를 작성할 차례인데.
Paul : 그 전에요 질문이 있는데요. 제가 보기엔 "Started" 밑에 두 번째 칸에 "F" 표시가 되어 있는데요. 아까 보기에는 새로 추가된 함수를 제일 아래 쪽에 추가했던 것 같은데 에러는 위쪽의 함수에서 난다고 되어 있네요?
CB : 잘 봤어. 테스트 함수들은 꼭 파일에서 정해진 순서에 따라서 실행되지는 않아. 그래서 반드시 마지막에 나오는 에러 메시지를 봐야 해. 여기서는 이번에 새로 추가한 함수인 44번째 줄에서 에러가 난다고 말하고 있네.
assert_equal(false, rec_with_too_long_name.save)
위 assertion이 실패했다는 건 이름이 충분히 긴데도 불구하고 저장이 되었다는 걸 의미하지. MySQL에서 문자열이 길 때는 넘어가는 문자열은 잘라버리고 저장하게 되어 있어. 이건 별로 올바른 연산 같지는 않지만 어쨌든 이렇게 되지 않도록 검증 기능을 만들어 보겠어. appmodelscategory.rb 파일을 열고 전에 추가했던 validates_presense_of 바로 밑에 다음 한 줄을 추가하자고
validates_length_of :name, :maximum >= 100
Paul : 네 그 전에 최소 길이는 어떻게 할까요? "X" 같은 것도 통과하도록 할까요?
CB : 정말 좋은 질문이야. 그건 보스가 대답을 좀 해줬으면 좋을 만한 질문인데 우선 허용 범위를 지정할 수 있게 해주는 다른 옵션을 써보자고. 최소 한 글자는 되야 한다는 것을 알고 있으니 다음과 같이 추가해 보면
validates_length_of :name, :in => 1..100
나중에 보스가 최소 글자 수를 바꾸고 싶어하면 금방 바꿔줄 수 있지. 그럼 다시 테스트를 실행해 보면
ruby testunitcategory_test.rb
그림 36.
CB : 테스트가 실패했네. 그럼 이제 코드를 작성할 차례인데.
Paul : 그 전에요 질문이 있는데요. 제가 보기엔 "Started" 밑에 두 번째 칸에 "F" 표시가 되어 있는데요. 아까 보기에는 새로 추가된 함수를 제일 아래 쪽에 추가했던 것 같은데 에러는 위쪽의 함수에서 난다고 되어 있네요?
CB : 잘 봤어. 테스트 함수들은 꼭 파일에서 정해진 순서에 따라서 실행되지는 않아. 그래서 반드시 마지막에 나오는 에러 메시지를 봐야 해. 여기서는 이번에 새로 추가한 함수인 44번째 줄에서 에러가 난다고 말하고 있네.
assert_equal(false, rec_with_too_long_name.save)
위 assertion이 실패했다는 건 이름이 충분히 긴데도 불구하고 저장이 되었다는 걸 의미하지. MySQL에서 문자열이 길 때는 넘어가는 문자열은 잘라버리고 저장하게 되어 있어. 이건 별로 올바른 연산 같지는 않지만 어쨌든 이렇게 되지 않도록 검증 기능을 만들어 보겠어. appmodelscategory.rb 파일을 열고 전에 추가했던 validates_presense_of 바로 밑에 다음 한 줄을 추가하자고
validates_length_of :name, :maximum >= 100
Paul : 네 그 전에 최소 길이는 어떻게 할까요? "X" 같은 것도 통과하도록 할까요?
CB : 정말 좋은 질문이야. 그건 보스가 대답을 좀 해줬으면 좋을 만한 질문인데 우선 허용 범위를 지정할 수 있게 해주는 다른 옵션을 써보자고. 최소 한 글자는 되야 한다는 것을 알고 있으니 다음과 같이 추가해 보면
validates_length_of :name, :in => 1..100
나중에 보스가 최소 글자 수를 바꾸고 싶어하면 금방 바꿔줄 수 있지. 그럼 다시 테스트를 실행해 보면
ruby testunitcategory_test.rb
그림 38.
바꾼 후에도 테스트가 성공하는지 확인해 봐야지?
그림 39.
Paul : 와우. 그럼 이제 Categories는 다 끝난거죠?
CB : 유닛 테스트는 그렇다고 할 수 있지. 이제 현재까지 카테고리 모델의 정의된 기능을 테스트할 수 있게 된거야. 이제 조리법 모델에도 같은 내용을 적용해 보자. 카테고리 모델에서 사용했던 검증 기능을 조리법 모델에도 적용을 하게 될거야(appmodelsrecipe.rb), 필드 이름만 잘 적용해 주면 돼
validates_presence_of :title
validates_length_of :title, :in => 1..100
그리고 나서 recipe_test.rb 파일에 새로운 함수들을 추가할 거야. 역시나 "name" 필드의 이름을 "title"로 잘 변경해야겠지. 이제 조리법 테스트 케이스가 다음과 같이 되었어.
require File.dirname(__FILE__) + "/../test_helper"
class RecipeTest < Test::Unit::TestCase
fixtures :recipes
def test_read_and_update
rec_retrieved = Recipe.find_by_title("pizza")
assert_not_nil rec_retrieved
rec_retrieved.title = "pie"
assert rec_retrieved.save
changed_rec = Recipe.find_by_title("pie")
assert_not_nil changed_rec
unwanted_rec = Recipe.find_by_title("pizza")
assert_nil unwanted_rec
end
def test_create_and_destroy
initial_rec_count = Recipe.count
new_rec = Recipe.new(:title => "something new")
new_rec.category_id = 1
new_rec.save
assert_equal(initial_rec_count + 1, Recipe.count)
new_rec.destroy
assert_equal(initial_rec_count, Recipe.count)
end
def test_must_have_a_valid_title
rec_with_title = Recipe.new(:title => "something new")
rec_with_title.category_id = 1
assert rec_with_title.save
rec_with_no_title = Recipe.new
rec_with_no_title.category_id = 1
assert_false rec_with_no_title.save
rec_with_blank_title = Recipe.new(:title => " ")
rec_with_blank_title.category_id = 1
assert_false rec_with_blank_title.save
end
def test_long_titles
partial_title = ""
"a".upto("y") {|letter| partial_title << letter}
rec_with_borderline_title = Recipe.new(:title => (partial_title * 4))
rec_with_borderline_title.category_id = 1
assert_equal(100, rec_with_borderline_title.title.size)
assert rec_with_borderline_title.save
rec_with_too_long_title = Recipe.new(:title => ((partial_title * 4) << "z"))
rec_with_too_long_title.category_id = 1
assert_equal(101, rec_with_too_long_title.title.size)
assert_false rec_with_too_long_title.save
end
def teardown
recipes = Recipe.find(:all)
recipes.each do |this_recipe|
this_recipe.destroy
end
end
end
CB : 이제 테스트를 실행해 보면
그림 40.
Paul : 짜잔.
CB : 쉽지?
Paul : 네 정말 그렇네요. 이제 Functional과 Integration 테스트를 정말 해보고 싶네요. 그동안 그것때문에 힘들었거든요. 근데 이제 가봐야 겠어요. 보스, 고객들과 함께 프로젝트 현황에 대한 회의를 해야 하거든요. 제가 가기전에 아까 참고할 문서들을 좀 알려주신다고 했었는데...
CB : 아직 "Agile Web Development with Rails" 책 안 읽었어? 거기에 테스팅에 관한 챕터가 있어. 그리고 "Rails Recipes"라는 책도 좋은 책이고. Chad Fowler 님이 쓴 책인데 유용한 테스팅 방법들이 많이 있어. 그리고 좋은 온라인 튜토리얼 문서로 "Testing the Rails" 라는 문서가
http://manuals.rubyonrails.com/read/chapter/20 에 있고, Bala Paranj 라는 친구가 운영하고 있는 페이지에 루비 테스팅에 유용한 문서들의 링크가 많이 있어. 여기서 보면 돼.
http://bparanj.blogspot.com/2006/11/resources-for-testing-ruby-on-rails.html. 이 정도면 충분할 거라고.
다음에 만날때는 Functional과 Integration 테스트에 대해서 해보자고.
Paul이 나가고 나서 CB는 생각에 잠겼다. "여기서 있었던 일로 정말 긍정적인 변화가 시작되겠는걸"