Começando com TDD

     Vinícius Bail Alonso

Quem sou eu

  • Desenvolvedor Web Full Stack                                                                                                                                                           
  • Graduado em Sistemas para Internet (UTFPR)                             
  • Membro do GURU/PR, PHPPR e Pato Livre

Nós somos Softfocus!

Adivinha de onde somos?

Sumário

  • Introdução
  • Alguns tipos de testes
  • Dúvidas comuns
  • Frameworks de Testes
  • RSpec
  • Nosso primeiro teste
  • Baby Steps
  • Entendendo o xUnit
  • Conhecendo melhor o RSpec
  • Carrinho de compras
  • Mocks e Stubs
  • Testando uma aplicação web

Test Driven Development

Um pouco de história

  • TDD: by example

Kent Beck

  • Em 1994 escreveu o SUnit (Smalltalk)
  • 1995 apresenta o TDD na OOPSLA
  • Em 2000 surge o JUnit (Java)
  • Junto com Erich Gamma publicou "Test Infected"

Introdução

  • Um pouco de história
  • O que é na prática?
  • Por que escrever testes?

Atenção!

TDD não é sobre testes

é sobre design

O que é TDD na prática?

Alguns tipos de testes

  • Testes Unitários
  • Testes de Integração
  • Testes de Sistema   
  • Testes de Aceitação

Porque escrever testes?

  • Foco no teste e não na implementação
  • Código nasce testado
  • Simplicidade
  • Melhor reflexão sobre o design de classes
  • Tornam os projetos mais simples de manter a longo prazo

Testes Unitários

  • Testa um componente de forma isolada             
  • Em um sistema OO é comum que seja uma classe  
  • Testa apenas uma classe sem interação com componentes externos 
  • São os testes mais simples de serem escritos

Testes Unitários

class Person
  def say_hello
    "Hello!"
  end
end
require 'spec_helper'
require_relative '../app/person'

describe Person do
  it '#say_hello' do
    person = Person.new
    expect(person.say_hello).to eq('Hello!')
  end
end

examplos/app/person.rb

examplos/spec/person_spec.rb

Testes de Integração

  • Testam a integração entre duas partes do sistema
  • Exemplo: escrita de arquivos, acesso a banco de dados, etc

Testes de Integração

class Field < ApplicationRecord
  scope :get_ordened, -> { select(:name, :id, :area).order(:name) }
end
  describe ".get_ordened" do
    it 'return fields ordened' do
      factories = %i(soy_field aba_field)
      culture = FactoryGirl.create(:soy)
      factories.each { |f| FactoryGirl.create(f, culture: culture) }

      ordened = described_class.get_ordened
      expect(ordened.first.name).to eq("Abacate Talhão")
      expect(ordened.last.name).to eq("Soja Talhão")
    end
  end

Testes de Sistema

  • Testes que simulam a interação do usuário com o sistema
  • Por exemplo:                                                                    
    • Clica em links
    • Preenche e envia formulários
  • A vantagem desses testes é que testam a aplicação como um todo, encontrando problemas que só ocorrem no mundo real

Testes de Sistema

require 'rails_helper'
 
feature 'User signs in' do
  given!(:user) { FactoryGirl.create(:user) }
 
  scenario 'with valid credentials' do
    visit root_path
    fill_in 'Email', with: user.email
    fill_in 'Password', with: user.password
    check 'Remember me'
    click_button 'Sign in'
    expect(page).to have_content "Welcome back, #{user.first_name}!"
  end
end

Testes de Aceitação

  • São uma extensão dos testes de sistema                   
  • Servem para aplicar critérios de aceitação em requisitos implementados

Testes de Aceitação

scenario "User pass his data and create a new account" do
    visit "/users/sign_up"

    fill_in "user_email",                  with: "test@hotmail.com"
    fill_in "user_password",               with: 12345678, match: :prefer_exact
    fill_in "user_password_confirmation",  with: 12345678, match: :prefer_exact
    fill_in "user_name",                   with: "Juca"
    fill_in "user_cpf",                    with: "357.548.846-05"
    fill_in "user_phone",                  with: "(42) 99900-1122"


    click_button "Registrar-se"
    expect(current_path).to eq(root_path)
  end

Modelo V

Pirâmide de Testes

Dúvidas comuns

Dúvidas comuns

  • Não vou demorar mais para entregar algo se estiver escrevendo testes?
  • No início, provavelmente sim, pela falta de prática     
  • Porém, essa prática nos ajuda a encontrar bugs mais cedo, tornando sua correção mais barata

