메뉴 바로가기 검색 및 카테고리 바로가기 본문 바로가기

한빛출판네트워크

IT/모바일

Cookin" with Ruby on Rails - More Designing for Testability(1)

한빛미디어

|

2007-10-11

|

by HANBIT

9,554

제공 : 한빛 네트워크
저자 : Bill Walton
역자 : 김탁용
원문 : Cookin" with Ruby on Rails - More Designing for Testability

[이 시리즈의 이전 기사]
Cooking with Ruby on Rails - Designing for Testability(4)

CB: 폴, 안녕하세요? 기능 테스트(Funtional Test)할 준비는 됐나요?

Paul: 물론이죠, CB. 그런데 우리가 지난 번에 했던 단위 테스트(Unit Test)에 대해 생각해봤는데 말이죠, 우리가 그걸 너무 빨리 끝마친 게 아닌가 싶은 걱정이 조금 들더라고요. 기능 테스트를 시작하기 전에 애플리케이션을 빠르게 한 번 살펴보고 하는 게 어떨까요?

CB: 좋아요! 바로 시작하죠. 당신을 괴롭히는 게 뭐죠?

Paul: 그 문제("Cookin" with Ruby on Rails: Designing for Testability" 기사, 그림 22) 기억나요? 레코드가 남아 있는 조리법 테이블 때문에 카테고리 픽스쳐(fixture)를 로딩하지 못했었잖아요. 우리가 보스와 일하고 있을 때 실제 그런 문제를 봤었던 걸 당신이 상기시켜줬었고요. 나중에 다시 보자고 하고선 임시로 고쳐놓은 데를 얘기했었는데. 픽스쳐 문제는 해결 했지만, 우리는 그 코드를 다시 열어보지 않았잖아요. 내 생각에 우리가 정말로 단위 테스트들을 끝내려 한다면 코드를 빨리 한번 훑어봐야 같아요.

CB: 상기시켜줘서 고마워요, 풀. 이제 기억이 나요. 제가 “두 군데 문제를 해결해보자”같은 말을 했던 게 생각나네요. 하지만 테스트를 동작시키는 것에만 신경 쓰고, 다시 돌아가서 두 번째 부분을 하는 것은 완전히 잊어버렸네요. 내 기억이 맞다면, 방문자들이 조리법이 들어있는 카테고리는 지우지 못하게 하는 코드들에 대해 이야기하고 있었어요. 카테고리 컨트롤러에 있는 코드를 한번 보죠.


[그림 1]

그래요. 더 나빠지기 전에, 이걸 지금 고치는 게 좋겠는데요. 여기서 해줘도 작동은 하겠지만, 이건 컨트롤러 대신에 모델 안에서 다뤄져야 할 거 같군요.

Paul: 왜 그렇게 말하는 거죠? 모델 안에서 다뤄져야 한다는 말 말이예요.

CB: 가장 큰 이유는 이건 비즈니스 로직이라는 거예요. 우리 보스라면 이렇게 물을 거예요. "방문자들이 조리법이 있는 카테고리를 지우고자 하면, 그걸 어떻게 처리할 건가? " 여기엔 많은 기술적인 선택 조건들이 있어요. 우리는 카테고리와 그 아래 있는 조리법을 모두 지울 수도 있죠. 빈 카테고리라든지 기본 카테고리를 분류한 다음에, 카테고리가 지워진 조리법들은 다시 카테고리를 설정하게 할 수도 있고요. 어쩌면 그는 일부 방문자들에게만 카테고리를 지울 수 있게 하는 것을 원할지도 몰라요. 요점은, 그의 요청이란 비즈니스의 문제라는 거예요. MVC 패턴에서 비지니스 로직은 모델에 속하는 거죠. 다르게 생각해보면 이것은 정말 하나의 유효성 체크예요. 우리 지난 번에 일정조건이 없는 카테고리 객체들은 데이터베이스에 저장하지 못하도록 유효성 체크를 모아놨었잖아요. 이것도 어떤 조건이 없다면 데이터베이스에서 카테고리 레코드를 삭제하지 못하게 하는 유효성 체크죠.

Paul: 훌륭하군요. 나도 그것들에 대해 많이 생각해 봤었는데, 우리가 같은 생각을 하고 있는 지 알고 싶었어요. 자 그럼, 어떻게 진행을 해볼까요?

CB: 리셋부터 해요. 카테고리 모델에 대한 단위 테스트들을 다시 실행하고요. 커맨드 창을 열게요..
ruby testunitcategory_test.rb

[그림 2]


[독자를 위한 안내] 카테고리 단위 테스트들 실행하다가 에러가 생긴다면 테스트용 데이터베이스에 조리법 레코드들이 아직 남아있는 것 있을 수 있다. 그것들을 지우려면 recipe_test.rb를 실행하라.

이제 애플리케이션을 멈추게 하는 카테고리 컨트롤러 안 코드를 열어봐요.


[그림 3]

그럼, 프로그램을 체크하죠. Mongrel을 시작하고, 브라우저에서 http://localhost:3000/category/list를 열어서 카테고리들 중 하나를 지울게요.


[그림 4]

CB: 흠, 멈췄군요. 그럼 테스트들을 다시 실행시키죠.


[그림 5]

Paul: 프로그램이 멈췄지만, 지금 테스트들은 그걸 못 잡아요.

CB: 맞아요. 우리가 코드를 작성하기 전에 테스트를 먼저 작성해두었더라면 아마도 이렇게 되지 않았겠죠. 그렇지만 진실은, 우리는 매번 테스트를 까먹고 프로그램이 망가지는 것을 본 뒤에야 다시 돌아가 테스트를 추가하곤 한단 거예요. 하지만 처음에 코드를 작성하기 전에 생각해보고 테스트들을 작성하면, 문제가 생긴 뒤에 하는 것보다 훨씬 문제가 줄어들 거예요. 그럼 우리에게 문제점과 그걸 어떻게 고칠지를 보일 실패 테스트를 작성해보죠.

우리 테스트를 작성하기 전에 이것 하나만 생각해봐요. 우리는 애플리케이션이 의도한 대로 작동한다는 것을 테스트가 보장해주기를 바라잖아요. 우리는 자식 레코드가 있는 카테고리는 삭제할 수 없다는 걸 확신시켜줄 테스트가 필요해요. 문제는, 테스트 관점에서 봤을 때 우리는 아무런 자식 레코드도 없다는 거예요. 조리법 테스트들이 실행된 후에는 조리법 레코드들이 모두 지워지도록 조리법 픽스쳐에 teardown 메소드를 추가했어요. 그렇게 하지 않으면 카테고리 픽스쳐가 로드되지 않겠죠. 그래서 시작하기 위해, 지우려는 카테고리 레코드에 자식 레코드를 넣어줄 거예요.

앞에서 난 에러에서 보듯이, 카테고리 레코드를 삭제하려고 할 때 Rails는 예외를 일으키죠. Rails에서 어떤 예외는 다른 것들보다 더 안좋기도 해요. 예를 들어, 존재하지 않는 레코드를 찾으면, RecordNotFound 예외가 발생하죠. 우리가 원하면 그것을 사용할 수도 있고, 아니면 단지 그것이 비어있는지 결과만 체크할 수도 있어요. 우리 애플리케이션에서 발생한 StatementInvalid 예외는 테스트에서도 충돌을 일으킬 거예요. 다행히 Rails는 테스트 케이스와의 충돌 대신에 테스트 메소드가 failure를 보고하도록, built-in assertion을 던져주겠죠.

자 먼저, 카테고리 픽스처를 열어서 레코드를 하나 넣도록 하죠.


[그림 6]

CB: beverages 레코드를 사용할 생각이예요. 그럼 이제 testunitcategory_test.rb 를 열어 새로운 테스트 메소드를 추가할게요.
def test_cannot_delete_record_with_child

   category_with_child = Category.find_by_name("beverages")
   assert_not_nil category_with_child
 
   new_recipe = Recipe.new(:title => "test drink",
                            :category_id => category_with_child.id)
   new_recipe.save
   recipe_exists =
            Recipe.find_by_category_id(category_with_child.id)
   assert recipe_exists
 
   assert_nothing_raised(ActiveRecord::StatementInvalid) {category_with_child.destroy}
   category_with_child = Category.find_by_name("beverages")
   assert_not_nil category_with_child

   new_recipe.destroy
end

[독자를 위한 안내] assert_nothing_raised 메소드의 포맷은 assert_nothing_raised (exception) {block} 이다. {category_with_child.destroy} 은 위에 보여진 것처럼 두 라인으로 나누지 말고 같은 줄에 놓이도록 하라.

Paul: assert_nothing_raised로 시작하는 세번째 섹션이 핵심인 것 같군요.

CB: 맞아요. 우린 방문자가 자식 레코드가 있는 카테고리를 삭제하려고 할 때 예외가 발생하지 않길 원하잖아요. 처음 두 섹션은 셋업 과정이에요. 카테고리 레코드 하나를 가져와서 정말 원한 대로 가져왔는지를 테스트하고요. 조리법 자식 레코드를 하나 만들어서 이를 테스트하고. 그 다음에 우리가 알듯이 애플리케이션에서 StatementInvalid 예외를 일으키는 레코드를 지우는 시도를 하는거죠. assert_nothing_raised asertion은 테스트를 통과하는지 못하는지를 잡을 거예요. 테스트 케이스에서 충돌을 일으키는 게 아니라. 그럼 다시 테스트에서 그 카테고리 레코드가 남아있는지 아닌지를 이중으로 확인하는 거죠. 끝으로 만들었던 조리법 레코드를 지우고요. 레코드를 지우지 않는다면, 다음 테스트 때 조리법 테이블에 레코드가 있는 카테고리 픽스쳐들은 Rails가 로드를 못하기 때문에 문제가 생길 거예요.

그럼 이제 카테고리 단위테스트를 실행할 준비가 되었군요.
ruby testunitcategory_test.rb 

[그림 7]

Paul: 와!!!

CB: 걱정 말아요. 보기 보다 나쁘지 않으니. 사실, 그건 우리가 원하던 바예요. 우리는 실패 테스트를 하나 얻은 거라고요. 더 가까이, 위에서부터 봐요. test_cannot_delete_record_with_child가 처음에 실행되고 우리 실패하리라 기대하던 바로 그곳, assert_nothing_raised에서 실패했어요. 더 자세하게 들여다보면, 다른 테스트 메소드와 결합한 뒤에 따라 나오는 각각의 에러를 볼 수 있을 거예요. Rails는 failure나 error를 만나면 바로 테스트 메소드의 실행을 멈춰요. 우리가 failure를 만났을 때 Rails는 그 메소드 실행을 멈췄어요. 그건 자식 레코드가 아직 지워지지 않았다는 의미이고, 우리가 이미 아는 것처럼 카테고리 픽스쳐는 로드될 수 없을 거예요. 그것이 이 에러들의 모든 것이죠. 자, failure난 것을 고쳐볼까요! 카테고리 모델을 열어보죠.


[그림 8]

CB: 여기서 우리가 해야 할 것은, 자식 레코드가 없는 레코드를 지우려고 하기 전에 체크를 하는 거예요. 표준 Rails 콜백 중 하나인 before_destroy 를 쓸 거예요. before_ 콜백이 False를 리턴한다면 결합된 액션은 취소되요, 그럼 먼저 콜백을 위한 호출을 추가할게요.
before_destroy :check_for_children
그리고 콜백 메소드를 추가하고요..
def check_for_children
   recipes = Recipe.find_all_by_category_id(self.id)
   if !recipes.empty?
     return false
   end
end
그래서 이렇게 끝났어요..


[그림 9]

CB: 잊어버리기 전에, 다시 문제가 생기지 않도록 데이터베이스에 조리법 레코드가 하나도 없다는 것이 확실하게 해보죠. 조리법 테스트를 다시 실행시키고 teardown 메소드로 모두 지우게 할 수도 있어요. 하지만 이제는 순서대로 실행시키지 않을 때 어떤 일이 생기는지를 봤으니 순서 문제를 생각해야만 해요. 레코드들이 만들어지는 곳이기 때문에 조리법 단위테스트에 teardown 메소드를 넣었죠. 하지만 우리 문제는 조리법 테스트가 끝난 후에 조리법 레코드들이 존재한다는 것이 아니에요. 카테고리 테스트가 시작하는 시점에 조리법 레코드가 존재한다는 것이 우리의 문제죠. 그러니 레코드들을 지우는 코드를 recipe_test.rb에 있는 teardown메소드에서 category_test.rb의 setup 메소드로 옮기는 게 어떨까요?

Paul: 괜찮은 것 같은데요.

CB: 예, 제 생각도 그래요. 그럼 recipe_test,rb에서 category_test.rb로 teardown 메소드를 복사하고 이름을 고칠게요.
def setup
   recipes = Recipe.find(:all)
   recipes.each do |this_recipe|
     this_recipe.destroy
   end
end
조리법 픽스쳐를 추가해야 해요.
fixtures :categories, :recipes
그 다음에 카테고리 테스트를 다시 실행하고요.
ruby testcategory_test.rb
그리고..


[그림 10]

CB: 와다다다!!!

Paul: 진정해요, CB. 겪어본 거잖아요. 이제 우리 애플리케이션이 확실히 고쳐졌는지 어떻게 확인하죠?

CB: 문제없어요, 폴. Mongrel은 여전히 돌아가고 있죠? 좋아요. 앞으로 가서 카테고리 목록 페이지를 열고 다시 하나를 지워봐요.

Paul: 좋아요. 알겠어요. 카테고리가 지워지지 않았고, 충돌도 없어요. 우리가 잘 처리한 것처럼 보이는 군요. 그런데 궁금한 게요. 브라우저에서 테스트할거면, 단위 테스트를 만드는데 왜 시간을 쓴 거죠?

CB: 좋은 질문이에요, 폴. 더 높은 레벨에서 애플리케이션을 바라볼 테스트가 빠졌다는 걸 알겠어요. 높은 레벨에서 문제를 발견했기 때문에 우리가 한 것이 실제로 문제를 고쳤는가를 확신하기 위해서는 그 레벨로 가고 싶었던 거죠. 그렇게 가는 게 매우 일반적일 거예요. 하지만 이제 우리는 그 문제를 잡아내는 낮은 레벨의 테스트를 갖고 있어요. 그럼 높은 레벨에서 그것을 다시 테스트해볼 필요는 없죠. 우리가 원한다면 할 수 있고, 또, 테스트 슈트가 단계별 방어 같은 거라고 생각하기 때문에 저는 아마 하겠지만. 높은 레벨에서 다시 테스트를 하는 것의 가치는 우리 애플리케이션이 망가지지 않았는지를 확인하는 것보다 우리 테스트 슈트가 망가지지 않았다는 것을 확인하는데 근본적인 의미가 있어요. 당신한테 보여주는 게 더 쉬울 거예요. 당신이 준비되면, 기능 테스트(Functional Test)를 하도록 하죠.

Paul: 전 준비됐어요. 시작해요.

CB: 좋아요. 우리를 위해 Rails가 만들어 놓은 기능 테스트 스텁을 보죠. test functionalcategory_controller_test.rb를 열어봐요.


[그림 11]

Paul: 단위 테스트 스캐폴드들보다 훨씬 복잡해 보이는데요. Rails가 애플리케이션 그 자체를 위해 만들어 놓은 스캐폴딩같아요.

CB: 예, 내 생각에 Rails가 단위 테스트 스캐폴드와 더 많은 것을 하지 않는 이유는, 전에도 말했듯이 모델은 유효성 검사와 비즈니스 로직을 넣어두는 곳이기 때문이에요. Rails가 우리가 그것을 갖고 뭘 하고 싶어하는지를 전부다 예측할 수는 없어요. 하지만 컨트롤러를 위한 스캐폴드 코드에서 기본 CRUD 기능을 만들기 때문에 그 기능들에 대한 테스트는 만들 수가 있죠. 우리는 컨트롤러 안의 기본 메소드들 모두에 대한 테스트 메소드를 얻었고 각각의 테스트 메소드 실행을 준비할 setup메소드까지 더해서 가진 거예요.

Paul: 그렇군요. 네 개의 인스턴스 변수들을 설정하는 게 보여요. 하지만 그 중 하나만 사용하는 거 같은데요. 왜 다른 것들은 사용하지 않는거죠?

CB: 폴, 그렇게 말하니 당신이 숙제를 안 해왔다고 생각이 들잖아요. ;-) 지난 번에 당신한테 주었던 링크들을 기억해봐요. 당신의 기억을 새로고침 해줄까요? "Testing the Rails"의 작가 Evan "Rabble" Henshaw-Plath는 이렇게 일컬었어요.


