giant robots smashing into other giant robots

Written by thoughtbot

hrward

How to test Sinatra based Web Services

Sinatra is a fantastic lightweight framework for building web services. We’ll use it as the application framework for the HTTP endpoints in our Service Oriented Architecture.

The testing approach will be in-process, which means that the test suite is running in the same Ruby process as the web service. This eliminates the need to run an external HTTP web server.

Application structure

Unlike Rails, Sinatra isn’t all that opinionated on how you set up your application (it has a few sensible defaults), which can lead to a lot of open questions on how to structure the application.

Here’s the directory structure we’ll use for our example application.

app/
app/models
app/my_service.rb
client/
client/lib/my_client.rb
client/my_client.gemspec
config/
spec/

Outside-in development with a Client Gem

For internal services we’ll build a client gem directly into the project. With the client embedded in our codebase we can follow outside-in development cycles — Features start with request specs from the client side, followed by the addition of end points to the service, and then unit tests within the application.

This allows the us to dogfood our client gem and bring it in as the first step of our development process.

To achieve this we need to configure a few things.

1. Set up the project gemfile to use a local copy of the client in test mode

Include the local client gem in test suite.

# Gemfile
group :test do
  gem 'my_client', path: 'client'
  gem 'webmock'
  # ...
end

2. Use webmock to send all http requests to the service

Instead of booting up a web server every time the test suite is run we’ll mount the Sinatra service as a rack application with webmock.

This allows the client to talk directly to the mounted rack application without going through HTTP or a web server.

# spec/spec_helper.rb
RSpec.configure do |config|
  config.include WebMock::API

  config.before(:each) do
    MyClient.base_url = 'http://www.example.com'
    stub_request(:any, /www.example.com/).to_rack(MyService)
  end
end

3. Use the client in our request specs

Once MyService is mounted as a rack application we can use the client gem directly in our test suite.

# spec/requests/widget_management_spec.rb
require 'spec_helper'

describe "Widget management" do
  it "creates a Widget" do
    # set up fixture data if needed

    response = MyClient::Widget.create(widget_params)

    # assert expectations on the response
  end
end

Private gem hosting

To use the client gem in other projects we can use a private gem hosting service like Gemfury. This allows us to include the client via gemfile in our other projects.

# Gemfile
source 'https://452f6E403CDph10714e41@gem.fury.io/me/'
gem 'my_client'

source 'https://rubygems.org
# ...

Takeaways

  • Sinatra is great framework for creating lightweight web services.
  • Webmock allows us to test the client in-process against a rack application.
  • Use private gem hosting to distribute the shared client.

Written by Harlow Ward

jayroh

Fight back UTF-8 Invalid Byte Sequences

Chances are, some of you have run into the issue with the invalid byte sequence in UTF-8 error when dealing with user-submitted data. A Google search shows that my hunch isn’t off.

Among the search results are plenty of answers—some using the deprecated iconv library—that might lead you to a sufficient fix. However, among the slew of queries are few answers on how to reliably replicate and test the issue.

In developing the Griddler gem we ran into some cases where the data being posted back to our controller had invalid UTF-8 bytes. For Griddler, our failing case needs to simulate the body of an email having an invalid byte, and encoded as UTF-8.

What are valid and invalid bytes? This table on Wikipedia tells us bytes 192, 193, and 245-255 are off limits. In ruby’s string literal we can represent this by escaping one of those numbers:

> "hi \255"
 => "hi \xAD"

There’s our string with the invalid byte! How do we know for sure? In that IRB session we can simulate a comparable issue by sending a message to the string it won’t like - like split or gsub.

> "hi \255".split(' ')
ArgumentError: invalid byte sequence in UTF-8
    from (irb):9:in `split'
    from (irb):9
    from /Users/joel/.rvm/rubies/ruby-1.9.3-p125/bin/irb:16:in `<main>'

Yup. It certainly does not like that.

Let’s create a very real-world, enterprise-level, business-critical test case:

invalid_byte_spec.rb

require 'rspec'

def replace_name(body, name)
  body.gsub(/joel/, name)
end

describe 'replace_name' do
  it 'removes my name' do
    body = "hello joel"

    replace_name(body, 'hank').should eq "hello hank"
  end

  it 'clears out invalid UTF-8 bytes' do
    body = "hello joel\255"

    replace_name(body, 'hank').should eq "hello hank"
  end
end