Dúvidas comuns

  • Devo usar TDD o tempo todo?                                      
  • Cuidado com as palavras sempre e nunca em Engenharia de Software

Frameworks de Testes

RSpec

  • Framework de testes escrito em Ruby                         
  • Os testes são escritos de uma forma mais natural   
  • As saídas são bem formatadas facilitando a leitura

Nosso Primeiro Teste

  • Vamos fazer um programa que verifica se uma string é um palíndromo
  • Um palíndromo é uma palavra ou frase que pode ser lida em ambos os sentidos

Nosso Primeiro Teste

Iniciando a aplicação

# Criando um novo diretório para o programa
minicurso@tdd:~$ mkdir palindrome

# Acessando o diretório criado
minicurso@tdd:~$ cd palindrome/

# Iniciando a suite de testes RSpec
minicurso@tdd:~/palindrome$ rspec --init
  create   .rspec
  create   spec/spec_helper.rb

# Criando um diretório para o código do programa
minicurso@tdd:~/fizz-buzz$ mkdir app

# Listando os diretórios
minicurso@tdd:~/palindrome$ ls
app  spec

Nosso Primeiro Teste

  • Vamos começar pelo básico                                          
  • Criando uma classe responsável por verificar            
    • Se chamará Checker
    • Deverá possuir um método chamado is_palindrome?
    • Pelo qual receberá uma string retornando um boleano

Nosso Primeiro Teste

  • Vamos começar testando o caso mais simples          
  • A classe deve retornar true para o palíndromo "arara"

Nosso Primeiro Teste

  • Agora testamos um caso de uma palavra que não seja palíndromo
  • A classe deve retornar false para a palavra "test"     

Nosso Primeiro Teste

  • Agora vamos testar nosso método com frases que são palíndromos
  • A classe deve retornar true para a frase "roma me tem amor"     

Nosso Primeiro Teste

  • Até agora não tratamos palavras com letras maiúsculas e minúsculas
  • A classe deve retornar true para a frase "Roma Me tEm amOr"     

Nosso Primeiro Teste

  • Agora testaremos nossa classe com uma frase que tenha caracteres especiais
  • A classe deve retornar true para a frase "Socorram-me, subi no ônibus em Marrocos"     

Nosso Primeiro Teste

  • Por fim, deve quando o usuário enviar um String vazia devemos retornar false

Nosso Primeiro Teste

  • Agora que já criamos alguns casos de testes vamos refatorar o código da classe Checker
  • E o código do nosso arquivo de testes                        

Baby Steps

  • Note que começamos a testar nossa funcionalidade pelo caso mais simples
  • Essa prática é chamada baby steps                              
  • Em seguida, fomos testando casos um pouco mais complicados
  • Sempre escrevendo o código mínimo necessário para passar no teste

Conhecendo o xUnit

  • As suites de testes que seguem padrão:                     
    • Setup
    • Exercise
    • Verify
    • Teardown
  • Quando interagem com o SUT (System Under Test) 
  • Dizemos que eles fazem parte da família xUnit         

Conhecendo o xUnit

  • Setup                                                                                  
    • Preparamos o ambiente para começar o teste
  • Exercise                                                                             
    • Interagimos com o SUT a fim de gerar o comportamento à ser testado
  • Verify                                                                                  
    • Verificamos se o comportamento teve o resultado esperado
  • Teardown                                                                          
    • O ambiente volta ao normal antes do teste

Conhecendo o xUnit

describe Person do
  describe "#save" do
    it "a new Person" do
      #setup
      person = Person.new params
      #exercise
      person.save
      #verify
      expect(person.new_record?).to be true
    end
    #teardown
    after do
      Person.destroy_all
    end
  end
end

Conhecendo melhor o RSpec

  • O RSpec é uma ferramenta de Behaviour Driven Development (BDD) para Ruby
  • Fornece vários métodos para facilitar a descrição do comportamento de uma classe
    • describe
    • context
    • it

Conhecendo melhor o RSpec

  • describe                                                                             
    • É utilizado para descrever uma classe ou método
    • Exemplo:
      • describe Account
      • describe '#create'

Conhecendo melhor o RSpec

  • context                                                                             
    • É utilizado para agrupar casos de teste em um contexto específico
    • Melhorando a semântica de nossa bateria de testes
    • Exemplo:
      • context 'with valid params'
      • context 'user is logged'