Big 3
setup 메소드에서는 3개의 객체를 만든다.
  • 테스트하는 컨트롤러 (@controller)
  • 웹 요청을 가장하는TestRequest (@request)
  • 테스트 요청에 대한 정보를 제공하는 TestResponse (@response)
당신의 기능 테스트 중 99%는 setup 메소드에 이 3개의 객체를 가질 것이다.

Paul: 맞다. 이제 정말 기억나요, 하지만 “웹 요청을 가장하는” 이라는 말이 무슨 말인지 이해를 잘 못하겠어요. 제 생각에, 테스트 프레임워크가 Mongrel로 요청을 보내고 응답을 기다린다. 이렇게 되는 게 맞아요?

CB: 정확하게는, 아니예요. 전혀 그렇지 않거든요. 지난 번 세션에서 Mongrel이 실행 중인지를 확인했는데, 그래야 당신이 이해할 수 있으니까요. 그리고 다른 프레임워크들도 그런 식으로 작동하고요. 보스가 애플리케이션에서 Ajax를 사용하기를 요구할 지 몰라서 내가 Short Cut을 쓴 Philip Plumlee과 이메일을 교환했던 적이 있었는데. 내 생각에 Phlip(포럼에서 그가 쓰는 필명)이 이에 대해 아주 잘 설명해주었다고 봐요. 그가 말하길..


