Rspec, SQLite3와 Fixture를 이용한 Rails API 테스트
어떤 산업군에서든지 퀄리티가 좋은 제품을 생산한다는 것은 어쩌면 얼마나 견고한 테스트를 거쳤냐는 것을 뜻하는 것일 수 있습니다. 시간과 인력이 부족하다는 이유로 테스트를 경미하게 생각하거나, 이를 무시하고 출시하고 나서 나중에 예상치 못한 결함으로 오히려 더 큰 시간적 재무적인 손실을 보게 되는 경우를 종종 접하게 됩니다 . 웹/앱, API등의 소프트웨어를 만드는 일도 마찬가지라고 생각합니다. 여러 다른 component에 dependency가 있고 예상치 못하는 트래픽이 발생하는 등 언제 어디에서도 실패의 원인은 존재 할 수 있기 때문에 처음부터 100% 완벽한 product를 만드는 일은 거의 불가능 하다고 봅니다. 그럼에도 불구하고, 개발자로서 본인이 구현한 project에 대한 테스트 케이스를 작정하고 이성적인 테스트를 작성하는 것은 어쩌면 개발자가 가져야 하는 최소한의 미덕이 아닐까 싶습니다.
물론 초기 개발비용을 무시할 수 있는 것은 아니지만, 잘 짜여진 테스트는 단지 제품을 견고하게 만드는 데 일조하는 것 뿐만 아니라 자신의 코드를 누군가에게 이해시키기에 어떤 문서나 데모보다도 확실한 도구가 됩니다.
그럼 이쯤에서 Rails에서는 어떻게 테스트 환경을 구축할 수 있는지 간단한 API test를 통해서 알려드리고자 합니다.
우선 제가 선택한 test 도구들은 다음과 같습니다.
- ActiveRecord model (No migration)
- Sqlite3
- Fixtures
- Rspec
필요한 gem들을 Gemfile에 추가합니다.
1 2 3 4 5 6
# Gemfile group :test do gem 'sqlite3' gem 'rspec-rails' gem 'oj' #to load JSON response end
기본적으로 Rails project를 개발하게 되면 ActiveRecord 모델, Migration을 통한 DB 테이블 생성등을 떠올리게 되는데요, 조금 복잡하고 큰 프로젝트를 경험하다 보니 하나의 Master DB를 여러 프로젝트에서 공유하는 것을 종종 볼 수가 있습니다. 서로 다른 project에서 각자 필요한 table들을 하나의 Master DB에 생성하게 되는데요, 이런 경우에 migration을 통한 DB관리가 힘들어 지게 되어 이번 글을 통해 migration이 없는 프로젝트의 test 환경 구축에 대해 설명을 드리도록 하겠습니다.
우선 migration이 없는 환경이기 때문에 db/schema.rb 파일이 존재하지 않습니다.
이럴경우에는 필요한 데이터의 DB schema를 보고 직접 schema.rb 파일을 작성해야 합니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
ActiveRecord::Schema.define(version: xxxxxxxx) do create_table "comments", force: :cascade do |t| t.integer "post_id", limit: 4 t.integer "user_id", limit: 4 t.string "title", limit: 255 t.text "comment_body", limit: 65535 t.datetime "created_at", null: false t.datetime "updated_at", null: false end create_table "posts", force: :cascade do |t| t.integer "user_id", limit: 4 t.string "title", limit: 255 t.text "post_body", limit: 65535 t.integer "rank", limit: 4 t.datetime "created_at", null: false t.datetime "updated_at", null: false end create_table "users", force: :cascade do |t| t.string "first_name", limit: 255 t.string "last_name", limit: 255 t.string "nick_name", limit: 255 t.datetime "created_at", null: false t.datetime "updated_at", null: false end end
Migration을 통해 생긴 schema.rb와 동일하게 작성하시면 됩니다.
rake db:test:prepare 명령어를 부르면 database.yml에 다음과 같이 정의되어 있는 DB 정보를 가지고 test.sqlite3 라고 하는 database를 생성하게 됩니다.
1 2 3 4 5 6 7
#database.yml test: adapter: sqlite3 encoding: utf8 pool: 5 timeout: 5000 database: db/test.sqlite3
데이터를 담을 그릇이 준비 되었으니 이제는 테스트의 기반이 되는 데이터를 생성해야 하는데요, Rails에서는 fixture라고 하는 yml 형태의 파일을 쓰는 방법과, factory-girl gem 을 사용하는 방법으로 크게 나뉘는데, 이부분은 다들 호불호가 있고, 장단점이 있는 부분이라고 생각하지만 개인적으로는 fixture를 사용하고 있어 이방법으로 설명드리려고 합니다.
Fixture의 namining convention은 모델의 복수형태 즉 db table 의 이름과 동일하다고 보시면 됩니다. (예: Post < ActiveRecord::Model 의 fixture의 경우 posts.yml )
1 2 3 4 5 6 7 8 9
# spec/fixtures/posts.yml post: id: 1 user_id: 1 title: "Nice Spring" post_body: "Spring is nice and warm" rank: 1 created_at: "2015-12-17 03:10:15" updated_at: "2016-05-03 11:01:50"
사실 테스트에서 제일 중요하고 시간이 많이 가는 부분이 이 fixture를 생성하는 부분입니다. 견고한 데이터 없이 진행되는 테스트는 거의 무의미 하다고 생각하기 때문에 이부분은 시간을 가지고 공을 들여서 작성해야 합니다.
데이터가 완성되었으면 이제 실제로 해당 API에 대한 test 작성을 해야 하는데요, 테스트를 돌리기 전에 spec_helper.rb파일과 rails_helper.rb 파일에 필요한 도구와 데이터들을 로딩시키기 위해 다음과 같은 configuration을 추가 합니다.
schema.rb가 업데이트 되면 테스트를 수행하기 전에 DB 업데이트를 하기위해 spec_helper 에 다음 라인들을 추가합니다.
1 2 3 4 5 6 7
#spec_helper.rb RSpec.configure do |config| silence_stream STDOUT do load "#{Rails.root}/db/schema.rb" end .... end
rails_helper에는 fixture들을 로딩하는 정보를 추가 합니다.
1 2 3 4 5 6 7
# rails_helper.rb require 'spec_helper' ... RSpec.configure do |config| config.fixture_path = "#{::Rails.root}/spec/fixtures" ... end
1 2 3 4 5 6
# rake routes api_posts GET /api/posts(.:format) api/posts#index {:format=>:json} api_user_posts GET /api/posts/:user_id(.:format) api/posts#get_user_posts {:format=>:json} api_users GET /api/users(.:format) api/users#index {:format=>:json} api_comments GET /api/comments(.:format) api/comments#index {:format=>:json} api_post_comments GET /api/comments/:post_id(.:format) api/comments#get_post_comments {:format=>:json}
Api 관련 routes인데요, 위에 열거된 API 중 기본적으로 posts 전체를 가져오는 api 테스트를 구현해 보도록 하겠습니다.
여기서 중요한 것은 json 형태로 돌아오는 response 값인데요, API 에서 jbuilder를 이용한 json view template을 사용했기 때문에 rails_helper에 추가적으로 view template을 통해서 응답을 받겠다고 명시해야 합니다.
1 2 3 4 5 6
# rails_helper.rb RSpec.configure do |config| config.fixture_path = "#{::Rails.root}/spec/fixtures" config.render_views = true ... end
1 2 3 4 5 6 7 8 9 10 11 12 13 14
# spec/controllers/api/posts_controller_spec.rb require 'rails_helper' describe Api::PostsController, :type => :controller do fixtures :all describe 'GET #index' do it 'returns all posts ' do get :index, format: :json results = Oj.load(response.body, symbol_keys: true) expect(results[:status]).to eq(0) expect(results[:posts].length).to eq(2) expect(results[:posts][0][:title]).to eq('Nice Spring') end end end
실제 API 를 통해서 돌아오는 response 값은 다음과 같습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14
{ status: 0, posts: [ { id: 1, user_id: 1, title: "Nice Spring", post_body: "Spring is nice and warm", rank: 1, created_at: "2015-12-17 03:10:15", updated_at: "2016-05-03 11:01:50" } ] }
물론 현실에서의 API와 테스트는 훨씬더 복잡하고 생각해야 할 문제들이 많이 있기에 이 글에서 소개 한것 처럼 간단하게 끝이 나지는 않을것입니다. 그럼에도 이글을 쓰는 목적은 누군가 테스트 환경을 구축하고 첫번째 스텝을 밟아 좀 더 견고한 API를 만들기 위해 노력하고, 그로인해 개발자들이 API를 구현하게 될때 테스트를 먼저 생각하는 것이 당연한 일이 되고, Continuous Integration과 100% test coverage 는 언제나 뗄래야 뗄 수 없는 관계라고 인식하는 개발 사이클을 경험했으면 하는 바램입니다.
Happy Testing!!!
Popit은 페이스북 댓글만 사용하고 있습니다. 페이스북 로그인 후 글을 보시면 댓글이 나타납니다.