Conhecendo melhor o RSpec

  • it                                                                                          
    • É onde os casos de testes são executados
    • E o comportamento descrito em um contexto é verificado
    • Exemplo:
      • it 'render to root page'
      • it 'create a new tweet'

Carrinho de compras

  • Foi solicitado para que seja desenvolvido um carrinho de compras
  • O usuário deve ser capaz de                                          
    • Adicionar um produto ao seu carrinho
    • Remover um produto do seu carrinho
    • Somar o valor total do carrinho

Carrinho de compras

  • Após estudar o domínio do problema decidimos que seriam necessárias 3 classes
    • Product
      • Representa o produto
    • Item
      • Representa um item no carrinho de compras
    • Cart
      • Representa o carrinho de compras

Carrinho de compras

  • Vamos começar pela classe Product                            
  • Ela deve ser capaz de ser instanciada e depois retornar os valores de seus atributos
    • nome
    • preço

Carrinho de compras

  • Em seguida vamos desenvolver a classe Item                       
  • Ela deve ser capaz de ser instanciada e depois retornar os valores de seus atributos
    • produto
    • quantidade

Carrinho de compras

  • Agora devemos desenvolver a classe Cart                
  • Ela deve ser instanciada recebendo por parâmetro
    • dono
    • itens
  • Ela deve ser capaz de                                                      
    • Adicionar um produto
    • Remover um produto
    • Somar o valor total do carrinho

Bug encontrado

  • Foi encontrado um bug em produção!                        
  • Quando o valor de um produto é alterado os valores dos produtos que já estavam nos carrinhos dos clientes são alterados também
  • Precisamos arrumar isso imediatamente!                  

Reflexão

  • Os testes já escritos ajudaram a encontrar o problema com mais facilidade?
  • Após resolver o problema, o fato de estar tudo testado lhe trouxe mais segurança?

Reflexão

  • Mesmo utilizando TDD o software ainda apresenta erros. Devo abandonar a técnica?
    • Não!
    • Durante a escrita de testes prevemos apenas os fluxos óbvios
    • Muitos fluxos de executação não podem ser previstos pelo desenvolvedor

Mocks e Stubs

  • É muito comum precisarmos substituir algum colaborador do SUT em algumas situações:
    • Instanciar o colaborador é muito complicado
    • Instanciar o colaborador real pode trazer consequências ruins
      • Enviar um e-mail durante os testes
    • Você quer verificar o comportamento ao invés do estado
  • Quando nossos testes precisam substituir colaboradores do SUT, chamamos isso de Test Doubles

Mocks e Stubs

  • Existem muitos tipos de Test Doubles                         
    • Mock Objects
    • Stub
    • Spy
    • Fake Object
    • Dummy
  • Nesse minicurso estudaremos apenas dois: mock e stub

Mocks e Stubs

  • Podemos testar nossas classes de duas formas        
    • Por estado
    • Por comportamento
  • Quando desejamos testar o estado utilizamos stub
  • Quando desejamos testar o comportamento utilizamos mock

Mocks e Stubs

  • Os stubs são utilizados na fase do setup do nosso padrão xUnit
  • No pequeno projeto do carrinho de compras            
  • Já utilizamos stubs em um de nossos exemplos        

Mocks e Stubs

describe "#total_value" do
    context 'with 2 items' do
      it 'the sum should be 3500' do
        laptop       =  instance_double('Product', name: 'Notebook', price: 3000)
        item_laptop  =  instance_double('Item', product: laptop, quantity: 1)

        book       =  instance_double('Product', name: 'Livro', price: 50)
        item_book  =  instance_double('Item', product: book, quantity: 10)

        allow(item_laptop).to receive(:total_value).and_return(3000)
        allow(item_book).to receive(:total_value).and_return(500)


        cart.add_item(item_laptop)
        cart.add_item(item_book)

        expect(cart.total_value).to eq(3500)
      end
    end
  end

Exemplo utilizando stub

Mocks e Stubs

describe "#total_value" do
    context 'with 2 items' do
      it 'the sum should be 3500' do
        laptop       =  instance_double('Product', name: 'Notebook', price: 3000)
        item_laptop  =  instance_double('Item', product: laptop, quantity: 1)

        book       =  instance_double('Product', name: 'Livro', price: 50)
        item_book  =  instance_double('Item', product: book, quantity: 10)

        allow(item_laptop).to receive(:total_value).and_return(3000)
        allow(item_book).to receive(:total_value).and_return(500)


        cart.add_item(item_laptop)
        cart.add_item(item_book)

        expect(cart.total_value).to eq(3500)
      end
    end
  end