Rails의 built-in 테스트 프레임워크는 Mock The Webserver 패턴을 따른다.

테스트 프레임워크가 서버를 흉내내지 않는다면, 테스트 프레임워크는 느리고 불투명한 시스템을 흉내 낼 때 생기는 효과들을 잃어버리고 만다.

테스트의 대부분, 특히 TDD테스트들은 코드가 웹서버나 브라우저에 따라 주의를 요하는 코드를 적용하지 않는다. 따라서 모든 테스트들은 가능한 그들의 테스트된 코드들에 가까운 형태로 실행되어야 한다. 다른 웹 플랫폼과 다르게 Rails는 페이지를 HTML로 렌더링하기 위한 공간을 제공한다. 이 말은, 뷰 레벨 테스트는 단지 이 HTML을 파싱하고 테스트된 세부사항들을 확인만 하면 된다는 것이다. 웹 서버를 흉내냄으로써 테스트를 쉽게 만드는 것은 테스트를 더 많이 작세할 수 있게 하고, 더 강력한 환경에서 테스트를 사용할 수 있도록 우리를 돕는다.

Paul: 그럼, 테스트를 실행시키기 위해 Mongrel을 실행시키고 있을 필요가 없다고요?

CB: 없어요. 그것을 증명해보죠. Mongrel을 끄고 윈도우 커맨드 창과 브라우저를 닫아봐요. 그럼 제가 Instant Rails 매니저와 MySQL 서버만 실행시키고 있고, 다른 건 전혀 없죠. 새로운 커맨드 창을 열어서 이 기능 테스트 스캐폴드 코드를 시험해보죠.

