Blog ยป Full Stack Testing a HOBO Application
Posted on 03 Dec 2010 18:10
All testing strategies and dsl covered in one simple Hobo application. Hands on introduction the unit, functional and acceptance tests follow here. If you are only insterested in testing bits then jump directly to that section.
Important Note! Hobo for Rails 3 is still Beta. Therefore in production, I am still using Rails 2…. When Hobo for Rails 3 is out of Beta it will be a heaven. Counting down. Soon… Thus this article and related pages are written on Rails 2.
Quick steps to make a hobo application. Let's have a database of organizations with a name.
|
Table of Contents
|
123, Serve
Goto your console and:
hobo organizations cd organizations script/generate hobo_model_resource Organization name:string
Now edit app/models/organization.rb and enhance the name field definition:
fields do name :string, :required, :unique, :null=>false, :index=>true timestamps end
And continue on the console:
script/generate hobo_migration rake db:migrate script/server
Try your application at http://localhost:3000
Preparation
If you don't have Rails and/or Hobo then….
Do Ruby on the Rails Installation. Then install hobo:
sudo gems install hobo
Hobo Fields API
name :string, :required, :unique
:required and :unique are parameters of Hobo Fields API. They add validations to the field. Note that they only add validations. They do not modify the database.
Hobo Migrations
name :string, :required, :unique, :null=>false, :index=>true
:null=>false and :index=>true are process by Hobo Migration Generator. They modify the database schema! Now :name field has a unique index as well and it cannot be :null.
Migration parameters must follow field API parameters! The code below will not work!
name :string, :required, :null=>false, :unique , :index=>true #Wrong hobo code
Multi field indexes
An example of declaring indexes over multiple fields:
fields do ... end index [:first_name, :last_name], :unique=>true
In addition to the ability to specify an index per field, you can add indexes over multiple fields to ensure uniqueness.
Your First DRYML
#TODO
Populating Database
You can use FactoryGirl to populate the databse during development. You will need Factories for test. But I don't see any reason that you cannot use them in development environment also.
First create those 2 factory files:
#test/factories/user.rb require 'factory_girl' Factory.define :admin, :class=> User do |u| u.sequence(:name ){|n| "admin#{n}"} u.sequence(:email_address ){|n| "admin#{n}@test.com"} u.password 'pass' u.administrator true end Factory.define :user do |u| u.sequence(:name ){|n| "user#{n}"} u.sequence(:email_address ){|n| "user#{n}@test.com"} u.password 'pass' u.administrator false end
#test/factories/organzation.rb require 'factory_girl' Factory.define :organization do |m| m.sequence(:name) {|n| "Test Organization #{n}"} end
Then a rake task to populate your database:
#lib/tasks/app_populate.rake namespace :app do desc 'Rebuild the database from scratch' desc 'Fill database with test data' task :populate=> :environment do Rake::Task["db:reset"].invoke require 'factory_girl' Dir['test/factories/*.rb'].each { |f| load f } p "Manifacturing Objects..." (1..2).each { Factory(:organization) } end end
You can search for rake tasks:
rake -T populate
You should see the newly defined task now.
Now go and type the command:
rake app:populate
Fire your server and browse to http://localhost:3000
script/server
Now you will see the populated application to try.
This way you are trying out data to use in tests also.
Below I will present repeating test patterns so that you can repeat them in your projects. Like above, you need to use the source as it is only.
Testing
- Test are like declarations in typed language. I can make specification. Only authors and admins can edit blog posts. Blog titles are unique. An author can create/read/update/delete a blog entry, etc. They are the declarations about your application!
- They provide a better environment for debugging then console. It you'll repeatedly execute something then thats a test.
We will go over those testing strategies
- Unit tests
- Functional tests
- Acceptance tests
Each has its own powerful points and better suits to certain tasks.
After experiencing the strategy here, I hope you will be able to repeat it in other projects leading to test-first-implementation.
That's a concern if one does not know the testing DSLs then one cannot go test-first (Extreme Programming)
Preparing Test Environment
First follow deleting-all-records-in-rails
Then follow environments-testing to make the necessary changes to project configuration files… In time you'll need all those config changes and doing them one by one is a loss of time.
Make sure that you've initialized the steak environment as described in steak-capybara-in-rails-2-3 executing this bit:
script/generate rspec #Initialize rspec script/generate steak --capybara #Initialize steak
Then execute
sudo rake gems:install RAILS_ENV=test
Now your machine is configured for testing
Unit Tests
Edit test/unit/organization_test.rb:
#test/unit/organization_test.rb require File.dirname(__FILE__) + '/../test_helper' #See # https://github.com/thoughtbot/shoulda class OrganizationTest < ActiveSupport::TestCase context "ActiveRecord" do setup { Factory(:organization)} should validate_uniqueness_of( :name ) end context "An Organization" do setup { @organization = Factory(:organization)} should "do something" should "do anotherthing" end end
context "ActiveRecord" is declarations about your model. Always provide this context complete with declarations about your model. Refer to https://github.com/thoughtbot/shoulda for all ActiveRecord tests
context "Organization is just an example of how you should phrase your tests.
Execute
rake unit:tests
You'll see DEFERRED: An Organization should do something.
So a context is a the subject of your test. Then you say what it should do. Always start with empty should blocks s to check that it reads correctly.
You've executed your first test and verified the declaration of the model.
Designing Functional Tests
Same principle again. Start with empty blocks and make sure the test reads correctly:
Edit test/functional/organizations_controller_test.rb
require File.dirname(__FILE__) + '/../test_helper' #See # https://github.com/thoughtbot/shoulda # http://guides.rubyonrails.org/testing.html#functional-tests-for-your-controllers # GET /items #=> index # GET /items/1 #=> show # GET /items/new #=> new # GET /items/1/edit #=> edit # PUT /items/1 #=> update # POST /items #=> create # DELETE /items/1 #=> destroy class OrganizationsControllerTest < ActionController::TestCase context "Security: " do setup { @organization = Factory(:organization) @attrs = Factory.attributes_for(:organization) } context "Guest" do #setup {login_as somebody} context "(read_actions)" do should "get index" should "get show" end context "(edit actions)" do should "not get new" should "not get edit" end context "(write_actions)" do should "not post create" should "not put update" should "not delete" end end end end
Execute:
rake test:functionals
And check how the test reads. Got the idea?
Securing the Site Using Functional Tests
Edit test/functional/organizations_controller_test.rb again:
require File.dirname(__FILE__) + '/acceptance_helper' #See # https://github.com/thoughtbot/shoulda # http://guides.rubyonrails.org/testing.html#functional-tests-for-your-controllers # GET /items #=> index # GET /items/1 #=> show # GET /items/new #=> new # GET /items/1/edit #=> edit # PUT /items/1 #=> update # POST /items #=> create # DELETE /items/1 #=> destroy class OrganizationsControllerTest < ActionController::TestCase context "Security: " do setup { @organization = Factory(:organization) @attrs = Factory.attributes_for(:organization) } context "Guest" do #setup {login_as somebody} context "(read_actions)" do should "get index" do get :index assert_response :success end should "get show" do get :show, :id=>@organization.id assert_response :success end end context "(edit actions)" do should "not get new" do get :new assert_response :success assert_no_tag :tag=>'form' end should "not get edit" do get :edit, :id=>@organization.id assert_response :success assert_no_tag :tag=>'form' end end context "(write_actions)" do should "not post create" do count1 = Organization.count post :create, :organization => @attrs count2 = Organization.count assert_equal count1, count2, "Nothing created" assert_response :forbidden end should "not put update" do put :update, :id=>@organization.id, :organization => @attrs assert_response :forbidden end should "not delete" do delete :destroy, :id=>@organization.id assert_response :forbidden end end end end end
and execute:
rake test:functionals
You will see that there is a failing test. Follow http://conceptspace.wikidot.com/blog:hobo-security-hole and try again.
You never know if your security will fail because of an upgrade or changes in your system. You can never assume your application is secure because you've coded the right way.
Provide a 'context "Security"' for all your controller tests and use other contexts for your other tests. This way context "Security" can be peer reviewed easily. We are checking the security there. And it repeats almost the same for different controllers.
So functinal tests are a perfect place to verify authentication constraints. This way you are declaring the existence of actions also.
Note that the actions are grouped as edit, read and write. This way if you have to define a custom action, you can add the it to the related group and repeat the same test pattern. All actions in the same group are tested almost the same way.
You've declared the authentication constraints of your application and proved that your application is secure.
Ruby Debugger
While running all kinds of tests you can use the rails debugger to solve your problem
Edit models/organization.rb
require File.dirname(__FILE__) + '/acceptance_helper' class Organization < ActiveRecord::Base hobo_model # Don't put anything above this fields do name :string, :required, :unique, :null=>false, :index=>true timestamps end #index [:name], :unique=>true # --- Permissions --- # def create_permitted? # puts "create_permitted? #{acting_user.administrator?}" debugger #THIS IS THE LINE TO ADD TO INVOKE DEBUGGER acting_user.administrator? end def update_permitted? acting_user.administrator? end def destroy_permitted? acting_user.administrator? end def view_permitted?(field) true end end
And execute again:
rake test:functionals
You will end up in the debugger.
Type 'help' or 'help command' to learn about the facilities.
Most frequently you'll use those commands:
l=
Show current line
n
Execute next line
s
Step into next line
h
Help
bt
See the call stack
pp self
Pretty print the current object
Designing Acceptance Tests
Edit spec/acceptance/organization_crud_spec.rb
require File.dirname(__FILE__) + '/acceptance_helper' #See # https://github.com/cavalle/steak # https://github.com/jnicklas/capybara # https://github.com/thoughtbot/factory_girl feature "Organization CRUD", %q{ As an Admin I want to CRUD organizations } do background {@attrs = Factory.attributes_for(:organization)} describe "When admin, " do background do @admin = Factory(:admin) login_as(@admin) end describe "With nothing, " do describe "At list, " do background {visit '/organizations'} scenario "I browse" scenario "I create" end end describe "With a record, " do background {@organization = Factory(:organization)} describe "At list, " do background {visit '/organizations'} scenario "I browse" describe "At show, " do background do within('.collection.organizations .card.organization') do click_link @organization.name end end scenario "I read" describe "At edit, " do background {find('.edit-link.organization-link').click} scenario "I update" scenario "I delete" end end end end end # describe "When guest, " do # end end
and execute
rake spec:acceptance
Same principle again. When designing your own tests. Use empty scenarios to see that it reads correctly.
Describing User Experience
Again edit spec/acceptance/organization_crud_spec.rb
require File.dirname(__FILE__) + '/acceptance_helper' #See # https://github.com/cavalle/steak # https://github.com/jnicklas/capybara # https://github.com/thoughtbot/factory_girl feature "Organization CRUD", %q{ As an Admin I want to CRUD organizations } do background {@attrs = Factory.attributes_for(:organization)} describe "When admin, " do background do @admin = Factory(:admin) login_as(@admin) end describe "With nothing, " do describe "At list, " do background {visit '/organizations'} scenario "I browse" do page.should_not have_content(@attrs[:name]) end scenario "I create" do find('.new-link.new-organization-link').click fill_in 'organization[name]', :with => @attrs[:name] click_button 'Create Organization' visit('/organizations') within('.collection.organizations .card.organization') do page.should have_content @attrs[:name] end end end end describe "With a record, " do background {@organization = Factory(:organization)} describe "At list, " do background {visit '/organizations'} scenario "I browse" do within('.collection.organizations .card.organization') do page.should have_content(@organization.name) end page.should_not have_content(@attrs[:name]) end describe "At show, " do background do within('.collection.organizations .card.organization') do click_link @organization.name end end scenario "I read" do page.should have_content @organization.name end describe "At edit, " do background {find('.edit-link.organization-link').click} scenario "I update" do fill_in 'organization[name]', :with => @attrs[:name] click_button 'Save' visit('/organizations') within('.collection.organizations .card.organization') do page.should have_content @attrs[:name] end end scenario "I delete" do find('.delete-button').click visit('/organizations') page.should_not have_css('.collection.organizations .card.organization') page.should_not have_content(@organization.name) end end end end end end # describe "When guest, " do # end end
*The principle: * We are doing CRUD (create read update delete) on our objects. In addition to other features always have a crud_spec.rb for any object you have. This is a pattern to repeat for all projects and objects…
Be aware, we are not concerned about security here! We are not specifying what one cannot do! It's too verbose and too long.
We are concerned with what can be done! We have specified security constraints already in functional tests. We are specifying here what a certain type of user is doing to use our system. It's usage scenarios.
Also we are not specifying model constraints here. It's handled in unit tests. Organizations have a unique name. It's already done.
Here we are proving that the feature exists.
Here we are proving that the user can use the system as intended.
If something is beyond crud, security and object constraints then you have worthy feature do describe. Go and make your new feature spec file then.
Repeating the Pattern
The principles so far are
- In unit tests, always have context "ActiveRecord" and declare your model constraints there.
- In functional tests, always have a context "Security" and verify access rights there.
- In acceptance tests, specify the positive user experience. Negative user experience is specified elsewhere. The exception to this is you need to verify that the user is getting a custom user interface on negative experience. But this is another feature… Saying that the user sees such a message when names are unique is altogether another feature.
Finally running
rake test rake spec::acceptance
executes all the tests. This is what should be done
- After checking out a new project
- Before merging someone else's branch
- After an upgrade in your computer and libraries
And of course make sure that somebody is not deceiving you by providing empty tests.
Even if you run out of imagination, always use the repeating test pattern above. This guarantees that the system is running always.
Now you can go ahead to your own projects and do them "test-first"
References
Testing References
- deleting-all-records-in-rails
- environments-testing
- steak-capybara-on-rails-2-3
- Using Capybara with Steak
- Steak
- Capybara
- Shoulda
- Mocha
- Factory Girl
- Rack Test
If you like this page, please spread the word:
You can contact me if you have questions or corrections.