Exemplo utilizando stub

Mocks e Stubs

  • Já os mocks são utilizados na fase verify do nosso padrão xUnit
  • Em seguida, um exemplo de teste utilizando mock  
  • Na classe Game que controla um jogo FizzBuzz, que é responsável por imprimir o resultado na tela

Mocks e Stubs

class Game
  # ...
  def play
    @interval.each do |number|
      puts @response.formatted number
    end
  end
end
describe '#play' do
    it 'show a single Fizz' do
      game = described_class.new (1..3)
      expect(STDOUT).to receive(:puts).with('1')
      expect(STDOUT).to receive(:puts).with('2')
      expect(STDOUT).to receive(:puts).with('Fizz')

      game.play
    end
end

Testando uma aplicação web

Testando uma aplicação web

  • Primeiro devemos clonar o projeto base                    
  • Em seguida instalando as dependências                     
$ git clone https://github.com/viniciusalonso/minicurso-tdd-app.git
$ cd minicurso-tdd-app.git/
$ bundle install

Testando uma aplicação web

  • Nossa primeira funcionalidade será um CRUD básico
    • Create
    • Read
    • Update
    • Delete
  • De uma classe chamada Post com os atributos​        
    • título (title)
    • conteúdo (content)

Testando uma aplicação web

  • Vamos começar gerando a classe Post e criando sua tabela no banco de dados
$ rails g model Post title content:text

Gerando a classe e a migration para a tabela

$ rails db:migrate

Criando a tabela no banco à partir da migration

Listagem

Testando uma aplicação web

  • Vamos começar criando uma listagem para os posts
$ rails g controller Posts

Gerando o controlador e demais arquivos

  • Gerando o controlador e testando sua action            

Testando uma aplicação web

  • Testando a action index do PostsController               
    • Devemos testar
      • Retorno do status code 200 (ok)
      • Buscar os posts no banco de dados

Testando uma aplicação web

  • Returno do status code 200                                           
describe "GET #index" do
    it 'return http success' do
      get :index
      expect(response).to have_http_status(:success)
    end
  end

spec/controllers/posts_controller_spec.rb

Failures:

  1) PostsController GET #index return http success
     Failure/Error: get :index
     
     ActionController::UrlGenerationError:
       No route matches {:action=>"index", :controller=>"posts"}
$ rspec spec/controllers/posts_controller_spec.rb

Testando uma aplicação web

  • Returno do status code 200                                           
resources :posts, only: :index

config/routes.rb

Failures:

  1) PostsController GET #index return http success
     Failure/Error: get :index
     
     AbstractController::ActionNotFound:
       The action 'index' could not be found for PostsController
$ rspec spec/controllers/posts_controller_spec.rb

Testando uma aplicação web

  • Returno do status code 200                                           
resources :posts, only: :index

config/routes.rb

Failures:

  1) PostsController GET #index return http success
     Failure/Error: get :index
     
     AbstractController::ActionNotFound:
       The action 'index' could not be found for PostsController
$ rspec spec/controllers/posts_controller_spec.rb

Testando uma aplicação web

  • Returno do status code 200                                           
def index
end

app/controllers/posts_controller.rb

Failures:

  1) PostsController GET #index return http success
     Failure/Error: get :index
     
     ActionController::UnknownFormat:
       PostsController#index is missing a template for this request format and variant.
     
       request.formats: ["text/html"]
       request.variant: []
$ rspec spec/controllers/posts_controller_spec.rb

Testando uma aplicação web

  • Returno do status code 200                                           
$ rails spec/controllers/posts_controller_spec.rb
$ touch app/views/posts/index.html.erb

Testando uma aplicação web

  • Buscar os posts no banco de dados                             
# Adicionar na linha 19
config.include FactoryGirl::Syntax::Methods

spec/rails_helper.rb

  • Nesse teste será necessário criar registro no banco de dados, por isso vamos precisar da biblioteca factory_girl

Testando uma aplicação web

  • Buscar os posts no banco de dados                             
let(:post) { FactoryGirl.create(:post) }
it 'assigns @posts' do
  get :index
  expect(assigns(:posts)).to include(post)
end

spec/controllers/posts_controller_spec.rb

Testando uma aplicação web

  • Buscar os posts no banco de dados                             
