Advanced Rails with Rspec, Capybara, and friends

TDD

Advanced

What is Hermetic

• Describe Blocks

• It Blocks

• Derived Class

Let’s assume we had an object called WebService

RSpec.describe YourApp::WebService do

1/ Install Rspec + Factory Bot

Add to Gemfile in a group marked :development and :test. Be sure to add it to a group for both :development and :test (do not add it to only a :test group).

gem 'rspec-rails'
gem 'factory_bot_rails'

then run bundle install

Run the rspec installer with

rails generate rspec:install



Now, notice that these files are created

      create  .rspec
      create  spec
      create  spec/spec_helper.rb
      create  spec/rails_helper.rb

Now delete your test/ folder with

rm -rf test/

(This removes the default folder for Minitest, which is the alternative to Rspec that we are not covering in this tutorial.)

Check-in this code using Git and give it a commit message of something like “installs Rspec”

2/ Meet the spec_helper.rb and rails_helper.rb

For historical reasons, Rails-Rspec apps come with two default files: spec_helper and rails_helper.

All Rspec specs must begin with this exact line, at the very top of the file:

require "rails_helper"

Note that the rails_helper.rb is inside of spec/ folder, even though your test might not be. When you run any rspec test, it runs as if the working directory is the spec/ folder itself, even when you are running a spec nested several folders deep. So all Rspec specs you write, including ones nested several folders deep, so do not try to use relative paths if your spec is nested several folders deep. Just include the line exactly as-is: require "rails_helper"

Both files contain Rspec configuration, but rails_helper.rb contains configuration specific to Rails. Let’s take a look a the default spec/rails_helper.rb file now.

First take note this is where RAILS_ENV is set to test. The ||= Ruby operator is called null coalescence and it means that if the RAILS_ENV is not set in the environment (ENV global variable), then set it here.

Then we explicitly load any settings specific to the test environment itself. (These are found in config/environment/test.rb)

Pay attention to what this comment says: Rspec looks for any file inside of the the spec/ directory that ends with _spec.rb. Those files will automatically be seen as spec files, but other files will not. If you can’t figure out why your spec isn’t running, make sure the file is named correctly.

Take a look for the top of the line that begins

RSpec.configure do |config|

A few lines down, you should see config.use_transactional_fixtures = true

Here is the important line that defines our use of transactional fixtures, as discussed in the introduction to this lesson. If you set this to false, you will switch Rspec into truncation strategy.

Towards the bottom of the RSpec.configure do |config| you will see another important config:

The infer_spec_type_from_file_location! setting. When enabled (which is just done by calling it with the bang method like you see here), specs will have added behaviors depending on what folder they are located in.

This is most relevant when we get to writing request specs and system specs, where we need either request spec helpers or Javascript, and so by putting the specs into these folders we are telling Rspec to run with either of these things.



Those are the most critical parts of the rails_helper configuration, and in this tutorial, I will go over the default Rspec configuration only. Once you’ve finished the tutorial, feel free to explore the various settings.

Now that we have a Rspec installed, it is time to write your first spec. Exciting no?

3/ Your First Test

Let’s suppose we have some pencils. Pencils can be of different colors, and we can set Pencils to be any color we want.

We will first create Pencils as its own PORO — plain old Ruby object.

Let’s begin by writing a spec.

Run the rspec generator to create a pencil spec

rails generate rspec:model pencil

require 'rails_helper'

RSpec.describe Pencil, type: :model do
 it "should create a pencil" do
  expect(Pencil.new).to be_a(Pencil)
 end
end

Then run your spec and watch it fail. You are now RED on the RED-GREEN-REFACTOR cycle.

Remember, always use the testing process to describe the code you want. In this fashion you will consistently describe the code you want first, then implement only as much code as to make your test pass.

To implement this spec, all we have to do is create a Pencil class at app/models/pencil.rb

class Pencil

end

Let’s go one step further and give our pencil a color attribute.

require 'rails_helper'

RSpec.describe Pencil, type: :model do

 subject {Pencil.new}

 it "should create a pencil" do

  expect(subject).to be_a(Pencil)

 end

 it "should have a color attribute that will default to nil" do

  expect(subject.color).to be(nil)

 end

 describe "#set_color" do

  before(:each) do

   subject.set_color("red")

  end

  it "should have a color attribute that can be set" do

   expect(subject.color).to eq("red")

  end

 end

And our implementation code:

class Pencil

 attr_accessor :color

 def set_color(color)

  @color = color

 end

end

We are now GREEN in the REDGREEN-REFACTOR cycle. This means that we can proceed to add new features by adding specs first, then implementing those features. We can also refactor the existing features with confidence knowing that there are automated specs covering expected results.

This is why it is called “RED-GREEN-REFACTOR” cycle. By continuously providing feedback to us the developer, the test cycle allows us to work exponentially smarter, which in turn gives us incredible powers to produce code that is open to being refactored and resilient to change.

Expect the Initializer to Take a Color Attribute

require 'rails_helper'

