Let's set up a few things to improve the overall testing mechanics of this app.


# ...

gem "jsonapi-resources", "~> 0.10.2"
gem "pry-rails"

group :development, :test do
  gem "byebug", platforms: %i[mri mingw x64_mingw]
  gem "factory_bot_rails"
  gem "faker"
  gem "pry-byebug"
  gem "rspec-rails", "~> 4.0.0"
  gem "rubocop-rails_config"
  gem "simplecov"

group :development do
  gem "brakeman"
  gem "guard-rspec"
  gem "listen", ">= 3.0.5", "< 3.2"
  gem "spring"
  gem "spring-watcher-listen", "~> 2.0.0"

# ...
bundle install

One thing at a time:

Adding pry-rails outside of the :development, :test groups so that rails console or rails c can ben run in any environment, with the benefit of pry.

factory_bot_rails will allow rapid creation of records within the scope of our tests.

faker will allow us to populate those records with realistic fake data.

simplecov is a code coverage reporter. This will let us know if our tests are doing their job.

brakeman is a tool for auditing our entire application for security issues. I haven't used this before but it seems like a very good idea.

Finally, guard-rspec, which includes guard is a nifty little program that will watch our code for changes and quickly run and re-run the related tests and report the test status in OS-specific notifications.


I'm going to go kind of in reverse, from bottom to top.


bundle exec guard init rspec

This will generate a Guardfile for us. But it doesn't include any expressions for rspec request specs, which I will make heavy use of. I amend this section of the Guardfile like so:

# ...

  watch(rails.controllers) do |m|

  # Rails config changes
  watch(rails.spec_helper)     { rspec.spec_dir }
  watch(rails.routes)          { "#{rspec.spec_dir}/routing" }

  watch(rails.app_controller) do
  # FactoryBot Factories
  watch(%r{^spec/factories/(.+)\.rb$}) do |m|

# ...


Don't check coverage reports into git. Add this to .gitignore:

# Ignore simplecov reports

At the very top of the spec_helper.rb file add:

require 'simplecov'
SimpleCov.start do
  add_filter '/config'
  add_filter '/spec/'

# This file was generated by the `rails generate rspec:install` command.
# ...

Now whenever your (rspec) test are run (e.g. by guard-rspec), files inside of coverage/ will be updated. Open coverage/index.html in a browser, and keep it refreshed to see the latest test coverage report.


In the spec/rails_helper.rb file there is a little section below require 'rspec/rails to add additional requires:

# ...
require 'rspec/rails'
# Add additional requires below this line. Rails is not loaded until this point!
require 'faker'

# ...

Factory Bot

Create a spec/support folder if it doesn't already exist, and also create a spec/factories folder, and make this file:

RSpec.configure do |config|
  config.include FactoryBot::Syntax::Methods

This ensures that factories are available in all of the spec files.

Speaking of factories, I'm going to make one for my Food model:

FactoryBot.define do
  factory :food do
    name { Faker::Food.unique.ingredient }

Yes, faker comes with a Food module!

Now I can refactor the 2nd test in my foods_request_spec as such (the first test doesn't use any data):

  # ...
  it "returns a collection of foods when there are foods" do
    create_list(:food, 3)

    get "/foods"

    expect(response).to have_http_status(:success)
    expect(response.content_type).to eq("application/vnd.api+json")

    response_hash = JSON.parse(response.body).deep_symbolize_keys
    expect(response_hash).to be_a(Hash)
    expect(response_hash).to have_key(:data)
    expect(response_hash[:data].length).to eq(3)
    expect(response_hash).to_not have_key(:errors)

    attrs = response_hash[:data].map { |item| item[:attributes][:name] }
    expect(attrs).to contain_exactly(*Food.all.map(&:name))

    ids = response_hash[:data].map { |item| item[:id].to_i }
    expect(ids).to contain_exactly(*Food.all.map(&:id))
# ...

Notice the use of create_list(:food, 3). That auto-magically generates 3 Food records. I have to be a bit more clever in verifying the names and ids because now they are random instead of hard-coded. But on the plus side I'm no longer relying on hard-coded data in  my test. This will bear fruit as things increase in complexity.

One More Thing

By default the included spec/spec_helper.rb file has a block of options that are commented out, from =begin to =end. I like to delete the lines of code with =begin and =end on them and so enable all of those optional configurations. This includes enabling the :focus symbol to run just one test, and running the tests in a random order.

Whew! That's plenty for now. Next I'm going to make this app actually do some stuff (with tests, of course).