The first test passes as expected, and the second will fail as expected but not with the error we want. By adding that extra byte we should see an exception raised similar to what we simulated in IRB. Instead it’s failing in the comparison with the expected value.

1) replace_name clears out invalid UTF-8 bytes
   Failure/Error: replace_name(body, 'hank').should eq "hello hank"

     expected: "hello hank"
          got: "hello hank\xAD"

     (compared using ==)
   # ./invalid_byte_spec.rb:17:in `block (2 levels) in <top (required)>'

Why isn’t it failing properly? If we pry into our running test we find out that inside our file the strings being passed around are encoded as ASCII-8BIT instead of UTF-8.

[2] pry(#<RSpec::Core::ExampleGroup::Nested_1>)> body.encoding
=> #<Encoding:ASCII-8BIT>

As a result we’ll have to force that string’s encoding to UTF-8:

it 'clears out invalid UTF-8 bytes' do
  body = "hello joel\255".force_encoding('UTF-8')

  replace_name(body, 'hank').should_not raise_error(ArgumentError)
  replace_name(body, 'hank').should eq "hello hank"
end

By running the test now we will see our desired exception

1) replace_name clears out invalid UTF-8 bytes
   Failure/Error: body.gsub(/joel/, name)
   ArgumentError:
     invalid byte sequence in UTF-8
   # ./invalid_byte_spec.rb:4:in `gsub'
   # ./invalid_byte_spec.rb:4:in `replace_name'
   # ./invalid_byte_spec.rb:17:in `block (2 levels) in <top (required)>'

Finished in 0.00426 seconds
2 examples, 1 failure

Now that we’re comfortably in the red part of red/green/refactor we can move on to getting this passing by updating our replace_name method.

def replace_name(body, name)
  body
    .encode('UTF-8', 'binary', invalid: :replace, undef: :replace, replace: '')
    .gsub(/joel/, name)
end

And the test?

Finished in 0.04252 seconds
2 examples, 0 failures

For such a small piece of code we admittedly had to jump through some hoops. Through that process, however, we learned a bit about character encoding and how to put ourselves in the right position—through the red/green/refactor cycle—to fix bugs we will undoubtedly run into while writing software.

dancroak

Improving Rails boot time with Zeus

Zeus improves Rails boot time. Saving seconds is most important when running focused tests:

rspec spec/models/user_spec.rb
rspec spec/models/user_spec.rb:123

Those are times when a tight feedback loop make a meaningful difference.

Install

Install the Zeus gem on your machine:

gem install zeus

Do not include it in your Gemfile. It is an external piece of software.

Set up

Initialize:

zeus init

This will create two files in your Rails app’s directory. Ignore them globally in ~/.gitignore:

custom_plan.rb
zeus.json

Edit zeus.json to include only the tasks for which you’ll use Zeus. Mine looks like this:

{
  "command": "ruby -rubygems -r./custom_plan -eZeus.go",

  "plan": {
    "boot": {
      "default_bundle": {
        "development_environment": {
          "prerake": {"rake": []},
          "console": ["c"],
          "generate": ["g"]
        },
        "test_environment": {
          "test_helper": {"test": ["rspec"]}
        }
      }
    }
  }
}

I remove cucumber in favor of RSpec and Capybara. I remove server in favor of Foreman and Pow.

Force the test environment

In spec/spec_helper.rb, change:

ENV['RAILS_ENV'] ||= 'test'

To:

ENV['RAILS_ENV'] = 'test'

Remove auto-running code

The goal is to run tests in the context of Zeus. So, remove other similar systems.

From the RSpec docs:

> Generally, life is simpler if you just use the rspec command. If you > must use the ruby command, however, you’ll want to do the following:

require 'rspec/autorun'

> This tells RSpec to run your examples.

We don’t need this behavior and can cause bugs when used with Zeus.

Remove either of these lines in spec/spec_helper.rb if they exist:

require 'rspec/autorun'
require 'rspec/autotest'

Remove Spork and Guard

For the same reasons, if you’re using Spork and Guard, delete them from your Gemfile, delete your Guardfile, and delete any related Spork code in spec/spec_helper.rb or spec/support/.

Start Zeus

Zeus will need to be running before you can use its commands:

zeus start

I usually run this, and other long-running processes in a tmux session.

Now, those original commands will have the benefit of Rails boot time in under a second:

zeus rspec spec/models/user_spec.rb
zeus rspec spec/models/user_spec.rb:123

Bonus: run specs from vim