Failures:

  1) PostsController GET #index assigns @posts
     Failure/Error: expect(assigns(:posts)).to include(post)
       expected nil to include 
       #<Post id: 1, title: "MyString",
       content: "MyText", 
       created_at: "2017-09-24 23:15:56", 
       updated_at: "2017-09-24 23:15:56">,
       but it does not respond to `include?`
$ rails spec/controllers/posts_controller_spec.rb

Testando uma aplicação web

  • Buscar os posts no banco de dados                             
def index
  @posts = Post.all
end

app/controllers/posts_controller.rb

$ rspec spec/controllers/posts_controller_spec.rb

Testando uma aplicação web

  • Testando a listagem de posts na view                         
require "rails_helper"

RSpec.describe "posts/index" do
  it "displays all posts" do
    assign(:posts, [
      FactoryGirl.create(:post, title: 'My title 1'),
      FactoryGirl.create(:post, title: 'My title 2'),
      FactoryGirl.create(:post, title: 'My title 3')
    ])

    render

    expect(rendered).to match /My title 1/
    expect(rendered).to match /My title 2/
    expect(rendered).to match /My title 3/
  end
end

spec/views/posts/index.html.erb_spec.rb

Create

Testando uma aplicação web

  • Agora que possuimos uma listagem criada e testada, vamos implementar a criação de posts
  • Essa funcionalidade deverá ser testada em duas partes
    • Renderização do formulário
    • Criação de um novo registro no banco de dados

Testando uma aplicação web

  • Renderização do formulário                                          
  • Vamos usar uma biblioteca para gerar formulários de uma forma mais simples
gem 'simple_form'

Gemfile

$ bundle install

Comando para instalar as dependêndias no rails

$ rails g simple_form:install

Instalando a biblioteca no projeto

Testando uma aplicação web

  • Renderização do formulário                                          
describe "GET #new" do
  it 'instance a new Post' do
    get :new
    expect(assigns(:post)).to be_a_new(Post)
  end
end

spec/controllers/posts_controller_spec.rb

$ rspec spec/controllers/posts_controller_spec.rb

Testando uma aplicação web

  • Renderização do formulário                                          
Failures:

  1) PostsController GET #new instance a new Post
     Failure/Error: get :new
     
     ActionController::UrlGenerationError:
       No route matches
      {:action=>"new", :controller=>"posts"}

Testando uma aplicação web

  • Renderização do formulário                                          
resources :posts, only: [:index, :new]

config/routes.rb

$ rspec spec/controllers/posts_controller_spec.rb

Testando uma aplicação web

  • Renderização do formulário                                          
Failures:

  1) PostsController GET #new instance a new Post
     Failure/Error: get :new
     
     AbstractController::ActionNotFound:
       The action 'new' could not be 
       found for PostsController

Testando uma aplicação web

  • Renderização do formulário                                          
def new
  @post = Post.new
end

app/controllers/posts_controller.rb

Failures:

  1) PostsController GET #new instance a new Post
     Failure/Error: get :new
     
     ActionController::UnknownFormat:
       PostsController#new is missing a template for this request format and variant.
     
       request.formats: ["text/html"]
       request.variant: []

Testando uma aplicação web

  • Testando a listagem de posts na view                         
<%= simple_form_for(@post) do |post| %>
  <%= f.input :title  %>
  <%= f.input :content  %>
  <%= f.button :submit %>
<% end %>

spec/views/posts/index.html.erb_spec.rb

$ rspec spec/controllers/posts_controller_spec.rb

Testando uma aplicação web

  • Criação de um novo registro no banco de dados      
describe "POST #create" do
  it 'create new Post' do
    expect do
      post :create,
      params: {post: {title: 'My post', 
                             content: 'My content'}}
    end.to change{ Post.count }.by(1)
  end
end

spec/controllers/posts_controller_spec.rb

$ rspec spec/controllers/posts_controller_spec.rb

Testando uma aplicação web

  • Criação de um novo registro no banco de dados      
Failures:

  1) PostsController POST #create create new Post
     Failure/Error: 
     post :create,
          params: {title: 'My post', content: 'My content'}
     
     ActionController::UrlGenerationError:
       No route matches {:action=>"create", :content=>"My content", 
                         :controller=>"posts", :title=>"My post"}

Testando uma aplicação web

  • Renderização do formulário                                          
resources :posts, only: [:index, :new, :create]

config/routes.rb

$ rspec spec/controllers/posts_controller_spec.rb

Testando uma aplicação web

  • Criação de um novo registro no banco de dados      
