TDD & BDD Best Practices with RSpec in Creating Web Service
- TDD Process
Hi everyone, I think that many of you have heard about TDD, BDD. Although you may appreciate this process of software development, you may not have opportunity to work with the process. Today, I want to give you an overview of TDD, and a tool called RSpec for behavior testing in Ruby. Let’s start!
I. Overview
1. TDD
TDD stands for ‘Test-driven development’, a software development process in which we repeat the cycle: Write test first → Code → Refactoring through the life time of project.
2. BDD
TDD stands for ‘Behavior-driven development’ which bases on the core concept of TDD. You write unit tests, acceptance tests, integration tests, etc to check how your application behaves with requirements. However, you must do the same process as TDD. Thus, there’s no clearly difference between TDD an BDD. Writing correct tests in TDD means you are following the BDD. Reversely, doing right BDD means you are doing TDD.
You can find so many results of the two concepts on Internet, in fact, they are the same. It is an efficient process of developing a software, to have a clearly understanding of requirements, good designs, and minimal bugs. Write tests first, then code, then refactoring.
3. RSPEC
Spec is a testing tool for Ruby programming language. RSpec is created under purpose of creating products in BDD process.
RSpec provides:
- Rich terminal commands
-
Automation testing
-
Textual descriptions which is almost the same with how people think
4. Benefits
- Understand more clearly about requirements
- Help developers improve code design
- Focus on function, output expectations, then validations
- Reduce significantly bugs
- Ensure functions are connected together in the right way (by integration test)
- Make source code more maintainable, more adaptable with changes, more flexible.
- Create more freedom for developers to improve source code, change requirements, merge with other features …
II. Practical RSpec in creating a web service
1. Describe method: short and meaningful
Ex1:
1 2 3 4 5 6 7 8 9 |
describe 'Users#index' do describe 'invalid cases' do describe 'data' do it 'user id' do expect sth.to eql sth end end end end |
Ex2:
1 |
describe 'admin?' |
2. Single expectation test
1 2 3 4 5 6 7 8 9 |
describe ‘valid’ do before do … end it {expect response.to be_ok} it ‘response format’ do … check response end end |
3. Check response format
1 2 3 4 5 6 7 8 9 10 11 |
describe ‘valid’ do before do … @response = JSON.parse(response.body) end it ‘response format’ do arr_res_fields.each do |key| @response.should be_has_key(key) end end end |
4. Check data
1 2 3 4 5 6 7 8 9 10 |
describe ‘valid’ do before do … @response = JSON.parse(response.body) end it ‘check returned data’ do expect @response[status].to eql(1) expect @response[message].to eql(‘ok’) end end |
5. Validate params
1 2 3 4 5 6 7 8 |
describe ‘invalid’ do it ‘category’ do [nil, 'a', 4].each do |value| get ‘products’, {category: value}, HEADER expect (response.status).to eql(:bad_request) end end end |
6. Use let & subject
1 2 3 |
let(:account) { Account.create(name: ‘Colin’)} subject {account} it {should respond_to(:name)} |
7. Create data when needed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
describe ‘CRUD’ do let(:user) {User.create(email: ‘colin@test.com‘)} describe ‘create’ do before do post ‘users’, {email: ‘colin@test.com‘}, HEADER @response = JSON.parse(response.body) end it {expect(response.status).to eql(:bad_request)} it {expect(@response)['message']).to eql(‘email used’)} end end describe ‘CRUD’ do let(:user) {User.create(email: ‘colin@test.com‘)} let(:admin) {Administrator.create(email: ‘admin@test.com’)} describe ‘Delete’ do before do params = {auth_token: admin.authentication_token} delete ‘users/#{user.id}’, params, HEADER @response = JSON.parse(response.body) end it {expect(response.status).to eql(:no_content)} end end |
8. DRY: shared examples
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 26 27 28 29 30 |
shared_example for ‘listing users’ do |response| it ‘response format’ do ['status','message', 'data'].each do |key| expect response.to be_has_key(key) end end it ‘data format’ do ['account_id', 'name', 'avatar_thumb'].each do |key| expect response['data'].to be_has_key(key) end end it {expect response.to be_ok} it {expect response['status'].to eql(1)} it {expect response['message'].to eql(0)} end describe ‘friends#index list’ do before do get ‘users/user_id/friends end it_behaves_like “listing users”, response end describe ‘followers#index’ do before do get ‘users/user_id/followers’ end it_behaves_like “listing users”, response it ‘other custom validations’ end |
9. Use FactoryGirl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
# factories/user.rb FactoryGirl.define do factory :user do first_name “John” last_name “Doe” admin false end factory :admin, class: User do first_name “Admin” last_name “User” admin true end end # Spec let(:user) {FactoryGirl.create(:user)} let(:admin) {FactoryGirl.create(:admin)} |
10. Integration Test
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
describe ‘users#update’ do before do get ‘users/index’, {page: 1} # cache users put ‘users/user_id’, params = {…} # success end describe ‘check update success’ # pending spec describe ‘index user’ do before do get ‘users/index’, {page: 1} end it ‘should reset cache after updating user’#pending end end |
11. Code Coverage (Ex: simplecov for Ruby application)