Many of us are running specs directly from vim. If you edit your ~/.vimrc to use Zeus like in this commit, you can run focused specs with:

t

Enjoy!

Written by .

jdclayton

Better Acceptance Tests with Page Objects

During my Test-Driven Rails workshop earlier this week (which is also available as an online workshop), my students and I were writing acceptance tests surrounding marking todo items as complete. The spec looked like this:

feature 'Manage todos' do
  scenario 'view only todos the user has created' do
    sign_in_as 'other@example.com'
    create_todo_titled 'Lay eggs'
    sign_in_as 'me@example.com'
    user_should_not_see_todo_titled 'Lay eggs'
  end

  scenario 'complete my todos' do
    sign_in_as 'person@example.com'
    create_todo_titled 'Buy eggs'
    complete_todo_titled 'Buy eggs'
    user_should_see_completed_todo_titled 'Buy eggs'
  end

  scenario 'mark my todos incomplete' do
    sign_in_as 'person@example.com'
    create_todo_titled 'Buy eggs'
    complete_todo_titled 'Buy eggs'
    mark_incomplete_todo_titled 'Buy eggs'
    user_should_see_incomplete_todo_titled 'Buy eggs'
  end

  def create_todo_titled(title)
    click_link 'Create a new todo'
    fill_in 'Title', with: title
    click_button 'Create'
  end

  def user_should_see_todo_titled(title)
    within 'ol.todos' do
      expect(page).to have_css 'li', text: title
    end
  end

  def user_should_see_completed_todo_titled(title)
    within 'ol.todos' do
      expect(page).to have_css 'li.complete', text: title
    end
  end

  def user_should_see_incomplete_todo_titled(title)
    within 'ol.todos' do
      expect(page).not_to have_css 'li.complete', text: title
    end
  end

  def user_should_not_see_todo_titled(title)
    within 'ol.todos' do
      expect(page).not_to have_css 'li', text: title
    end
  end

  def complete_todo_titled(title)
    todo = Todo.where(title: title).first
    within("[data-id='#{todo.id}']") { click_link 'Complete' }
  end

  def mark_incomplete_todo_titled(title)
    todo = Todo.where(title: title).first
    within("[data-id='#{todo.id}']") { click_link 'Incomplete' }
  end
end

There’s a handful of things that can probably be refactored in the helper methods, but that’s not what I wanted to focus on; check out the scenarios themselves:

scenario 'create a new todo' do
  sign_in_as 'person@example.com'
  create_todo_titled 'Buy eggs'
  user_should_see_todo_titled 'Buy eggs'
end

scenario 'view only todos the user has created' do
  sign_in_as 'other@example.com'
  create_todo_titled 'Lay eggs'
  sign_in_as 'me@example.com'
  user_should_not_see_todo_titled 'Lay eggs'
end

scenario 'complete my todos' do
  sign_in_as 'person@example.com'
  create_todo_titled 'Buy eggs'
  complete_todo_titled 'Buy eggs'
  user_should_see_completed_todo_titled 'Buy eggs'
end

scenario 'mark completed todo as incomplete' do
  sign_in_as 'person@example.com'
  create_todo_titled 'Buy eggs'
  complete_todo_titled 'Buy eggs'
  mark_incomplete_todo_titled 'Buy eggs'
  user_should_see_incomplete_todo_titled 'Buy eggs'
end

While each of these lines reads well, the subject of each line is the user, or “you”. You sign in, you create a todo titled “Buy eggs”, and you should see a todo with the correct title; the focus should be the todo. The todo is the subject of the test; we’re making assertions about if it’s on the page and its state after certain page interactions occur. The other thing we’re doing is repeating the todo title everywhere, which seems too verbose.

What if we used a Page Object?

scenario 'create a new todo' do
  sign_in_as 'person@example.com'
  todo = todo_on_page
  todo.create

  expect(todo).to be_visible
end

scenario 'view only todos the user has created' do
  sign_in_as 'other@example.com'
  todo = todo_on_page
  todo.create

  sign_in_as 'me@example.com'

  expect(todo).not_to be_visible
end

scenario 'complete my todos' do
  sign_in_as 'person@example.com'
  todo = todo_on_page
  todo.create
  todo.mark_complete

  expect(todo).to be_complete
end

scenario 'mark completed todo as incomplete' do
  sign_in_as 'person@example.com'
  todo = todo_on_page
  todo.create
  todo.mark_complete
  todo.mark_incomplete

  expect(todo).not_to be_complete