Failures:

  1) PostsController POST #create create new Post
     Failure/Error:
       expect do
         post :create, params: {title: 'My post', content: 'My content'}
       end.to change{ Post.count }.by(1)
     
       expected `Post.count` to have changed by 1, but was changed by 0

Testando uma aplicação web

  • Criação de um novo registro no banco de dados      
def create
  @post = Post.new post_params
  @post.save
  redirect_to posts_path
end

private
def post_params
  params.require(:post).permit(:title, :content)
end

app/controllers/posts_controller.rb

$ rspec spec/controllers/posts_controller_spec.rb

Update

Testando uma aplicação web

  • Renderização do formulário                                          
describe "GET #edit" do
  let(:post) { FactoryGirl.create(:post) }

  it 'assigns @post' do
    get :edit, params: { id: post.id }
    expect(assigns(:post)).to eq(post)
  end
end

spec/controllers/posts_controller_spec.rb

$ rspec spec/controllers/posts_controller_spec.rb

Testando uma aplicação web

  • Renderização do formulário                                          
Failures:

  1) PostsController GET #edit assigns @post
     Failure/Error: get :edit, params: { id: post.id }
     
     ActionController::UrlGenerationError:
       No route matches 
     {:action=>"edit", :controller=>"posts", :id=>1}

Testando uma aplicação web

  • Renderização do formulário                                          
resources :posts, only: [:index, :new, :create, :edit]

config/routes.rb

$ rspec spec/controllers/posts_controller_spec.rb

Testando uma aplicação web

  • Renderização do formulário                                          
Failures:

  1) PostsController GET #edit assigns @post
     Failure/Error: 
      get :edit, params: { id: post.id }
     
     AbstractController::ActionNotFound:
       The action 'edit' could 
       not be found for PostsController

Testando uma aplicação web

  • Renderização do formulário                                          
def edit
  @post = Post.find(params[:id])
end

app/controllers/posts_controller.rb

Failures:

  1) PostsController GET #edit assigns @post
     Failure/Error: get :edit, params: { id: post.id }
     
     ActionController::UnknownFormat:
       PostsController#edit is missing a template for this request format and variant.
     
       request.formats: ["text/html"]
       request.variant: []

Testando uma aplicação web

  • Renderização do formulário                                          
Failures:

  1) PostsController GET #edit assigns @post
     Failure/Error: get :edit, params: { id: post.id }
     
     ActionController::UnknownFormat:
       PostsController#edit is missing a 
       template for this request format and variant.
     
       request.formats: ["text/html"]
       request.variant: []

Testando uma aplicação web

  • Atualização do registro no banco de dados                
describe "PATCH #update" do
  let(:post) { FactoryGirl.create(:post) }

  it 'update post' do
    params = { id: post.id, post: { title: 'Meu novo título' } }
    patch :update, params: params
    post.reload.title
    expect(post.title).to eq('Meu novo título')
  end
end

spec/controllers/posts_controller_spec.rb

$ rspec spec/controllers/posts_controller_spec.rb

Testando uma aplicação web

Failures:

  1) PostsController PATCH #update update post
     Failure/Error: patch :update, params: params
     
     ActionController::UrlGenerationError:
       No route matches
      {:action=>"update", :controller=>"posts", 
      :post=>{:id=>1, :title=>"Meu novo título"}}
  • Atualização do registro no banco de dados                

Testando uma aplicação web

resources :posts, except: :destroy

config/routes.rb

$ rspec spec/controllers/posts_controller_spec.rb
  • Atualização do registro no banco de dados                

Testando uma aplicação web

Failures:

  1) PostsController PATCH #update update post
     Failure/Error: put :update, params: params
     
     AbstractController::ActionNotFound:
       The action 'update' could not be found for PostsController
  • Atualização do registro no banco de dados                

Testando uma aplicação web

def update
  @post = Post.find(params[:id])
  @post.update(post_params)
  redirect_to posts_path
end

app/controllers/posts_controller.rb

$ rspec spec/controllers/posts_controller_spec.rb
  • Atualização do registro no banco de dados                

Delete

Testando uma aplicação web

  • Deletando um registro do banco de dados                
describe "DELETE #destroy" do
  it 'delete post' do
    attrs = FactoryGirl.attributes_for(:post)
    post = Post.create(attrs)
    expect do
      delete :destroy, params: { id: post.id }
    end.to change{ Post.count }.by(-1)
  end
end

spec/controllers/posts_controller_spec.rb

$ rspec spec/controllers/posts_controller_spec.rb

Testando uma aplicação web

