Build Robust & Production Quality Applications - Lesson 6: Writing Feature Tests for Emails

Interact with Emails in Feature Tests:

Sometimes applications may require you to complete a form and an email is is generated. To test this feature, Capybara-email is a great way to test this functionality and provides a nice interface that allows you to write specs on the level of user interaction. This is better than as creating a feature test for ActionMailer and its deliveries because it will require too much low level detail.

gem 'capybara-email'
 documentation: github//dockyard/capybara-email

Example spec for inviting a user:

#user_resets_password_spec.rb
require 'spec_helper'
feature 'User resets password' do
  scenario 'user successfully resets the password' do
    alice = Fabricate(:user, password: 'old_password')
    visit sign_in_path
    click_link "Forgot Password"
    fill_in "Email Address", with: alice.email
    click_button "Send Email"

    open_email(alice.email)
    current_email.click_link("Reset My Password")

    fill_in "New Password", with: "new_password"
    click_button "Reset Password"

    fill_in "Email Address", with: alice.email
    fill_in "Password", with: "new_password"
    click_button "Sign in"
    expect(page). to have_content("Welcome, #{alice.full_name}")

    clear_email
  end
end

Build Robust & Production Quality Applications - Lesson 5: Three Styles of BDD

Testing Styles in Rails: Three Styles of Behavior Driven Development

Traditionally we have followed the following flow:

Meet in the Middle Technique

  1. Turn mock ups into view templates: copy the mockups and replace the placeholder data
  2. Controller Tests (inner layer)- the controller tests will drive the model layer tests, routes, db migrations, etc.
  3. Model Level Tests(inner layer) - if we need active record based and non-active record based model - logic, we will need to drop down to model layer to test their
  4. Feature Specs/Integration - (Outer most layer) Tests will allow us to test everything including the view templates
  • Here you will end up with less feature/integration specs than outside in because they are typically written after multiple specs.

Inside out Approach

  1. Start from the models and migrations that are required for that feature and then right specs to test that model
  2. Controller Level
  3. View/Intergation Specs
  • easier to start .. allows you to work with something small
  • But you sometimes don't know where you're going and you may end up adding alot of code that is not needed.
  • This is used primarily in productized shops where its easier to add a model to a more mature product.

Outside in Approach

Start at the Integration Level - This integration level does not mean in the same way that we reference the Integration Tests in the "Meet in the Middle" technique where where we test multiple features (horizontal) but rather vertical -integration means where we test for models, controllers and views for one feature - written in a very business level decision level - you have to think 'what is the business value we are looking to provide?'

  • route, controller, models
  • also called BDD or ABDD (A is for Acceptance)
  • often used in consultancys, clients provide the feature
  • often follows more a natural flow
  • requires you to know rails really well and how things work together
  • people often use cucumber or rspec feature tests for this.

Build Robust & Production Quality Applications - Lesson 4: Transactions & Exception Resues

The[ previous example reegarding updating video review while in queue does not follow typical Rails convention: - We are using in the updatequeue method. - we have to use parameter naming conventions to parse out the queueitem id and position id in the form. - We have to specify the order of queue_items using in the user model according to position.

Using Transactions & Exception Rescues when the value does not save.

def update_queue
  begin
    ActiveRecord::Base.transaction do
      params[:queue_items].each do |queue_item_data|
        queue_item = QueueItem.find(queue_item_data["id"])
        queue_item.update_attributes!(position: queue_item_data["position"])
      end
    end
  rescue ActiveRecord::RecordInvalid
    flash[:error] = "You must use whole numbers for play position."
    redirect_to my_queue_path
    return
  end

  current_user.queue_items.each_with_index do |queue_item, index|
    queue_item.update_attributes(position: index+1)
  end
  redirect_to my_queue_path
end

Build Robust & Production Quality Applications - Lesson 4: Parameter Naming Conventions

Parameter Naming Conventions

Visit Rails Guides' Parameter Naming Conventions Use that as the starting point to construct the params that hit the server.

As you've seen in the previous sections, values from forms can be at the top level of the params hash or nested in another hash. For example, in a standard create action for a Person model, params[:person] would usually be a hash of all the attributes for the person to create. The params hash can also contain arrays, arrays of hashes and so on.

Fundamentally HTML forms don't know about any sort of structured data, all they generate is name-value pairs, where pairs are just plain strings. The arrays and hashes you see in your application are the result of some parameter naming conventions that Rails uses.

Basic Structures

The two basic structures are arrays and hashes. Hashes mirror the syntax used for accessing the value in params. For example, if a form contains:

<input id="person_name" name="person[name]" type="text" value="Henry"/>

the params hash will contain

{'person' => {'name' => 'Henry'}}

and params[:person][:name] will retrieve the submitted value in the controller.

Hashes can be nested as many levels as required, for example:

<input id="person_address_city" name="person[address][city]" type="text" value="New York"/>

will result in the params hash being

{'person' => {'address' => {'city' => 'New York'}}}

Normally Rails ignores duplicate parameter names. If the parameter name contains an empty set of square brackets [] then they will be accumulated in an array. If you wanted users to be able to input multiple phone numbers, you could place this in the form:

<input name="person[phone_number][]" type="text"/>
<input name="person[phone_number][]" type="text"/>
<input name="person[phone_number][]" type="text"/>

This would result in params[:person][:phone_number] being an array containing the inputted phone numbers.

Combining Them

We can mix and match these two concepts. One element of a hash might be an array as in the previous example, or you can have an array of hashes. For example, a form might let you create any number of addresses by repeating the following form fragment

<input name="addresses[][line1]" type="text"/>
<input name="addresses[][line2]" type="text"/>
<input name="addresses[][city]" type="text"/>

This would result in params[:addresses] being an array of hashes with keys line1, line2 and city. Rails decides to start accumulating values in a new hash whenever it encounters an input name that already exists in the current hash.

There's a restriction, however, while hashes can be nested arbitrarily, only one level of "arrayness" is allowed. Arrays can usually be replaced by hashes; for example, instead of having an array of model objects, one can have a hash of model objects keyed by their id, an array index or some other parameter.

Build Robust & Production Quality Applications - Lesson 4: Update Review in Queue

In Myflix, we want to be able to allow the user to update video rating in the video_play queue.

Goal: To change video rating from the queueitem index. To do so, we have to create the virtual attribute on the queueitem model, as though it is a column on the queue_item model (table). Since it is not in the model, we have to define the getter and setter.

#queue_item.rb
def rating

end

def rating=(new_rating)
 -this is where you begin to write your tests
end

Update Column vs Update Attribute

updateattributes will go through validation error unlike the updatecolumn method,which will bypass validation.

def rating=(new_rating)
  review = Review.where(user_id: user.id, video_id: video.id).first
  review.update_column(:rating, new_rating)
end

Memoization

Memoization means the first time the record is called, it will hit the database only once and store that info into the instance variable and will not database again when record is called.

def review
  @review ||= Review.where(user_id: user.id, video_id: video.id).first
end