Paul: 알았어요.
ruby testfunctionalcategory_controller_test.rb

[그림 14]

Paul: 어엇! 정말 Mongrel을 실행시키지 않아도 되는군요. 그런데 이건 뭐죠? 우리가 코드를 고쳐서 생긴 게 아닌 거 같은데요? 그러니까.. 모든 테스트가 실패한 것 같은데. 우린 카테고리 컨트롤러의 한 개 메소드를 바꿨을 뿐이잖아요.

CB: 아니에요. 이 에러 메시지는 Rails가 first라는 픽스쳐를 찾을 수 없다는 거예요. 조리법 컨트롤러 테스트를 돌렸어도 같은 결과를 얻을 거예요. 이것을 피할 수도 있었지만 한번 당신에게 보여주고 싶었어요. 아직 픽스쳐 이름들에 관해 너무 주의를 기울일 필요가 없어서 걱정 안 한 거예요. 그렇지만 후에 픽스쳐의 이름들은 테스트의 가독성을 위해서 더 중요해질 거예요. 이건 단지 저의 의견이긴 하지만, DHH가 “syntactiv vinegar”라고 부르는 것들 중 하나의 예라고 생각해요. Rails는 단위 테스트와 기능 테스트 둘 다를 같은 시점에 만들고 둘 다 같은 픽스쳐를 사용하죠. 그래서 Rails는 픽스쳐에 “one”, “two”로 이름을 붙이죠. ;-) 제 생각에, 여기서 DHH와 그 팀의 메시지는 이런 거예요. “스캐폴딩은 생각하지 않음을 변명하는 게 아니다. 여러분의 단위 테스트와 기능 테스트에서 같은 픽스쳐를 사용하기를 원하는가? 또는 서로 다른 픽스쳐들이 필요한가? 당신의 의도를 코드가 표현가 위해서 당신이 픽스쳐를 어떻게 이름 지을 것인가?” 이런 종류의 질문들은 중요하고, 가까운 미래에 그것들에 대해 이야기하기 위해 당신과 내가 시간을 쏟을 거예요. 그렇지만 지금 당장은 기초적인 것들을 빨리 하리 위해 테스트 케이스만 고치기로 해요. 괜찮죠?

Paul: 예. 좋은 생각이예요. 적어도 기초는 이해하고 저런 토픽들에 대해 생각해 볼 수 있을 거 같아요.

CB: 좋아요. 고치는 건 쉬워요. 다시 category_controller_test.rb에 있는 setup 메소드를 봐요. 죄인이 코드 아래쪽 줄에 있군요.
@first_id = categories(:first).id
이렇게 바꿀 필요가 있겠어요.
@first_id = categories(:one).id
이름이 없는 카테고리 레코드를 저장하려 하면 모델 유효성체크에서 실패할 거니까, 그걸 피하려면 앞으로 가서 test_create 메소드를 고쳐야겠군요. 이거를..
post :create, :category => {}
다음처럼 바꾸죠..
post :create, :category => {:name => "new category"}
계속...


역자 김탁용님은 연세대학교 컴퓨터과학과를 졸업하고 현재 대한항공 정보시스템실에서 근무하고 있습니다. 무엇인가 새로운 것을 만드는 일, 그로 인해 다른 사람들에게 도움이 되는 일에 관심이 많아 소프트웨어 개발을 계속 하고 있습니다.
TAG :
댓글 입력
자료실