Esse repositório contém as informações referentes ao Curso TDD com Ruby on Rails, RSpec e Capybara do professor Jackson Pires
O repositório oficial do curso está disponível em https://github.com/jacksonpires/rails-tdd
gem instal
# instala o Rspecrspec --init
#inicia um projeto Rspecgem install bundler
# instala o gerenciador de dependências do rubybundle install
- instala as dependências
- should Vs expect: RSpec's New Expectation Syntax
- Better Specs
Um teste do padrão xUnit tem quatro fases, são elas:
- Setup: Quando você coloca o SUT (system under test, o objeto sendo testado) no estado necessário para o teste;
- Exercise: Quando você interage com o SUT;
- Verify: Quando você verifica o comportamento esperado;
- Teardown: Quando você coloca o sistema no estado em que ele estava antes do teste ser executado.
rspec --format documentation
- executa os testes exibindos as descrições dos teste de forma mais completa- adicione
rspec --format documentation
no arquivo.rspec
para executar somente comrspec
it 'with negative numbers'
(it sem o corpo) : executa o teste marcando como pendentexit
: executa o teste marcando como pendenteit 'with negative numbers' do result = subject.sum(-5,7) expect(result).to eq(2) end
rspec -e 'with negative numbers'
: executa somente o teste com oit 'with negative numbers'
rspec ./spec/calculator_spec.rb:13
: executa somente o teste da linha 13
equal/be
: verifica se ops objetos são iguaiseql/eq
: verifica se os valores são iguais
Exemplos com divisão por zero:
- expect{3 / 0}.to raise_exception # com warning
- expect{3 / 0}.to raise_error #com warning
- expect{3 / 0}.to raise_error(ZeroDivisionError)
- expect{3 / 0}.to raise_error("divided by 0")
- expect{3 / 0}.to raise_error(ZeroDivisionError, "divided by 0")
- expect{3 / 0}.to raise_error(/divided/)
- expect(subject).to include(2)
- expect(subject).to include(2,1)
- expect(subject).to contain_exactly(3,1,2)
- expect(subject).to match_array([2,3,1])
describe (1..5), 'Ranges' do
it '#cover' do
expect(subject).to cover(2)
expect(subject).to cover(5)
end
end
it { is_expected.to cover(2) }
it { is_expected.to cover(2,5) }
it { is_expected.not_to cover(0,6) }
it { is_expected.to start_with('Ruby').and end_with('Rails') }
it { expect(fruta).to eq('banana').or eq('laranja').or eq('uva')}
def fruta
%w(banana laranja uva).sample
end
it { expect([1,5,9]).to all((be_odd).and (be_an(Integer))) }
it { expect(['ruby', 'rails']).to all(be_a(String).and include('r')) }
describe 'be_within' do
it { expect(12.5).to be_within(0.5).of(12) }
it { expect([12.5,12.1,12.4]).to all (be_within(0.5).of(12)) }
end
it { expect(10).to satisfy {|number| number % 2 == 0} }
it {
expect(9).to satisfy('be a multiply of 3') do |number|
number % 3 == 0
end
}
- Helper Arbitrário:
describe 'Ruby on Rails' do
it { is_expected.to start_with('Ruby').and end_with('Rails') }
it { expect(fruta).to eq('banana').or eq('laranja').or eq('uva')}
def fruta
%w(banana laranja uva).sample
end
end
- Helper de Módulo:
- Adicione
require_relative '../helpers/helper'
no início do arquivoexemplo_rspec_tdd/spec/spec_helper.rb
e adicioneconfig.include Helper
dentro deRSpec.configure do |config|
. Exemplo:
require_relative '../helpers/helper' RSpec.configure do |config| # Helper Methods de Módulo config.include Helper # code ... end
- Adicione
Arquivo exemplo_rspec_tdd/spec/spec_helper.rb
:
config.before(:suite) do
puts ">>>>>>>>>> ANTES de TODA a suíte de testes"
end
config.after(:suite) do
puts ">>>>>>>>>> DEPOIS de TODA a suíte de testes"
end
config.before(:context) do
puts ">>>>>>>>>> ANTES de TODOS os testes"
end
config.after(:all) do
puts ">>>>>>>>>> DEPOIS de TODOS os testes"
end
Dentro dos testes:
before(:each) do
puts "ANTES"
@pessoa = Pessoa.new
end
after(:each) do
@pessoa.nome = "Sem nome!"
puts "DEPOIS >>>>>>> #{@pessoa.inspect}"
end
around(:each) do |teste|
puts "ANTES"
@pessoa = Pessoa.new
teste.run # roda o teste
@pessoa.nome = "Sem nome!"
puts "DEPOIS >>>>>>> #{@pessoa.inspect}"
end
let
: a variável é carregada apenas
quando ela é utilizada pela primeira vez
no teste e fica na cache até o teste em
questão terminar.
$counter = 0
describe 'let' do
let(:count) { $counter += 1 }
it 'memoriza o valor' do
expect(count).to eq(1)
expect(count).to eq(1)
end
it 'não é cacheado entre os testes' do
expect(count).to eq(2)
end
end
let!
: forçar a invocação do método/helper antes de cada teste
$count = 0
describe 'let!' do
ordem_de_invocacao = []
let!(:contador) do
ordem_de_invocacao << :let!
$count += 1
end
it 'chama o método helper antes do teste' do
ordem_de_invocacao << :exemplo
expect(ordem_de_invocacao).to eq([:let!, :exemplo])
expect(contador).to eq(1)
end
end
it { expect { Contador.incrementa }.to change {Contador.qtd} }
it { expect { Contador.incrementa }.to change {Contador.qtd}.by(1) }
it { expect { Contador.incrementa }.to change {Contador.qtd}.from(2).to(3) }
it { expect { puts "adriano"}.to output.to_stdout }
it { expect { print "adriano" }.to output("adriano").to_stdout }
it { expect { puts "adriano avelino" }.to output(/adriano/).to_stdout }
it { expect { warn "adriano" }.to output.to_stderr }
it { expect { warn "adriano" }.to output("adriano\n").to_stderr }
it { expect { warn "adriano" }.to output(/adriano/).to_stderr }
- Adicione
RSpec::Matchers.define_negated_matcher :<novo>, :<velho>
no início do arquivo e acrescente o teste abaixo:
RSpec::Matchers.define_negated_matcher :exclude, :include
describe Array.new([1,2,3]), "Array" do
it '#include' do
expect(subject).to include(2)
expect(subject).to include(2,1)
end
it { expect(subject).to exclude(4)}
end
No exemplo acima ele se comporta da forma negativado include
.
Por padrão os testes dentro it que possuem diversas expects param ao encontrar a primeira falha no expect. É nesses casos que podemos agregar as falhas, mostrando as demais expects com erro, caso possuam.
- Agregando falhas em bloco:
it 'be_between inclusive' do
aggregate_failures do
expect(5).to be_between(2,7).inclusive
expect(2).to be_between(2,7).inclusive
expect(7).to be_between(2,7).inclusive
end
end
- Agregando falhas dentro do
it
:
it 'be_between inclusive / Falhas agregadas', :aggregate_failures do
expect(5).to be_between(2,7).inclusive
expect(1).to be_between(2,7).inclusive
expect(8).to be_between(2,7).inclusive
end
- ou na forma antiga:
it 'be_between inclusive / Falhas agregadas', aggregate_failures: true do
expect(5).to be_between(2,7).inclusive
expect(1).to be_between(2,7).inclusive
expect(8).to be_between(2,7).inclusive
end
- dentro do spec_helper:
#requires
RSpec.configure do |config|
config.define_derived_metadata do |meta|
meta[:aggregate_failures] = true
end
#codes
end
- classe
pessoa.rb
class Pessoa
attr_reader :status
def feliz!
@status = "Sentindo-se Feliz!"
end
def triste!
@status = "Sentindo-se Triste!"
end
def contente!
@status = "Sentindo-se Contente!"
end
end
shared_example
:
require 'pessoa'
shared_examples 'status' do |sentimento|
it "#{sentimento}" do
pessoa.send("#{sentimento}!")
expect(pessoa.status).to eq("Sentindo-se #{sentimento.capitalize}!")
end
end
describe 'Pessoa' do
subject(:pessoa) { Pessoa.new }
include_examples 'status', :feliz
it_behaves_like 'status', :triste
it_should_behave_like 'status', :contente
# it 'feliz!' do
# pessoa.feliz!
# expect(pessoa.status).to eq('Sentindo-se Feliz!')
# end
#
# it 'triste!' do
# pessoa.triste!
# expect(pessoa.status).to eq('Sentindo-se Triste!')
# end
#
# it 'contente!' do
# pessoa.contente!
# expect(pessoa.status).to eq('Sentindo-se Contente!')
# end
end
- define um matcher customizado:
RSpec::Matchers.define :be_a_multiple_of do |expected|
match do |actual|
actual % expected == 0
end
end
- define a mesagem de erro de matcher customizado:
failure_message do |actual|
"expected that #{actual} would be a multiple of #{expected}"
end
- define a mesagem de sucesso do matcher customizado:
description do
"be a multiple of #{expected}"
end
Exemplo completo com teste:
RSpec::Matchers.define :be_a_multiple_of do |expected|
# expected == 3
# actual == subject == 18
#custom matcher
match do |actual|
actual % expected == 0
end
#custom failure message
failure_message do |actual|
"expected that #{actual} would be a multiple of #{expected}"
end
#custom success message
description do
"be a multiple of #{expected}"
end
end
#test
describe 18, 'Custom Matcher' do
it { is_expected.to be_a_multiple_of(3)}
end
São usados para filtar alguns tipos de testes, como por exemplo collections, arrays e etc.
Podemos colocar tag nos testes das seguintes formas:
tagname: true
:
describe 'all', collection: true do
it { expect([1,7,9]).to all( (be_odd).and be_an(Integer) )}
end
Para testar executamos: rspec . -t collection: true
type: tagname
describe 'all', type: 'collection' do
it { expect([1,5,9]).to all((be_odd).and (be_an(Integer))) }
end
Para testar executamos: rspec . -t type:collection
- ou com symbols:
describe 'all', :collection do
it { expect([1,5,9]).to all((be_odd).and (be_an(Integer))) }
end
Para testar executamos: rspec . -t collection
Também podemos negar alguns testes. Exemplo:
it '#include' do
expect(subject).to include(2)
expect(subject).to include(2,1)
end
it '#contain_exactly', :slow do
expect(subject).to contain_exactly(3,1,2)
end
Para testar executamos: rspec . -t collection -t ~slow
E caso seja necessário pode-se acrescentar no arquivo .rspec
:
--tag type:collection
--tag ~slow
describe 'Test Double' do
it 'Double' do
user = double('User')
#resumido
# allow(user).to receive_messages(name: 'Adriano', password: 'secret')
# verboso
allow(user).to receive(:name).and_return('Jack')
allow(user).to receive(:password).and_return('secret')
user.name
user.password
end
end
describe 'Stub' do
it '#has_finished?' do
student = Student.new
course = Course.new
allow(student).to receive(:has_finished?)
.with(an_instance_of(Course))
.and_return(true)
course_finished = student.has_finished?(course)
expect(course_finished).to be_truthy
end
end
- argumento dinâmico:
it 'Argumentos Dinâmicos' do
student = Student.new
allow(student).to receive(:foo) do |arg|
if arg == :hello
"olá"
elsif arg == :hi
"Hi!!!"
end
end
expect(student.foo(:hello)).to eq('olá')
expect(student.foo(:hi)).to eq('Hi!!!')
- Qualquer instância de Classe:
it 'Qualquer instância de Classe' do
student = Student.new
other_student = Student.new
allow_any_instance_of(Student).to receive(:bar).and_return(true)
expect(student.bar).to be_truthy
expect(other_student.bar).to be_truthy
end
- Testando Erros:
it 'Erros' do
student = Student.new
allow(student).to receive(:bar).and_raise(RuntimeError)
expect{ student.bar }.to raise_error(RuntimeError)
end
describe 'Mocks' do
it '#bar' do
# setup
student = Student.new
# verify
expect(student).to receive(:bar)
# exercise
student.bar
end
end
- mock com restrição de argumento
it 'args' do
student = Student.new
expect(student).to receive(:foo).with(123)
student.foo(123)
end
- mock com contagem de chamadas
it 'repetição' do
student = Student.new
expect(student).to receive(:foo).with(123).twice
student.foo(123)
student.foo(123)
end
- mock com valor de retorno
it 'retorno' do
student = Student.new
expect(student).to receive(:foo).with(123).and_return(true)
puts student.foo(true)
end
it 'as_null_object' do
user = double('User').as_null_object
allow(user).to receive(:name).and_return('Jack')
allow(user).to receive(:password).and_return('secret')
puts user.name
puts user.password
user.abc
end
rails _5.1.4_ new test_app -T
: cria um projeto rails. A opção-T
não cria a pasta padrão de testes do Rails- adicione no arquivo
Gemfile
, dento do blocogroup :development, :test
o seguinte conteúdo:gem 'rspec-rails', '~> 3.6'
- execute
bundle install
- verifique a configuração do arquivo
config/database.yml
- crie os bancos de dados com
rails db:create:all
- instale o rspce no projeto:
rails generate rspec install
- adicione no final do seu arquivo .rspec:
--format documentation
- instale o binário do
rspec
:- adicione no arquivo
Gemfile
, emgroup :development
o seguinte:gem 'spring-commands-rspec'
- instale as dependências:
bundle install
- execute:
bundle exec spring binstub rspec bundle exec spring binstub --all
- para testar execute:
bundle exec rspec bin/rspec
- adicione no arquivo
- configure o generator do rails para criar os testes do rspec adicionando o seguinte no arquivo
config/application.rb
, abaixo deconfig.generators.system_tests = nil
:config.generators do |g| g.test_framework :rspec, fixtures: false, view_specs: false, helper_specs: false, routing_specs: false end
- adicione a no arquivo
Gemfile
, dentro do grupo de test e developement o seguinte:
gem 'capybara'
- atualize as dependências:
bundle install
- adicione a no arquivo
- crie o arquivo
spec/fixtures/customers.yml
:
jackson:
name: Jackson Pires
email: jackson@pires.com.br
jose:
name: José da Silva
email: jose@jose.com
- exemplo de uso:
require 'rails_helper'
RSpec.describe Customer, type: :model do
fixtures :all
it 'Create a Customer' do
customer = customers(:jackson)
expect(customer.full_name).to eq("Sr. Jackson Pires")
end
end
- adicione nas dependências do grupo development e test:
gem "factory_bot_rails"
- adicione em
spec/rails_helper.rb
:include FactoryBot::Syntax::Methods
- exemplo de uso:
it 'Create a Customer with FactoryBot' do customer = create(:customer) expect(customer.full_name).to eq("Sr. Jackson Pires") end
- adicione nas dependências do grupo development e test:
gem "fake"
- altere o arquivo
spec/factories/customer.rb
:FactoryBot.define do factory :customer do name {Faker::Name.name} email {Faker::Internet.email} end end
- exemplo de teste:
it 'Create a Customer with FactoryBot' do customer = create(:customer) expect(customer.full_name).to start_with("Sr.") end it { expect{create(:customer)}.to change{Customer.all.size}.by(1) }
- exemplo de sobrescrita de atributo:
it 'Overwrites attributes with FactoryBot' do customer = create(:customer, name: 'Jackson Pires') expect(customer.full_name).to eq("Sr. Jackson Pires") end
- alias para fábrica:
- altere o arquivo
spec/factories/customer.rb
:FactoryBot.define do factory :customer, aliases: [:user] do name {Faker::Name.name} email {Faker::Internet.email} end end
- exemplo de uso:
it 'FactoryBot with alias' do customer = create(:user) expect(customer.full_name).to start_with("Sr.") end
- altere o arquivo
- exemplo de fábrica usando herança:
FactoryBot.define do
factory :customer, aliases: [:user] do
name {Faker::Name.name}
email {Faker::Internet.email}
factory :customer_vip do
vip {true}
days_to_pay {30}
end
factory :customer_default do
vip {false}
days_to_pay {15}
end
end
end
- exemplo de uso:
it 'Herança com customer_vip' do
customer = create(:customer_vip)
expect(customer.vip).to eq(true)
end
it 'Herança com customer_default' do
customer = create(:customer_default)
expect(customer.vip).to eq(false)
end
it 'usando attributes_for' do
attrs = attributes_for(:customer)
attrs1 = attributes_for(:customer_vip)
attrs2 = attributes_for(:customer_default)
puts attrs
puts attrs1
puts attrs2
end
it 'usando attributes_for 2' do
attrs = attributes_for(:customer)
customer = Customer.create(attrs)
expect(customer.full_name).to start_with("Sr.")
end
FactoryBot.define do
factory :customer, aliases: [:user] do
transient do
upcased {false}
end
name {Faker::Name.name}
email {Faker::Internet.email}
factory :customer_vip do
vip {true}
days_to_pay {30}
end
factory :customer_default do
vip {false}
days_to_pay {15}
end
after(:create) do |customer, evaluator|
customer.name.upcase! if evaluator.upcased
end
end
end
it 'Atributo transitório' do
customer = create(:customer_default, upcased: true)
expect(customer.name.upcase).to eq(customer.name)
end
FactoryBot.define do
factory :customer, aliases: [:user] do
transient do
upcased {false}
end
name {Faker::Name.name}
email {Faker::Internet.email}
trait :male do
gender {'M'}
end
trait :female do
gender {'F'}
end
trait :vip do
vip {true}
days_to_pay {30}
end
trait :default do
vip {false}
days_to_pay {15}
end
factory :customer_male, traits: [:male]
factory :customer_female, traits: [:female]
factory :customer_vip, traits: [:vip]
factory :customer_default, traits: [:default]
factory :customer_male_vip, traits: [:male, :vip]
factory :customer_female_vip, traits: [:female, :vip]
factory :customer_male_default, traits: [:male, :default]
factory :customer_female_default, traits: [:female, :default]
after(:create) do |customer, evaluator|
customer.name.upcase! if evaluator.upcased
end
end
end
- exemplo de uso:
it 'Cliente Feminino' do
customer = create(:customer_female)
expect(customer.gender).to eq('F')
end
it 'Cliente Feminino Default' do
customer = create(:customer_female_default)
expect(customer.gender).to eq('F')
end
it 'Cliente Masculino Vip' do
customer = create(:customer_male_vip)
expect(customer.gender).to eq('M')
expect(customer.vip).to eq(true)
end
- arquivo de factory:
FactoryBot.define do
factory :order do
sequence(:description) { |n| "Pedio número - #{n}"}
customer #form one
# association :customer, factory: :customer #form two
end
end
- arquivo de teste:
require 'rails_helper'
RSpec.describe Order, type: :model do
it 'Tem 1 pedido' do
order = create(:order)
expect(order.customer).to be_kind_of(Customer)
end
it 'Tem 1 pedido com sobrescrita' do
customer = create(:customer)
order = create(:order, customer: customer)
expect(order.customer).to be_kind_of(Customer)
end
end
it 'Tem 3 pedidos usando create_list com três' do
orders = create_list(:order, 3)
expect(orders.count).to eq(3)
end
it 'Tem 3 pedidos usando sobrescrita de atributos' do
orders = create_list(:order, 3, description: 'Testeee')
p orders
expect(orders.count).to eq(3)
end
- exemplo: https://github.com/jacksonpires/rails-tdd/commit/d11bbdfd9dd2f86f242bf438c943bea1082da099
- create_pair:
it 'Tem 2 pedidos usando create_pair' do
orders = create_pair(:order)
expect(orders.count).to eq(2)
end
- build_pair
- attributes_for_list
- build_stubbed
- build_stubbed_list
- exibe os erros de forma mais clara. Por exemplo quando adicoinamos uma coluna chamada address como obrigatório e não mudamos os testes
- adicione o seguinte dento do arquivo
spec/spec_helper.rb
:
#FactoryBot Lint
config.before(:suite) do
FactoryBot.lint
end
- httparty
- api para teste
- adicione a gem no grupo desenvolvimento e teste:
gem 'httparty'
describe 'HTTParty' do
it 'HTTParty' do
response = HTTParty.get('https://jsonplaceholder.typicode.com/posts/2')
content_type = response.headers['content-type']
expect(content_type).to match(/application\/json/)
end
end
- adicione no Gemfile:
gem 'webmock'
- adicione no
spec/spec_helper.rb
:require 'webmock/rspec'
- exemplo de uso:
it 'content-type' do
stub_request(:get, "https://jsonplaceholder.typicode.com/posts/2").
to_return(status: 200, body: "", headers: { 'content-type': 'application/json'})
response = HTTParty.get('https://jsonplaceholder.typicode.com/posts/2')
content_type = response.headers['content-type']
expect(content_type).to match(/application\/json/)
end
- adicione a gem:
gem 'vcr'
- adicione no
spec/spec_helper.rb
:
VCR.configure do |config|
config.cassette_library_dir = "spec/fixtures/vcr_cassettes"
config.hook_into :webmock
end
- exemplo de uso no teste:
it 'content-type' do
VCR.use_cassette("jsonplaceholder/posts") do
response = HTTParty.get('https://jsonplaceholder.typicode.com/posts/2')
content_type = response.headers['content-type']
expect(content_type).to match(/application\/json/)
end
end
- atualize a configuração do VCR no
spec/spec_helper.rb
como o exemplo abaixo:
VCR.configure do |config|
config.cassette_library_dir = "spec/fixtures/vcr_cassettes"
config.hook_into :webmock
config.configure_rspec_metadata!
end
- exemplo de teste:
it 'content-type', vcr: { cassette_name: 'jsonplaceholder/posts'} do
#stub_request(:get, "https://jsonplaceholder.typicode.com/posts/2").
# to_return(status: 200, body: "", headers: { 'content-type': 'application/json'})
response = HTTParty.get('https://jsonplaceholder.typicode.com/posts/2')
content_type = response.headers['content-type']
expect(content_type).to match(/application\/json/)
end
- adicone na configuração do VCR em
spec/spec_helper.rb
: config.filter_sensitive_data('') { 'https://jsonplaceholder.typicode.com' } - exemplo completo:
VCR.configure do |config|
config.cassette_library_dir = "spec/fixtures/vcr_cassettes"
config.hook_into :webmock
config.configure_rspec_metadata!
config.filter_sensitive_data('<API-URL>') { 'https://jsonplaceholder.typicode.com' }
end
- exemplo de configuração:
it 'content-type', vcr: { cassette_name: 'jsonplaceholder/posts', match_requests_on: [:body]} do
#stub_request(:get, "https://jsonplaceholder.typicode.com/posts/2").
# to_return(status: 200, body: "", headers: { 'content-type': 'application/json'})
response = HTTParty.get("https://jsonplaceholder.typicode.com/posts/#{[1,2,3,4,5].sample}")
content_type = response.headers['content-type']
expect(content_type).to match(/application\/json/)
end
- exemplo de VCR criando um novo cassete para cada lateração de URI:
it 'content-type', vcr: { cassette_name: 'jsonplaceholder/posts', :record => :new_episodes } do
response = HTTParty.get("https://jsonplaceholder.typicode.com/posts/#{[1,2,3,4,5].sample}") response = HTTParty.get("https://jsonplaceholder.typicode.com/posts/3")
content_type = response.headers['content-type'] content_type = response.headers['content-type']
expect(content_type).to match(/application\/json/) expect(content_type).to match(/application\/json/)
end
- adicione no seu arquivo spec/models/customer_spec.rb:
# ... code
RSpec.configure do |config|
# Time Helper
config.include ActiveSupport::Testing::TimeHelpers
end
# ... code
- exemplo de teste:
it 'travel_to' do
travel_to Time.zone.local(2004, 11, 23, 01, 04, 44) do
@customer = create(:customer_vip)
end
expect(@customer.created_at).to be < Time.now
end
- via comando com rspec:
bin/rspec --order random
- adicionando no arquivo .rspec:
--order random
e executando combin/rpec
- configurando no spec_helper:
- adicione no arquivo spec/spec_helper.rb:
config.order = "random"
. - exemplo de uso:
# ... code before RSpec.configure do |config| config.order = "random" #FactoryBot Lint config.before(:suite) do FactoryBot.lint end # ... code after end
- executar na mesma sequência ,que por exemplo, ocorreu um erro:
bin/rspec --seed <number>
- adicione no arquivo spec/spec_helper.rb:
rails g model Category description:string
rails g model Product description:string price:decimal category:references
- para gerar specs de model:
rails g rspec:model product
- exemplo de classe:
class Product < ApplicationRecord
belongs_to :category
validates :description, :price, :category, presence: true
def full_description
"#{self.description} - #{self.price}"
end
end
- exemplo de testes:
it 'is valid with description, price and category' do
product = create(:product)
expect(product).to be_valid
end
it 'is invalid without description' do
product = build(:product, description: nil)
product.valid?
expect(product.errors[:description]).to include("can't be blank")
end
it 'is invalid without price' do
product = build(:product, price: nil)
product.valid?
expect(product.errors[:price]).to include("can't be blank")
end
it 'is invalid without category' do
product = build(:product, category: nil)
product.valid?
expect(product.errors[:category]).to include("can't be blank")
end
it 'return a product with a full description' do
product = create(:product)
expect(product.full_description).to eq("#{product.description} - #{product.price}")
end
- adicione a gem no group test:
gem 'shoulda-matchers'
- adicione a configuração em spec/rails_helper.rb:
# Shoulda Matchers
Shoulda::Matchers.configure do |config|
config.integrate do |with|
with.test_framework :rspec
with.library :rails
end
end
- exemplos de teste:
it { is_expected.to validate_presence_of(:description) }
it { is_expected.to validate_presence_of(:price) }
it { is_expected.to validate_presence_of(:category) }
it { is_expected.to belong_to(:category) }
- gera um spec para o controller:
rails g rspec:controller customers
- exemplo de testes:
require 'rails_helper'
RSpec.describe CustomersController, type: :controller do
it 'reponds successfully' do
get :index
expect(response).to be_success
end
it 'reponds a 200 reponse' do
get :index
expect(response).to have_http_status(200)
end
end
- adicione no arquivo Gemfile:
gem 'devise'
- gere a instalação do devise:
rails generate devise:install
- adicione no seu arquivo config/environments/development.rb:
config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
- crie um model:
rails generate devise <model-name>
- gere as alterações no banco de dados:
rails db:migrate
- adicione no seu controller para exigir autenticação:
before_action :authenticate_user!
- adicione a factory para os teste:
FactoryBot.define do
factory :member do
email { Faker::Internet.email }
password {'123456'}
password_confirmation {'123456'}
end
end
- adicione no seu arquivo spec/rails_helper.rb:
config.include Devise::Test::ControllerHelpers, type: :controller
- exemplo de testes:
require 'rails_helper'
RSpec.describe CustomersController, type: :controller do
describe 'as a Guest' do
context '#index' do
it 'responds successfully' do
get :index
expect(response).to be_successful
end
it 'responds a 200 response' do
get :index
expect(response).to have_http_status(200)
end
end
it 'responds a 302 response (not authorized)' do
customer = create(:customer)
get :show, params: { id: customer.id }
expect(response).to have_http_status(302)
end
end
describe 'as Logged Member' do
it 'responds a 200 response' do
member = create(:member)
customer = create(:customer)
sign_in member
get :show, params: { id: customer.id }
expect(response).to have_http_status(200)
end
it 'render a :show template' do
member = create(:member)
customer = create(:customer)
sign_in member
get :show, params: { id: customer.id }
expect(response).to render_template(:show)
end
end
end
it 'with valid attributes' do
sign_in @member
customer_params = attributes_for(:customer)
expect {
post :create, params: { customer: customer_params }
}.to change(Customer, :count).by(1)
end
it 'Flash Notice' do
customer_params = attributes_for(:customer)
sign_in @member
post :create, params: { customer: customer_params }
expect(flash[:notice]).to match(/successfully created/)
end
it 'Content-Type JSON' do
customer_params = attributes_for(:customer)
sign_in @member
post :create, format: :json, params: { customer: customer_params }
expect(response.content_type).to eq('application/json')
end
- exemplo com atributos inválidos:
it 'with invalid attributes' do
customer_params = attributes_for(:customer, address: nil)
sign_in @member
expect{
post :create, params: { customer: customer_params }
}.not_to change(Customer, :count)
end
- Shoulda Matchers
- Exemplo:
it 'Route' do
is_expected.to route(:get, '/customers').to(action: :index)
end
- http://teamcapybara.github.io/capybara/
- https://github.com/teamcapybara/capybara
rails generate rspec:feature customers
- exemplo de teste:
it 'Visit index page' do
visit(customers_path)
expect(page).to have_current_path(customers_path)
end
- Debugging:
print page.html
save_and_open_page
page.save_screenshot('screenshot.png')
save_and_open_screenshot
- confugre o teste conforme o exemplo abaixo:
RSpec.feature "Customers", type: :feature, js: true do
# .. code
end
- instale o chromium-chromedriver
#linux
apt-get install chromium-chromedriver
- adicione as gems:
gem 'selenium-webdriver'
#gem 'chromedriver-helper' #somente no vagrant
- instale as dependências:
bundle install
- adicione no arquivo spec_helper.rb:
# Capybara Chrome Headless
Capybara.register_driver :chrome do |app|
Capybara::Selenium::Driver.new app, browser: :chrome,
options: Selenium::WebDriver::Chrome::Options.new(args: %w[headless disable-gpu])
end
Capybara.javascript_driver = :chrome
- atualize a configuração do VCR no spec_helper.rb:
VCR.configure do |config|
config.cassette_library_dir = "spec/vcr/vcr_cassettes"
config.hook_into :webmock
config.configure_rspec_metadata!
config.filter_sensitive_data('<API-URL>') { 'https://jsonplaceholder.typicode.com' }
config.ignore_localhost = true
end
- adicione o teste:
RSpec.feature "Customers", type: :feature, js: true do
it 'Visit index page' do
visit(customers_path)
page.save_screenshot('screenshot.png')
expect(page).to have_current_path(customers_path)
end
end
- adicione a configuração do capybara no rails_helper.rb
#... code
config.include Warden::Test::Helpers
- exemplo de teste:
it 'Creates a Customer' do
member = create(:member)
login_as(member, :scope => :member)
visit(new_customer_path)
fill_in('Name', with: Faker::Name.name)
fill_in('Email', with: Faker::Internet.email)
fill_in('Address', with: Faker::Address.street_address)
click_button('Create Customer')
expect(page).to have_content('Customer was successfully created.')
end
- utilize o Dev Tools e clique com o botão direito no elemento HTML na aba Elements e selecione a opção
Copy > Copy XPath
- configure a o capybara para aguardar 5 segundos até o carregamento da view. Adicione a seguinte linha no seu spec/spec_helper.rb:
Capybara.default_max_wait_time = 5
- configure sua view para aguardar 3 segundos e simular o carregamento mais lento. Adicione o seguinte no seu arquivo app/views/customers/index.html.erb:
<br/>
<a href="#" id="my-link">Add Message</a>
<div id="my-div"></div>
<script>
var $link = document.querySelector("#my-link");
$link.addEventListener('click', function(e){
e.preventDefault();
setTimeout(function(){
document.querySelector("#my-div").innerHTML = "<h1>Yes!<h1>";
}, 3000);
}, false)
</script>
- exemplo de teste:
it 'Ajax' do
visit(customers_path)
click_link('Add Message')
expect(page).to have_content('Yes!')
end
- exemplo de teste usando find para encontrar o id da
div
:
it 'Find' do
visit(customers_path)
click_link('Add Message')
expect(find('#my-div').find('h1')).to have_content('Yes!')
end
- https://www.rubydoc.info/github/jnicklas/capybara/Capybara/RSpecMatchers
- https://gist.github.com/tomas-stefano/6652111
- crie o arquivo spec/support/new_customer_form.rb:
class NewCustomerForm
include Capybara::DSL # Capybara
include FactoryBot::Syntax::Methods # FactoryBot
include Warden::Test::Helpers # Devise
include Rails.application.routes.url_helpers # Routes
def login
member = create(:member)
login_as(member, :scope => :member)
self
end
def visit_page
visit(new_customer_path)
self
end
def fill_in_with(params = {})
fill_in('Name', with: params.fetch(:name, Faker::Name.name))
fill_in('Email', with: params.fetch(:email, Faker::Internet.email))
fill_in('Address', with: params.fetch(:address, Faker::Address.street_address))
self
end
def submit
click_button('Create Customer')
end
end
- exemplo de teste:
it 'Creates a Customer - Page Object Pattern' do
new_customer_form = NewCustomerForm.new
new_customer_form.login.visit_page.fill_in_with(
name: 'Faker::Name.name',
email: 'Faker::Internet.email',
address: 'Faker:Address.street_address'
).submit()
expect(page).to have_content('Customer was successfully created.')
end
- crie os testes de request:
rails g rspec:request customers
- adicione a seguinte gem no Gemfile:
gem 'rspec-json_expectations'
- exemplo de testes:
it "works! 200 OK" do
get customers_path
expect(response).to have_http_status(200)
end
it "index - JSON 200 OK" do
get "/customers.json"
expect(response).to have_http_status(200)
expect(response.body).to include_json([
id: 1,
name: "Bria Lynch",
email: "meu_email-1@email.com",
])
end
it "show - JSON 200 OK" do
get "/customers/1.json"
expect(response).to have_http_status(200)
expect(response.body).to include_json(
id: 1
)
end
- exemplos de testes com valores genéricos:
it "index - JSON 200 OK" do
get "/customers.json"
expect(response).to have_http_status(200)
expect(response.body).to include_json([
id: /\d/,
name: (be_kind_of String),
email: (be_kind_of String),
])
end
it "show - JSON 200 OK" do
get "/customers/1.json"
expect(response).to have_http_status(200)
expect(response.body).to include_json(
id: /\d/,
name: (be_kind_of String),
email: (be_kind_of String)
)
end
- exemplo de POST:
it 'create - JSON' do
member = create(:member)
login_as(member, scope: :member)
headers = { "ACCEPT" => "application/json" }
customers_params = attributes_for(:customer)
post "/customers.json", params: { customer: customers_params }, headers: headers
expect(response.body).to include_json(
id: /\d/,
name: customers_params[:name],
email: customers_params.fetch(:email)
)
end
- exemplo de requisição com PATCH:
it 'update - JSON' do
member = create(:member)
login_as(member, scope: :member)
headers = { "ACCEPT" => "application/json" }
customers = Customer.first
customers.name += " - ATUALIZADO"
patch "/customers/#{customers.id}.json", params: { customer: customers.attributes }, headers: headers
expect(response.body).to include_json(
id: /\d/,
name: customers.name,
email: customers.email
)
end
- exemplo de teste com delete
it 'destroy - JSON' do
member = create(:member)
login_as(member, scope: :member)
headers = { "ACCEPT" => "application/json" }
customers = Customer.first
expect { delete "/customers/#{customers.id}.json", params: { customer: customers.attributes }, headers: headers }.to change(Customer, :count).by(-1)
expect(response).to have_http_status(204)
end
- exemplo de uso:
it "show - Rspec puro + JSON" do
get "/customers/1.json"
response_body = JSON.parse(response.body)
expect(response_body.fetch("id")).to eq(1)
expect(response_body.fetch("name")).to be_kind_of(String)
expect(response_body.fetch("email")).to be_kind_of(String)
end
- adicione no arquivo Gemfile:
gem 'json_matchers'
- adicione no arquivo spec/spec_helper.rb:
require "json_matchers/rspec"
- adicione o arquivo spec/support/api/schemas/customer.json com seguinte conteúdo:
{
"type": "object",
"properties": {
"customer" : {
"required" : ["id", "name", "email"],
"properties" : {
"id" : { "type" : "integer" },
"email" : { "type" : "string" },
"name" : { "type" : "string" },
"created_at" : { "type" : "string", "format": "date-time" },
"updated_at" : { "type" : "string", "format": "date-time" }
}
}
}
}
- crie o projeto:
rails _5.2.2_ new tdd_app -T
- adicione as gems em development/test:
gem 'rspec-rails', '~> 3.6'
gem 'capybara'
- adicione a gem em development:
gem 'spring-commands-rspec'
- instale o rspec:
rails g rspec:install
- adicione a configuração no arquivo .rspec:
--format documentation
- crie os bancos de dados:
rails db:create:all
- gere o binstub:
bundle exec spring binstub rspec
- adicione em config/application.rb:
config.generators do |g|
g.test_framework :rspec,
fixtures: false,
view_specs: false,
helper_specs: false,
routing_specs: false
end
- crie a feature:
rails g rspec:feature welcome
- adicione o conteúdo do teste no arquivo spec/features/welcome_spec.rb:
require 'rails_helper'
feature "Welcome", type: :feature do
scenarion 'Mostra a mensagem de Bem-Vindo' do
visit('/')
expect(page).to have_content('Bem-Vindo')
end
end
- adicione a rota:
root to: 'welcome#index'
- adicione o controller:
class WelcomeController < ApplicationController
def index
end
end
- adicone na view app/view/welcome/index.html.erb:
<h1>Seja Bem-Vindo!!!</h1>