end

Aside from signing in, everything focuses on the todo since it’s the star of the show. All that needs to be done is move the helper methods from the original code into methods #create, #mark_complete, #mark_incomplete, #visible?, and #complete?. First, we’ll start with #todo_on_page, though:

def todo_on_page
  TodoOnPage.new('Buy eggs')
end

An instance of TodoOnPage, instantiated with a specific title, is returned which will implement the handful of methods enumerated above.

class TodoOnPage &lt; Struct.new(:title)
  include Capybara::DSL

  def create
    click_link 'Create a new todo'
    fill_in 'Title', with: title
    click_button 'Create'
  end

  def mark_complete
    todo_element.click_link 'Complete'
  end

  def mark_incomplete
    todo_element.click_link 'Incomplete'
  end

  def visible?
    todo_list.has_css? 'li', text: title
  end

  def complete?
    todo_list.has_css? 'li.complete', text: title
  end

  private

  def todo_element
    find 'li', text: title
  end

  def todo_list
    find 'ol.todos'
  end
end

By using well-named methods like #todo_element and #todo_list, it becomes immediately obvious how to mark todos complete and incomplete, as well as how to check if a todo is complete or on the page.

The TodoOnPage is a page object. Given a specific context (in this case, the title of a todo), it encapsulates page interaction (#create, #mark_complete, #mark_incomplete) and assertions (using RSpec’s predicate matchers with #visible? and #complete?).

While I’ve written plenty of page objects before, often they only involve interaction and not predicate methods for matchers. Including both seems totally obvious now; I’m really excited to start using this pattern throughout other areas of my acceptance testing.

Next Steps & Related Reading

Ruby Science

Detect emerging problems in your codebase with Ruby Science. We’ll deliver solutions for fixing them, and demonstrate techniques for building a Ruby on Rails application that will be fun to work on for years to come.

Grab a free sample of Ruby Science today!

hrward

End-to-end testing with RSpec integration tests and Capybara

Integration tests with rspec and capybara

Stability can become an issue as web applications evolve and grow — integration tests provide a great way to perform end-to-end tests that validate the application is performing as expected.

Integration test with rspec and capybara

When writing integration tests, try to model the test around an actor (user of the system) and the action they are performing.

# spec/features/visitor_signs_up_spec.rb
require 'spec_helper'

feature 'Visitor signs up' do
  scenario 'with valid email and password' do
    sign_up_with 'valid@example.com', 'password'

    expect(page).to have_content('Sign out')
  end

  scenario 'with invalid email' do
    sign_up_with 'invalid_email', 'password'

    expect(page).to have_content('Sign in')
  end

  scenario 'with blank password' do
    sign_up_with 'valid@example.com', ''

    expect(page).to have_content('Sign in')
  end 

  def sign_up_with(email, password)
    visit sign_up_path
    fill_in 'Email', with: email
    fill_in 'Password', with: password
    click_button 'Sign up'
  end
end

Extracting common test functionality

To share code between features move common capybara steps into a Ruby module in the rspec support directory.

# spec/support/features/session_helpers.rb
module Features
  module SessionHelpers
    def sign_up_with(email, password)
      visit sign_up_path
      fill_in 'Email', with: email
      fill_in 'Password', with: password
      click_button 'Sign up'
    end

    def sign_in
      user = create(:user)
      visit sign_in_path
      fill_in 'Email', with: user.email
      fill_in 'Password', with: user.password
      click_button 'Sign in'
    end
  end
end

Modules must be explicitly included to share the common code between integration tests.

# spec/support/features.rb
RSpec.configure do |config|
  config.include Features::SessionHelpers, type: :feature
end

Running the integration tests

$ rspec -fd
Visitor signs up
  with valid email and password
  with invalid email
  with blank password

Finished in 0.35837 seconds
3 examples, 0 failures

Takeaways

  • Maintain application stability with end-to-end test coverage
  • Use RSpec to run your integration tests
  • Modules allow shared Capybara steps between specs

By Harlow Ward

Next Steps & Related Reading

Describe the User’s Perspective: DDD, acceptance testing, and you

The Quest Continues: Introducing capybara-webkit

Ruby Science

Detect emerging problems in your codebase with Ruby Science. We’ll deliver solutions for fixing them, and demonstrate techniques for building a Ruby on Rails application that will be fun to work on for years to come.

Grab a free sample of Ruby Science today!