Failures:

  1) PostsController DELETE #destroy delete post
     Failure/Error: delete :destroy, params: { id: post.id }
     
     ActionController::UrlGenerationError:
       No route matches {:action=>"destroy", :controller=>"posts", :id=>1}
  • Deletando um registro do banco de dados                

Testando uma aplicação web

resources :posts

config/routes.rb

$ rspec spec/controllers/posts_controller_spec.rb
  • Deletando um registro do banco de dados                

Testando uma aplicação web

Failures:

  1) PostsController DELETE #destroy delete post
     Failure/Error: delete :destroy, params: { id: post.id }
     
     AbstractController::ActionNotFound:
       The action 'destroy' could not be found for PostsController
  • Deletando um registro do banco de dados                

Testando uma aplicação web

def destroy
  @post = Post.find(params[:id])
  @post.destroy
  redirect_to posts_path
end

app/controllers/posts_controller.rb

$ rspec spec/controllers/posts_controller_spec.rb
  • Atualização do registro no banco de dados                

Read

Testando uma aplicação web

  • Lendo um registro do banco de dados                        
describe "GET #show" do
  let(:post) { FactoryGirl.create(:post) }

  it 'assigns @post' do
    get :show, params: { id: post.id }
    expect(assigns(:post)).to eq(post)
  end
end

spec/controllers/posts_controller_spec.rb

$ rspec spec/controllers/posts_controller_spec.rb

Testando uma aplicação web

  • Lendo um registro do banco de dados                        
def show
  @post = Post.find(params[:id])
end

app/controllers/posts_controller.rb

$ rspec spec/controllers/posts_controller_spec.rb

Testando uma aplicação web

  • Lendo um registro do banco de dados                        
<strong>
  <%= @post.title %>
</strong>

<p>
  <%= @post.content %>
</p>

<%= link_to 'Voltar', posts_path %>

app/views/posts/show.html.erb

$ rspec spec/controllers/posts_controller_spec.rb

Cobertura de testes

Testando uma aplicação web

  • Outro conceito importante é a cobertura de testes de sua aplicação
  • As ferramentas que fazem esse cálculo nos mostram qual a porcentagem do código que escrevemos que tem cobertura de testes

Testando uma aplicação web

  • Após rodar nossa suite podemos oberservar a mensagem abaixo
Coverage report generated for RSpec to 
/home/vinicius/Projects/minicurso-app/coverage. 39 / 39 LOC (100.0%) covered.
$ rspec
  • Para um relatório mais detalhado basta abrir o arquivo coverage/index.html

Testando uma aplicação web

coverage/index.html

Validações

Testando uma aplicação web

  • Por hora, nossa classe está aceitando os dados sem validá-los
  • É muito importante aceitar apenas dados confiáveis
  • Por essa razão vamos adicionar algumas validações
    • title
      • Não pode ser vazio
      • Deve ser único
    • content
      • Não pode ser vazio

Testando uma aplicação web

  • Para testar as validações vamos utilizar uma biblioteca
  #Adicionar nas linhas 15 e 16
  config.include(Shoulda::Matchers::ActiveModel, type: :model)
  config.include(Shoulda::Matchers::ActiveRecord, type: :model)

spec/rails_helper.rb

Testando uma aplicação web

  • Escrevendo testes para as validações na classe         
require 'rails_helper'

RSpec.describe Post, type: :model do
  context 'validations' do
    it { should validate_presence_of(:title) }
    it { should validate_uniqueness_of(:title) }
    it { should validate_presence_of(:content) }
  end
end

spec/models/post_spec.rb

$ rspec spec/models/post_spec.rb

Testando uma aplicação web

  • Adicionando as validações na classe                            
class Post < ApplicationRecord
  validates_presence_of :title, :content
  validates_uniqueness_of :title
end

app/models/post.rb

$ rspec spec/models/post_spec.rb

Adicionando uma home page

Testando uma aplicação web

  • Precisamos adicionar uma página inicial com uma mensagem de boas vindas para o usuário
require 'rails_helper'

RSpec.feature "HomePages", type: :feature do
  scenario "Visit the home page and see messages" do
    visit '/'
    expect(page).to have_title "Seja Bem Vindo (a)!"
    expect(page).to have_text("Bem Vindo(a)!")
  end
end

spec/features/home_pages_spec.rb

$ rspec spec/features/home_pages_spec.rb

Testando uma aplicação web

  • Precisamos adicionar uma página inicial com uma mensagem de boas vindas para o usuário