RSpec.describe Pencil, type: :model do

 subject {Pencil.new}

 describe "initialize" do

  it "should create a pencil" do

   expect(subject).to be_a(Pencil)

  end

  describe "with a custom initializer" do

   subject {Pencil.new(color: "red")}

   it "should have a color attribute that can be defined in the initializer" do

    expect(subject.color).to eq("red")

   end

  end

 end

 describe "#set_color" do

  before(:each) do

   subject.set_color("red")

  end

  it "should have a color attribute that can be set" do

   expect(subject.color).to eq("red")

  end

 end

end

class Pencil

 attr_accessor :color

 def initialize(* args)

  @color = args[0][:color] if args[0] # a passive initializer (silently fails of no color)

 end

 def set_color(color)

  @color = color

 end

end

  Refactoring Example (Address Denormalize)

4/ Your Rspec Workflow

Run the Next Failure

Rspec has a “Next failure” feature to shorten your workflow and make it efficient. To use this option (--only-failures), you must first set `config.example_status_persistence_file_path` in your Rspec.configure block.

rspec --next-failure

or rspec -n for short

This tells RSpec to use the last complete run (which it stores in a file in tmp/) to determine the first failing test. It runs just that test.

The simplest, most lightweight workflow is to use this flag to fix run the first failing test, fix it, then run the next one. Wish, wash, repeat.

5/ Random Specs and the Random Seed

By default, your specs are run in random order. The randomness is determined by a random seed number, which is shown to you at the top of every RSpec run. RSpec will generate a new random number at the start of each run, show it to you, and then use this number to determine what order to run the specs in. If you re-run the specs using the same seed number, the specs will re-run in the same order.

This is considered best practice because specs should be independent of one another. Running them in a random order helps to find flakey tests that only pass when they are run in a specific order and fail in another order. This is important because it’s common (dangerously easy) to accidentally write specs that will have an order-dependancy, or fail when one before or after a specific other test.

When RSpec runs the test in random order and then it fails, how can you re-run the test in the exact same order again to debug the issue?

That can be done by telling RSpec to use the same seed for its randomness as it was used before.

RSpec tells you this seed when it is starting:

$ rspec spec


Randomized with seed 29451

.....*.........

Here, re-run the specs in the exact same order run using the –seed flag with the same number that was generated randomly: 29451

$ rspec --seed 29451

6/ Dots and “Docs”

When you run RSpec by default, it will show you an output of green dots (or red ones for failing specs). Sometimes you may have specific specs which will hang or take

e a long time*, and thus you won’t be able to see which one is hanging. The t

*hanging is when a process does not terminate forever; for example, it is caught in an endless loop


To debug this kind of problem, you will want to set the flag -fdoc

This will tell Rspec to output the full documentation of what is run — listing out the test names themselves on the screen in red or green(*)

–format doc

COLORBLIND ACCOMMODATION ——

You can use this setting in Rspec.configure to set your Rspec to use any color for success or failure. You also get options for pending, fixed, and detail.

For example, to keep the failure color as RED and make the success color blue — an easy solution for programmers with red/green colorblindness, add this line to your Rspec.configure

RSpec.configure do |config|
  config.success_color = :blue
end

7/ Backtraces

To see the full backtrace of the crash, enable this in spec_helper.rb

config.full_backtrace = true

Or, just run your spec with --backtrace

8/ Factory Bot

Factory Bot is the premier Ruby tool for creating factories for your test suite.

Your factories will live in a folder at spec/factories and will have the same names as the objects they represent.

Each time you build a factory (or create one, explained below), a new instance variable will come into Ruby’s memory space.

Build vs. Create

When you call build(:xyz) it will be as-if you had called .new on the thing you are creating — therefore, it won’t save it to the database.

When you call create(:xyz), you will be telling Factory Bot to instantiate and save your newly created object.

Build is typically preferred in Rails apps because it is much faster. It makes it so that your tests aren’t dependent on “hitting the database” (that means making any database calls)

This means that model-level validations will not be applied because the record won’t actually be saved.

Typically for much unit testing, where you are testing the internals of the code you are writing,

Often, however, there are necessary reasons to use create instead of build:

– When working with multiple objects, like in an integration test, ActiveRecord cannot save child records until the parent record is saved. This makes it necessary to either use create (which instantiates and saves the new object), or use build along with a save. (You can save only the child record too, which will

Initialization

initialize_with

skip_create

Factory Example (…)

9/ Coverage Reports with SimpleCov

10/ Mocking, Stubs, and Doubles

11/ The Testing Hourglass

12/ End-to-End Testing with Capybara

• Selenium Setup & Browser options

Setting the window size, enabling chrome flags

• Setting Capybara.javascript_driver

Types of tests

• Mastering Race Conditions

• Using a scope block (within do)

• Using Capybara.using_session

• Seeing Your Console messages

https://stackoverflow.com/questions/46278514/capture-browser-console-logs-with-capybara/46292517#46292517

13/ Faking out Foreign Endpoints

Webmock

Faraday

14/ Flaky Tests

“Why do these tests fail randomly?”

“Why do these tests fail only after 7 pm at the end of the month?”

A common cause of flaky tests is time and date-based considerations.

15/ Brittle Tests

“I touched something unrelated, and my tests broke.”

“Every change I make these tests break.”

16/ The Virtuous TDD Cycle

18/ Testing Antipatterns

17/ Continuous Integration and Continuous Delivery

18/ Testing Nirvana