Testing +

Buenas Prácticas

Testing + Buenas Prácticas

  • Frameworks: RSpec vs Minitest
  • Fixtures
  • Debugging
  • Errores comunes con Rails
  • Gemas útiles para trabajar
  • Consultas
  • Cierre

Ciclo de Testing

Ciclo de Testing

Testing Frameworks

Qué framework elegir?

Testing Frameworks

🔗 7 Reasons I'm sticking with minitest and fixtures in Rails

Testing Frameworks

🔗 https://dhh.dk/2014/tdd-is-dead-long-live-testing.html

Testing Frameworks

RSpec

  • rspec-core
  • rspec-expectations
  • rspec-mocks

Y si usamos Rails:

  • rspec-rails

Cheatsheet

height:400px

http://kerryb.github.io/iprug-rspec-presentation/

Aclaración

El slide anterior usa una sintaxis vieja

En lugar de:

(2 + 2).should == 4

Se debe usar:

expect(2 + 2).to be == 4

Ejemplo

RSpec.describe 'MyClass' do
  context "when conditions"
    it "does something" do
      obj = MyClass.new
      expect(obj.some_method).to eq "Something"
    end
  end
end

Expectations

expect(2 + 2).to eq 4
expect(2 + 2).to_not eq 5

Matchers 🔗

# Igualdad
expect(actual).to eq expected

# Identidad
expect(actual).to be expected

# Comparación
expect(actual).to be >== expected
expect(actual).to be_within(delta).of(expected)

# Expresiones Regulares
expect(actual).to match(/expression/)

Matchers 🔗

# Errores / Excepciones
expect { ... }.to raise_error
expect { ... }.to raise_error(ErrorClass)
expect { ... }.to raise_error("message")
expect { ... }.to raise_error(ErrorClass, "message")

# Verdad / Falsedad
expect(actual).to be_truthy   # passes if actual is truthy (not nil or false)
expect(actual).to be true     # passes if actual == true
expect(actual).to be_nil      # passes if actual is nil
expect(actual).to_not be_nil  # passes if actual is not nil

# Predicate matchers
expect(actual).to be_xxx         # passes if actual.xxx?
expect(actual).to have_xxx(:arg) # passes if actual.has_xxx?(:arg)

Red, Green, Refactor...

  • Bloques before/after
  • Bloques let/let!
  • Context

before block

RSpec.describe 'Hello world' do
  before do
    @hw = HelloWorld.new
  end

  it "has a default greeting" do
    expect(@hw.say_hi).to eq "Hello World"
  end

  it "can receive a param" do
    expect(@hw.say_hi "there!").to eq "Hello There!"
  end
end

before block

  • before(:each)
  • before(:all)
  • after(:each)
  • after(:all)

let

RSpec.describe 'Hello world' do
  let(:hw) { HelloWorld.new }

  it "has a default greeting" do
    expect(hw.say_hi).to eq "Hello World"
  end

  it "can receive a param" do
    expect(hw.say_hi "there!").to eq "Hello There!"
  end
end

let + context

RSpec.describe 'Let inside Context' do
  context "as a User" do
    let (:user) { User.new }
    # ...
  end

  context "as an Admin" do
    let (:user) { Admin.new }
    # ...
  end
end

let

  • lazy evaluation
  • memoised on first use
  • reseted between examples

let!

  • siempre se inicializa

Mock (double)

require "rspec/mocks/standalone"

obj = double(:book) # => #<Double "book">
obj = double(:book, pages: 250)
obj.pages  # => 250

class User
  attr_accessor :username
end

# `instance_double` valida contra objetos reales
user = instance_double("User", :admin, username: 'admin') # OK
user = instance_double("User", :admin, email: 'admin@example.com') # Error

Method Stubs

allow(book).to receive(:title) { "The RSpec Book" }
allow(book).to receive(:title).and_return("The RSpec Book")
allow(book).to receive_messages(
    title: "The RSpec Book",
    subtitle: "Behaviour-Driven Development with RSpec, Cucumber, and Friends")

Message Expectations

Similares a un method stub, pero el test espera que el método sea llamado. (En caso contrario, falla)

# Stub
person = double("person")
allow(Person).to receive(:find) { person }

# Expectation
person = double("person")
expect(Person).to receive(:find) { person }

rspec-rails

  • Generators
  • Transactions
  • Directory Structure

Tipos de specs

  • Model
  • Controller
  • Request
  • Feature
  • View
  • Helper
  • Mailer
  • Routing
  • Job
  • System

Model spec

RSpec.describe Post, :type => :model do
  # ...
end

Controller spec

  • render_template
expect(response).to render_template(:new)   # wraps assert_template
  • redirect_to
expect(response).to redirect_to(location)   # wraps assert_redirected_to
  • have_http_status
expect(response).to have_http_status(:created)

System spec

RSpec.describe "Widget management", :type => :system do
  it "enables me to create widgets" do
    visit "/widgets/new"

    fill_in "Name", :with => "My Widget"
    click_button "Create Widget"

    expect(page).to have_text("Widget was successfully created.")
  end
end

Transacciones

Pros y Contras

Fixtures

# This is a YAML comment
david:
  name: David Heinemeier Hansson
  birthday: 1979-10-15
  profession: Systems development
 
steve:
  name: Steve Ross Kellock
  birthday: 1974-09-27
  profession: guy with keyboard

Fixtures associations

# In fixtures/categories.yml
about:
  name: About
 
# In fixtures/articles.yml
first:
  title: Welcome to Rails!
  body: Hello world!
  category: about

Fixtures - modo de uso

# this will return the User object for the fixture named david
users(:david)
 
# this will return the property for david called id
users(:david).id

FactoryBot - definición

FactoryBot.define do
  factory :user do
    first_name { "John" }
    last_name  { "Doe" }
    admin { false }
  end
end

FactoryBot - uso

# Returns a User instance that's not saved
user = build(:user)

# Returns a saved User instance
user = create(:user)

# Returns a hash of attributes that can be used to build a User instance
attrs = attributes_for(:user)

# Returns an object with all defined attributes stubbed out
stub = build_stubbed(:user)

# Passing a block to any of the methods above will yield the return object
create(:user) do |user|
  user.posts.create(attributes_for(:post))
end

Faker

require 'faker'
Faker::Name.name      #=> "Christophe Bartell"
Faker::Internet.email #=> "kirsten.greenholt@corkeryfisher.info"

# Ejemplo
FactoryBot.define do
  factory :user do
    first_name { Faker::Name.name }
    last_name  { Faker::Name.last_name }
    admin { false }
  end
end

Debugging

Errores comunes con Rails

  • No reiniciar el server (Gemfile / Routes)
  • GodObject / Fat models (ej: User)
  • Demasiada logica en los Controllers
  • Queries en las vistas
  • No usar scopes
  • N+1 consultas

Gemas útiles para desarrollo:

  • pry
  • awesome_print
  • hirb
  • letter_opener

Debugging:

  • better_errors
  • pry-rails
  • web-console

Testing:

  • factory_bot
  • faker
  • timecop
  • vcr
  • webmock
  • database_cleaner

Procesos en background:

  • sidekiq
  • rescue
  • delayed_job

Otras gemas útiles:

  • curb
  • rest-client
  • pundit
  • cancancan
  • draper

Otras gemas útiles:

  • dotenv
  • rails_12factor
  • rubocop
  • dependabot

Consultas

Gracias

✉️ Contacto:
joaquin@wecode.io