class WelcomeController < ApplicationController
  def index
  end
end

app/controllers/welcome_controller.rb

$ rspec spec/features/home_pages_spec.rb
root 'welcome#index'

config/routes.rb

Testando uma aplicação web

  • Precisamos adicionar uma página inicial com uma mensagem de boas vindas para o usuário
<!-- Linha 4 -->
<title>Seja Bem Vindo (a)!</title>

app/views/layouts/application.html.erb

$ rspec spec/features/home_pages_spec.rb
<h1>Bem Vindo(a)!</h1>

app/views/welcome/index.html.erb

Adicionando um link para a página dos posts

Testando uma aplicação web

  • Precisamos adicionar uma página inicial com uma mensagem de boas vindas para o usuário
scenario "Go from home page to posts page" do
  visit '/'
  click_on 'Posts'
  expect(page.current_path).to eq(posts_path)
end

spec/features/home_pages_spec.rb

$ rspec spec/features/home_pages_spec.rb

Testando uma aplicação web

  • Precisamos adicionar uma página inicial com uma mensagem de boas vindas para o usuário
<%= link_to 'Posts', posts_path %>

app/views/welcome.html.erb

$ rspec spec/features/home_pages_spec.rb

Adicionando autenticação

Testando uma aplicação web

  • Para adicionar autenticação vamos utilizar uma gem chamada devise
gem 'devise'

Gemfile

$ bundle install
$ rails generate devise:install

Instalando dependência

Instalando devise no projeto

Testando uma aplicação web

  • Criando model para representar o usuário                
$ rails generate devise User
$ rails db:migrate

Gerando model User

Criando tabela users no banco de dados

Testando uma aplicação web

  • Configurando ambiente de testes                                
#Linha 7
require 'devise'

#Linhas 15 e 16
config.include Devise::Test::ControllerHelpers,  type: :controller
config.include Devise::Test::IntegrationHelpers, type: :feature

spec/rails_helper.rb

Testando uma aplicação web

  • Faremos com que seja necessário estar logado para acessar qualquer página
class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  # Adicionar linha abaixo
  before_action :authenticate_user!
end

app/controllers/application_controller.rb

$ rspec

Testando uma aplicação web

  • É necessário simular um login de usuário nos testes
FactoryGirl.define do
  factory :user do
    email { Faker::Internet.email }
    password "password"
    password_confirmation "password"
  end
end

spec/factories/users.rb

  • Primeiro devemos configurar a fábrica de usuários 

Testando uma aplicação web

  • Depois simulamos o login em nossos testes              
before(:each) do
  sign_in FactoryGirl.create(:user)
end

spec/controllers/posts_controller_spec.rb

$ rspec

spec/controllers/welcome_controller_spec.rb

spec/features/home_pages_spec.rb

Cadastro de usuário

Testando uma aplicação web

  • Vamos começar gerando nosso arquivo de teste      
$ rails g rspec:feature user_register_himself
scenario "user pass the correct data" do
  visit '/users/sign_up'

  fill_in "user_email",                  with: "test@hotmail.com"
  fill_in "user_password",               with: 12345678, match: :prefer_exact
  fill_in "user_password_confirmation",  with: 12345678, match: :prefer_exact

  click_on "Sign up"

  expect(page).to have_text('Bem Vindo(a)!')
end

spec/features/user_register_himselves_spec.rb

Testando uma aplicação web

  • Caso o usuário deixe um campo vazio                        
scenario "user submit form with a blank email" do
  visit '/users/sign_up'

  fill_in "user_email",                  with: ""
  fill_in "user_password",               with: 12345678, match: :prefer_exact
  fill_in "user_password_confirmation",  with: 12345678, match: :prefer_exact

  click_on "Sign up"

  expect(page).to have_text("Email can't be blank")
end

spec/features/user_register_himselves_spec.rb

Reflexão

  • De que vale um teste que não quebra?                       
  • Quais garantias temos com os últimos testes que escrevemos?

E agora, por onde devo começar?

Cursos

Livros

Códigos utilizados

  • Palíndromo e Carrinho de compras                                   
    • https://github.com/viniciusalonso/minicurso-tdd
  • Aplicação
    • https://github.com/viniciusalonso/minicurso-tdd-app

Entre em contato

vba321@hotmail.com

Referências

Começando com TDD

By Vinícius Alonso

Começando com TDD

Minicurso apresentado no FTSL(Fórum de Tecnologia em Software Livre) em Curitiba 2017.

  • 1,291