Personal blog

Web developing.

CRUD Com Rails - Editando Um Cadastro

Seguindo nosso CRUD, vamos trabalhar nesse post, a alteração de dados (Update). Primeiramente crie o arquivo para testar o processo de alteração. Em spec/features/products crie mais um arquivo nomeado editing_products_spec.rb. Nesse arquivo adicione o seguinte código:

spec/features/products/editing_products_spec.rb
1
2
3
4
5
6
7
8
9
10
11
12
  require 'rails_helper'

  feature 'Editando Produtos' do
    before do
      visit root_path
      click_link 'Produtos'
    end

    scenario "posso editar um produto" do
      click_link "Editar"
    end
  end

Rode os testes e um erro devido a falta do link “Editar” será apresentado pelo rspec.

rspec spec/features/products/editing_products_spec.rb
1
2
3
  Failure/Error: click_link "Editar"
     Capybara::ElementNotFound:
       Unable to find link "Editar"

Precisamos adicionar o link de edição a listagem de produtos. Vamos lá, no arquivo de template app/views/products/index.html.erb adicione na listagem de dados, o link de edição:

app/views/products/index.html.erb
1
2
3
4
5
6
7
  <% @products.each do |product| %>
    <tr>
      <td><%= product.name %></td>
      <td><%= product.description %></td>
      <td><%= link_to "Editar", edit_product_path(product), class: "btn btn-default" %></td>
    </tr>
  <% end %>

Mas somente isso não é suficiente, precisamos usar o FactoryGirl novamente, para ao menos termos um registro e assim a nossa listagem ter o link. Ao teste adicione no início do bloco before:

rspec spec/features/products/editing_products_spec.rb
1
2
3
4
5
6
  before do
    product = FactoryGirl.create(:product)

    visit root_path
    click_link 'Produtos'
  end

Rode seu teste e ocorrerá um erro devido a falta de uma ação edit em nosso controlador. Adicione:

app/controllers/products_controller.rb
1
2
3
4
5
6
7
8
9
10
class ProductsController < ApplicationController

  ... Adicione antes dos metodos privados ...
  def edit

  end

  private
    ....
end

Rodando novamente os testes, teremos um erro pela falta do nosso template de edição. Adicione um arquivo de template em app/views/products/edit.html.erb. Agora nosso teste está passando. Ou seja, nossa aplicação já tem um página de edição. Nosso próximo passo e exibir um formulário com os dados e alterá-los.

Carregando os dados no formulário de edição

Editando os dados

Vamos començar pelo nosso teste, onde vamos preencher o campo nome do produto, na verdade vamos modificar o valor que deverá estar carregado. Adicione ao teste:

spec/features/products/editing_product_spec.rb
1
2
3
4
5
6
7
  scenario "posso editar um produto" do
    click_link "Editar"
    fill_in 'Nome', with: 'Produto 1'
    click_button 'Salvar'

    expect(page).to have_content('Produto foi editado.')
  end

Rode o teste. E teremos o error:

Failure/Error: fill_in ‘Nome’, with: ‘Produto 1’

 Capybara::ElementNotFound:
   Unable to find field "Nome"

Ok, não temos o nosso form e o campo Nome no formulário de edição. Vamos reaproveitar o formulário, usando o atual formulário de novo cadastro, tudo isso com o uso de partials. Crie no diretório app/views/products/ o arquivo _form.html.erb. No arquivo edit.html.erb (crie ele se existir) implemente a chamada do partial:

app/views/products/edit.html.erb
1
2
3
  <p>Editando</p>

  <%= render 'form' %>

Abra o arquivo new.html.erb e transfira o codigo do formulário para o arquivo _form.html.erb, não esquecendo de adiocionar a chamada do partial ao formulário.

app/views/products/new.html.erb
1
2
3
  <p>Criando</p>

  <%= render 'form' %>

Já o partial ficará assim:

app/views/products/_form.html.erb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  <%= form_for(@product) do |f| %>
    <%= render 'shared/errors', object: f.object %>
    <p>
      <%= f.label :name %><br />
      <%= f.text_field :name %>
    </p>
    <p>
      <%= f.label :description %><br />
      <%= f.text_field :description %>
    </p>
    <p>
       <%= f.submit "Salvar" %>
    </p>
  <% end %>

Obs: Mude o click_button do teste de novo registro para “Salvar”.

Rode o teste rspec spec/features/products/editing_products_spec.rb. Erros, erros novamente… ok mas isso é devido que temos a váriavel @product e nosso controller ela não existe. Vamos a implementação no nosso controller. Vamos encontrar o registro que queremos editar, usando o id que é passado a action edit junto com os params da requisição:

app/controllers/products_controller.rb
1
2
3
4
5
6
7
8
9
  class ProductsController < ApplicationController
    ... actions ...

    def edit
      @product = Product.find(params[:id])
    end

    ... private methods ...
  end

Rode o teste o erro agora é diferente e bem claro. Pois queremos gravar a alteração e o método de update não existe. Crie ele e adicione o código para realizar a alteração

app/controllers/products_controller.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  class ProductsController < ApplicationController
    ... actions ...

    def edit
      @product = Product.find(params[:id])
    end

    def update
      @product = Product.find(params[:id])
      if @product.update(product_params)
        redirect_to @product, notice: 'Produto foi editado.'
      end
    end

    ... private methods ...
  end

Ao rodarmos o teste, ele vai passar. Agora vamos colocar uma cenário de teste negativo. Deixaremos o campo nome em branco e vamos submeter, o campo nome é obrigatório, logo uma mensagem de validação vai ser retornada, impedindo que a alteração aconteça:

spec/features/products/editing_product_spec.rb
1
2
3
4
5
6
7
8
9
10
  ... outro cenario ...

  scenario "quando nome em branco não posso editar um produto" do
    click_link "Editar"
    fill_in 'Nome', with: ''
    click_button 'Salvar'

    expect(page).to have_content('Produto não foi alterado, verifique os erros.')
    expect(page).to have_content('Nome é muito curto (mínimo: 5 caracteres)')
  end

E para finalizar adicione uma mensagem de alerta para quando ocorrer uma falha:

app/controllers/products_controller.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  class ProductsController < ApplicationController
    ... actions ...

    def edit
      @product = Product.find(params[:id])
    end

    def update
      @product = Product.find(params[:id])
      if @product.update(product_params)
        redirect_to @product, notice: 'Produto foi editado.'
      else
        flash[:alert] = 'Produto não foi alterado, verifique os erros.'
        render :edit
      end
    end

    ... private methods ...
  end

Rode seus testes, e tudo estará verde. Finalizamos mais uma etapa, o código fonte como sempre estará no meu github. Até..

CRUD Com Rails - Listando Cadastros

Vamos começar a segunda parte, que será o carregamento dos dados. Iniciaremos mais uma vez por nossos testes, então a primeira coisa a fazer é criar o arquivo reading_products_spec.rb na pasta de features (spec/features/products/reading_products_spec.rb.

Acessando a página

Iniciaremos nosso teste com um cenário onde estaremos navegando até a página, então vamos ao trabalho. Implemente a navegação a página de listagem:

spec/features/products/reating_products_spec.rb
1
2
3
4
5
6
7
8
9
10
  require "rails_helper"

  feature "Listando Produtos" do
    scenario "listando todos os registros cadastrados" do
      visit '/'
      click_link 'Produtos'

      expect(page).to have_selector('h2', text: 'Cadastro de produtos')
    end
  end

Organizando a navegação

Vamos adicionar um link para a pagina de listagem. No partial que contém os links de navegação (app/views/layouts/_navigation_links.html.erb) substitua todo o conteúdo por:

app/views/layouts/_navigation_links.html.erb
1
  <li><%= link_to 'Produtos', products_path %></li>

Rode o teste com o comando rspec spec/features/products/reading_products_spec.rb, teremos um erro, pois ainda não criamos nossa ação index em nosso controlador de produtos:

Erro por falta da ação index no controlador
1
2
3
4
5
6
  Failures:

  1) Listando Produtos listando todos os registros cadastrados
     Failure/Error: visit products_path
     AbstractController::ActionNotFound:
       The action 'index' could not be found for ProductsController

A resolução desse erro é, criar uma ação index no nosso controlador:

app/controller/products_controller.rb
1
2
3
4
5
6
7
8
  class ProductsController < ApplicationController
    def index

    end

    ... demais código ...

  end

Rodar os testes novamente irá retornar um erro devido a falta do template para essa ação:

Erro pela falta de template
1
2
3
4
5
6
  Failures:

  1) Listando Produtos listando todos os registros cadastrados
     Failure/Error: visit products_path
     ActionView::MissingTemplate:
       Missing template products/index, application/index with {:locale=>[:"pt-BR"], :formats=>[:html], :variants=>[], :handlers=>[:erb, :builder, :raw, :ruby, :jbuilder, :coffee]}. Searched in:

Crie o template app/views/products/index.html.erb, com o conteudo:

Adicione o conteudo app/views/products/index.html.erb
1
  <h2>Cadastro de produtos</h2>

Rode o teste novamente e ele irá passar.

Carregando os dados

Na página criada vamos carregar os dados. Crie um bloco before, onde estarão os trechos de testes que se repetirão. O before já foi utilizado no post anterior, mas somente para lembrar…. o bloco before deve ser usado sempre que se precisa que algo seja carregado antes dos testes, assim como é o nosso caso:

spec/features/products/reading_products_spec.rb
1
2
3
4
5
6
7
8
9
10
11
12
  require "rails_helper"

  feature "Listando Produtos" do

    before do
      visit root_path
    end

    scenario "listando todos os registros cadastrados" do
      click_link 'Produtos'
    end
  end

Agora vamos implementar nosso cenário de carregamento dos dados. Vamos verificar se a nossa listagem possui ao menos um registro. Para isso vamos carregar nossa factory e então verificar se há ao menos um registro:

Ao menos um registro deve existir spec/features/products/reading_products_spec.rb
1
2
3
4
5
6
7
8
  scenario "listando todos os registros cadastrados" do
    @products = FactoryGirl.create_list(:product, 25)

    click_link 'Produtos'

    expect(page).to have_content(@products.first.name)
    expect(page).to have_content(@products.last.name)
  end

Aqui entra o FactoryGirl, que para quem não conhece é uma gem para facilitar a criação de fixtures e factories, evitando o uso direto do Active Record. Vamos usar para gerar uma lista de 25 produtos, assim testando a exibição.

Primeiro vamos adiocionar a gem ao projeto, então em seu arquivo Gemfile, abaixo das demais gem’s do grupo de development e test, adicione, salve e em seguida rode o comando bundle install:

Gemfile
1
2
3
  group :development, :test do
    gem 'factory_girl_rails'
  end

Instalada a gem, crie a factory para produtos. No diretorio spec crie uma pasta chamada factories e nela um arquivo com nomeado products.rb. Essa factory vai mapear o modelo Product, assim tudo que é referente a manipulação de modelos em nossos testes, serão de responsabilidade de nossa factory. Adicione os campos necessários para nosso teste:

Products spec/factories/products.rb
1
2
3
4
5
6
  FactoryGirl.define do
    factory :product do
      sequence(:name) { |n| "Produto #{n}"}
      description 'Descrição do produto 1 (um)'
    end
  end

Perceba o uso do sequence na factory, ele está é usado para nunca termos nomes iguais. Veja mais sobre o Factory Girl aqui: site

Nosso teste se executado nesse ponto, estará quebrado, pois estamos esperando um valor que ainda não está sendo apresentado na nossa página. Abaixo vamos implementar a exibição dos resultados.

Adicione na acao index do controlador o seguinte código, que carregará os dados cadastrados:

Carregando os dados app/controllers/products_controller.rb
1
2
3
4
5
6
7
8
  class ProductsController < ApplicationController
    def index
      @products = Product.all
    end

    ... Demais actions

  end

e o nosso template index ficara assim:

app/views/products/index.html.erb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  <h2>Cadastro de produtos</h2>

  <table class="table table-hover">
    <thead>
      <td>Nome</td>
      <td>Descricao</td>
    </thead>
    <tbody>
      <% @products.each do |product| %>
      <tr>
        <td><%= product.name %></td>
        <td><%= product.description %></td>
      </tr>
      <% end %>
    </tbody>
  </table>

Rode o teste rspec spec/features/reading_products_spec.rb e nossa listagem deverá funcionar. Caso quira inicie o server rails (rails s) e veja o resultado.

Ultímos ajustes

Primeiramente rode todos os testes, para garantir que tudo está funcionando ou achar ocasionais quebras. Execute o comando rspec e bingo, temos uma quebra. Ela ocorre devida a modificação do nosso arquivo de navegação, onde removemos o link para o formulário de cadastro. Para solucionar isso devemos mudar a forma como o Capybara navegava até o formulário e principalmente definir um link na página de listagem.

Na spec de cadastro (spec/features/creating_products_spec.rb) altere o bloco before para a navegação desejada:

spec/features/creating_products_spec.rb
1
2
3
4
5
  before do
    visit root_path
    click_link 'Produtos'
    click_link 'Novo Produto'
  end

Ao rodar o nosso teste, ainda permanece o mesmo erro, sendo a solução adicionar esse link em nosso template index (app/views/products/index.html.erb). Então a baixo da tag de fechamento da tabela adicione o link:

app/views/products/index.html.erb
1
2
3
4
5
6
7
  <h2>Cadastro de produtos</h2>

  <table class="table table-hover">
    ....
  </table>

  <%= link_to "Novo Produto", new_product_path, class: "btn btn-primary" %>

Agora todos os testes estão verdes. Para finalizar vamos adicionar um teste para quando não houver dados a serem retornados. Ao teste de carregamento adicione o cenário:

spec/features/reading_products_spec.rb
1
2
3
4
  scenario "exibindo mensagem de que nao ha dados" do
    click_link 'Produtos'
    expect(page).to have_content('Nenhum registro encontrado!')
  end

E ao nosso template basta colocar a validação para quando a listagem estiver vazia. Nosso template será dessa forma:

app/views/products/index.html.erb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
  <h2>Cadastro de produtos</h2>

  <div class="row">
    <% if !@products.blank? %>
    <table class="table table-hover">
      <thead>
        <td>Nome</td>
        <td>Descricao</td>
      </thead>
      <tbody>
          <% @products.each do |product| %>
            <tr>
              <td><%= product.name %></td>
              <td><%= product.description %></td>
            </tr>
          <% end %>
      </tbody>
    </table>
    <% else %>
      Nenhum registro encontrado!
    <% end %>
  </div>

  <div class="row">
    <%= link_to "Novo Produto", new_product_path, class: "btn btn-primary" %>
  </div>

Agora sim chegamos ao final. Desenvolvemos o carregamento dos dados cadastrados usando testes, com Cabybara, Rspec e FactoryGirl. No próximo post vamos ver como alterar nossos registros e como excluí-los. O código fonte desse post está no meu github, sendo separado por branches.

CRUD Com Rails - Criando Um Cadastro Usando Rspec

O acrônimo CRUD (Create, Read, Update e Delete) é algo que todo iniciante em linguagens de programação ou frameworks necessita saber, pois significa nada menos que as operações básicas para a criação/manutenção de cadastros.

O nosso primeiro passo é a parte de criação (C do CRUD). Para realizar todos os passos vamos desenvolver usando as metodologias de BDD/TDD, que irá nos possibilitar um código estável e mais fácil de manter.

Criando um cadastro

Nessa primeira história vamos criar um resource (recurso) que representa um produto, é somente para fins didáticos, o nome é o que menos interessa. Para o produto vamos ter dois atributos que são necessários: nome e descrição. O nosso primeiro passo é criar a nossa feature de spec, em spec/features (crie esse diretorios se não existirem), crie o diretório products e um arquivo com o nome de creating_products_spec.rb.

Uma dica, sempre use a nomenclatura em inglês em sua aplicação Rails, garanto que você evitará muitas dores de cabeça, pois o Rails se torna muito mais amigável na língua do tio Obama.

No arquivo criado vamos especificar o que desejamos que o nosso cadastro faça. Abaixo está o código da especificação:

spec/features/products/creating_products_spec.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
require rails_helper

feature 'Criando Produtos' do
  scenario "posso criar um produto" do
    visit '/'

    click_link 'Novo Produto'

    fill_in 'Nome', with: 'Produto 1'
    fill_in 'Descrição', with: 'Descrição do produto 1 (um)'
    click_button 'Criar produto'

    expect(page).to have_content('Produto foi criado.')
  end
end

Execute o comando rspec spec/features/products/creating_products_spec.rb. Esse comando executará a nossa feature de teste e que apresentará o seguinte erro:

Erro no bash
1
2
3
4
5
6
Failures:

  1) Criando Produtos posso criar um produto
      Failure/Error: click_link 'Novo Produto'
      Capybara::ElementNotFound:
        Unable to find link "Novo Produto"

O teste falhará pois não temos o link “Novo Produto” na rota “/”, que é a nosso root path(página inicial). Então antes de seguir, vamos criar o link na página inicial. Coloque o seguinte código dentro do arquivo _navigation_links.html.erb, que está na pasta layouts de nossas views (app/views/layouts/_navigation_links.html.erb):

app/views/layouts/_navigation_links.html.erb
1
  <li><%= link_to 'Novo Produto', new_product_path %></li>

Edite a página de layout para possuir esse link:

app/views/layouts/application.html.erb
1
  <%= render "layouts/navigation" %>

Rode os testes novamente e ocorrera um erro pois não definimos uma rota para os nossos produtos:

Erro no bash
1
2
3
  Failure/Error: visit '/'
  ActionView::Template::Error:
    undefined local variable or method 'new_product_path'

Adicione a rota no arquivo config/routes.rb:

config/routes.rb
1
  resources :products

Rode os testes novamente e acontecerá um nova quebra na nossa feature. Dessa vez é devido que a nossa suite de testes até conseguiu encontrar a rota, mas não encontrou nenhum controller que condize-se com o resource procurado. Veja o erro:

Erro no bash
1
2
3
  Failure/Error: click_link 'Novo Produto'
  ActionController::RoutingError:
    uninitialized constant ProductsController

Essa ficou fácil. Crie o controller, em app/controllers/products_controller.rb. Nesse arquivo declare um classe ruby com o nome de ProductsController extendendo de ApplicationController:

app/controllers/products_controller.rb
1
2
3
class ProductsController < ApplicationController

end

Rode novamente o teste. Problema resolvido, mas outro surgiu. Agora o rspec não conseguiu encontrar uma action no controller, que refletisse com a operação de novo cadastro. Veja:

Ação ainda não existe
1
2
3
  Failure/Error: click_link 'Novo Produto'
  AbstractController::ActionNotFound:
    The action 'new' could not be found for ProductsController

Ao controller criado instantes atrás, adicione a action new:

Ação de novo cadastro
1
2
3
4
class ProductsController < ApplicationController
  def new
  end
end

Se rodarmos novamente os testes, nossa ação é encontrada, porém o template erb não existe, ocasionando outra quebra:

Erro pois o template não encontrado
1
2
3
  Failure/Error: click_link 'Novo Produto'
  ActionView::MissingTemplate:
      Missing template products/new, application/new with ….

Devemos criar o nosso template na camada de visão do projeto. Então na pasta app/views, crie uma pasta chamada products e nela um arquivo com o nome new.html.erb. Feito isso rode o teste mais uma vez. Agora temos um problema diferente de todos até agora. O Capybara na tentativa de preencher os campos do formulário (que ainda não existe) acaba gerando um novo erro:

Não encontrou o elemento Nome
1
2
3
  Failure/Error: fill_in 'Nome', with: 'Produto 1'
  Capybara::ElementNotFound:
       Unable to find field "Nome"

Como podemos ver, o Capybara não conseguiu encontrar o elemento com o field “Nome”. Isso é lógico, nós não criamos ainda o formulário com os campos. Porém antes teremos que criar uma instância de Product em nossa controller e passar a nossa view para que ela possua os campos necessários:

Implementação da action new
1
2
3
def new
  @product = Product.new
end

A constante Product é o nosso model, que deve ser criado em app/models/product.rb. Mas antes de sair criando o arquivo, vamos acelerar as coisas usando um gerador de código do Rails.

Comando para gerar tudo relacionado ao model
1
rails g model Pproduct name:string description:string

Esse generator (gerador) nos poupa um tempo criando uma série de arquivos, como o arquivo de migração do banco de dados com os campos necessários, nosso model extendendo de ActiveRecord que contém um infinidade de funcionalidades para trabalhar com nossos dados e arquivos de testes unitários.

Um Model deve ser capaz de prover o acesso a camada de dados e é por isso que ele utiliza o Active Record. O Model também provê um local para a lógica de negócio, bem como, fazer validações e associações entre modelos.

Entre os arquivos gerados, está o arquivo de migration. Migration é um mecanismo eficiente de manter um controle de versão de nosso banco de dados, onde podemos evoluir e retrocer nosso schema quando necessário, sem trabalho adicional. Nosso arquivo de migração está é gerado dentro da pasta db/migrate e o nome leva em considereção o timestamp do momento de geração, para nunca ter o mesmo nome e o Rails conseguir controlar o historico de migrações. Nosso arquivo db/migrate/[data]_create_product.rb apresenta o seguite código Ruby:

db/migrate/[data]_create_product.rb
1
2
3
4
5
6
7
8
9
10
class CreateProducts < ActiveRecord::Migration
  def change
    create_table :products do |t|
      t.string :name
      t.string :description

      t.timestamps
    end
  end
end

Devemos rodar o comando abaixo para ter o nosso banco de dados atualizado:

Comando para atualizar nosso banco de dados
1
rake db:migrate

O esquema de nomenclatura é interessante, o Rails irá ter o model com o nome product (singular), no entanto a tabela no banco de dados será products(plural).

Com nosso model product criado e o nosso banco com a tabela necessária, vamos voltar ao formulário. Execute o teste novamente:

Rodando os testes
1
rspec spec/features/products/creating_products_spec.rb

E veremos que nada mudou desde a ultima execução, porém agora temos nosso banco de dados pronto, e um objeto do tipo Product que dará os atributos necessários para montar nosso formulário de cadastro. Abra o arquivo app/views/products/new.html.erb e adicione o código abaixo:

app/views/products/new.html.erb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<h2>New Product</h2>
<%= form_for(@product) do |f| %>
  <p>
    <%= f.label :name, "Nome" %><br />
    <%= f.text_field :name %>
  </p>
  <p>
    <%= f.label :description, "Descrição" %><br />
    <%= f.text_field :description %>
  </p>
  <p>
     <%= f.submit "Criar produto" %>
  </p>
<% end %>

Formulário criado, devemos rodar nosso teste novamente. Fazendo isso veremos que nesse ponto, o que ocorre é que o formulário é preenchido, porém ao ser submitido, nenhuma ação para criar (create) é encontrado no nosso controller:

A ação create não esta definida
1
2
3
Failure/Error: click_button 'Criar produto'
AbstractController::ActionNotFound:
  The action 'create' could not be found for ProductsController

Então para resolver isso devemos criar o nosso método create dentro do ProductContoller:

Definindo a action create
1
2
3
4
5
6
7
8
9
10
11
def create
  @product = Product.new(product_params)

  if @product.save
    redirect_to @product, notice: "Produto foi criado."
  else
    flash[:alert] = "Produto não pode ser criado."

    render "new"
  end
end

Também defina um método privado para permitir que nossos parametros sejam lidos, sem serem bloqueados pelo mecanismo de Strong Parameters do Rails 4:

Adicionando os campos permitidos
1
2
3
4
  private
    def product_params
      params.require(:product).permit(:name, :description)
    end

Como vamos usar flash messages do Rails, é necessário em nosso application.html.erb definir um local para fazer a exibição das mensagens. Crie um diretório denominado shared dentro da pasta views. Nessa nova pasta crie o arquivo _messages.html.erb. Adicione o seguinte conteúdo:

app/views/shared/_messages.html.erb
1
2
3
4
5
6
7
8
  <% flash.each do |name, msg| %>
    <% if msg.is_a?(String) %>
      <div class="alert alert-<%= name.to_s == 'notice' ? 'success' : 'danger' %>">
        <button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
        <%= content_tag :div, msg, :id => "flash_#{name}" %>
      </div>
    <% end %>
  <% end %>

Com o partial criado, adicione a chamada de renderização no arquivo de layout, logo acima da instrução yield:

app/views/layouts/application.html.erb
1
2
3
4
  <div class="container">
    <%= render 'shared/messages' %>
    <%= yield %>
  </div>

Rode seu teste e ele ira dizer que não encontrou a action show. Isso quer dizer que o nosso cadastro está sendo salvo, porém não temos uma ação para fazer a exibição de cadastro. Adicione a ação show ao seu ProductController:

Nossa ação para busca e exibir o usuário
1
2
3
def show
  @product = Product.find(params[:id])
end

Se rodarmos os testes vamos ter um erro por que o template correspondente a action, não pode ser encontrada, crie um o seguinte arquivo app/views/products/show.html.erb, e adicione o código abaixo:

app/views/products/show.html.erb
1
2
<h2><%= @product.name %></h2>
<p><%= @product.description %></p>

Rodandos os testes, nossa feature vai passar. Se quiser testar manualmente, inicie o servidor com o comando rails s e realize um cadastro.

Adicionando validações aos nossos campos

Como o nosso cadastro criado e funcionando, devemos adicionar algumas validações para os nossos campos. Ao campo nome vamos verificar se ele foi informado, se atende o tamanho minímo de 3 caracteres e o máximo de 50 e também, o nome deve ser único. Já para o campo descrição verificáremos se foi informado e se possue pelo menos 15 carácteres de texto. Nessa etapa, vamos usar testes unitários e testes de integração. Os testes unitários vão garantir que as validações estão sendo exigidas e o teste integrado deverá validar a operação de cadastro completo.

Testando unitáriamente nosso model

Vamos iniciar testando no nosso model Product se algum valor a propriedade name foi informado, assim impediremos que o nosso campo tenha um valor em branco. Vamos usar a validação de tamanho para impedir o não preenchimento do campo. Adicione o seguinte código de ao arquivo:

spec/models/product_spec.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  require 'rails_helper'

  describe Product do
    before do
      @product = Product.new(name: "Cadastro Exemplo",
        description: "Descrição do cadastro Exemplo")
    end

    describe "quando o nome não foi informado" do
      before { @product.name = ""}
      it { should_not be_valid }
    end

    describe "quando o nome é muito curto" do
      before { @product.name = "na"}
      it { should_not be_valid }
    end

    describe "quando o nome é muito longo" do
      before { @product.name = "n" * 50}
      it { should_not be_valid }
    end
  end

Rode o comando rspec spec/models/product_spec.rb e o nosso teste deverá falhar, pois estamos esperando que algo errado aconteça com o uso dos métodos should_not be_valid, mas como não definimos as validações no nosso model, nada de errado ocorre e o nosso teste falha. Adicione o código abaixo e rode o comando em seguida, nosso testes deve passar:

app/models/product.rb
1
2
3
  class Product < ActiveRecord::Base
    validates_length_of :name, minimum: 5, maximum: 50, allow_blank: false
  end

A validação do nome está quase completa, para finalizar adicione ao teste á baixo do último bloco describe:

spec/models/product_spec.rb
1
2
3
4
5
6
7
8
9
  describe "quando o nome de produto já está sendo usado" do
    before do
      product_with_same_name = @product.dup
      product_with_same_name.name = @product.name
      product_with_same_name.save
    end

    it { should_not be_valid }
  end

Usamos o método dup quando queremos duplicar um módelo. Aqui queremos que o nome que já exista, não seja duplicado, e é isso que o teste nos assegura. Ao model adicione:

app/models/product.rb
1
  validates_uniqueness_of :name

E rode o teste novamente. Tudo verde, o campo nome está validado, agora vamos validar o campo description. Ao teste insira abaixo do ultimo bloco de describe o seguinte código:

spec/models/product_spec.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  describe "quando a descrição não foi informada " do
    before { @product.description = "" }
    it { should_not be_valid }
  end

  describe "quando a descrição é muito curta" do
    before { @product.name = "n" * 15}
    it { should_not be_valid }
  end

  describe "quando a descrição é muito longa" do
    before { @product.name = "n" * 255}
    it { should_not be_valid }
  end

E ao nosso modelo insira as validações:

app/models/product.rb
1
  validates_length_of :description, minimum: 15, maximum: 255, allow_blank: false

Mas que beleza! Nosso modelo está validando nossos dados e temos a garantia disso com os testes unitários, hora de testar integrado e ajustar nossas mensagens.

Testando de maneira integrada

Antes de continuar a implementação, vamos refatorar a nossa spec feature spec/features/products/creating_products_spec.rb. Vamos adicionar um bloco before, e nele vamos colocar o que estará repetindo nos outros cenários, nossa feature deve ser semelhante a isso:

spec/features/products/creating_products_spec.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  require 'rails_helper'

  feature 'Criando Produtos' do
    before do
      visit '/'
      click_link 'Novo Produto'
    end

    scenario "posso criar um produto" do
      fill_in 'Nome', with: 'Produto 1'
      fill_in 'Descrição', with: 'Produto 1 (um)'
      click_button 'Criar produto'

      expect(page).to have_content('Produto foi criado.')
    end
  end

Rode a spec com o comando rspec spec/features/products/creating_products_spec.rb, seu primeiro cenário deverá falhar. Agora estamos validando nossos campos, e a nossa descrição não está no tamanho correto, corrija, colocando uma descrição com pelo menos 15 caracteres, rode o teste novamente e tudo estará correto.

Vamos agora focar nos cenários de teste negativo. Primeiro para valores do campo nome. Crie um novo cenário abaixo do último criado, como o seguinte código:

insira em spec/features/products/creating_products_spec.rb
1
2
3
4
5
6
7
  scenario "com nome inválido não posso criar um produto" do
    fill_in 'Nome', with: ''
    fill_in 'Descrição', with: 'Produto com uma bela descrição de teste'
    click_button 'Criar produto'

    expect(page).to have_content('Nome é muito curto (mínimo: 5 caracteres)')
  end

Se rodar o teste, irá falhar, pois essa mensagem não está sendo exibida. Vamos criar um arquivo, na pasta shared de nossas views, com o nome de errors.html.erb (app/views/shared/errors.html.erb). Será um partial, que irá exibir todos os erros de validação.

insira em app/views/shared/_errors.html.erb
1
2
3
4
5
6
7
8
9
10
11
12
  <% if object.errors.any? %>
  <div id="error_explanation">
    <div class="alert alert-error">
      O formulário contém <%= pluralize(object.errors.count, "erro") %>.
    </div>
    <ul>
      <% object.errors.full_messages.each do |msg| %>
      <li>* <%= msg %></li>
      <% end %>
    </ul>
  </div>
  <% end %>

Também deve-se incluir esse arquivo no formulário de cadastro:

altere em app/views/products/new.html.erb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  <h2>New Product</h2>

  <%= form_for(@product) do |f| %>
    <%= render 'shared/errors', object: f.object %>
    <p>
      <%= f.label :name, "Nome" %><br />
      <%= f.text_field :name %>
    </p>
    <p>
      <%= f.label :description, "Descrição" %><br />
      <%= f.text_field :description %>
    </p>
    <p>
      <%= f.submit "Criar produto" %>
    </p>
  <% end %>

Podemos rodar nosso teste novamente, e teremos ainda algum erro, mas estamos quase. Precisamos ajustar para que nossos atributos (name e description) sejam reconhecidos como nome e descrição. Basta mudar no nosso arquivo de tradução em config/locales/pt-BR/pt-BR.yml

adicione em config/locales/pt-BR/pt-BR.yml
1
2
3
4
5
6
7
  activerecord:
    models:
      product: Produto
    attributes:
      product:
        name: Nome
        description: Descrição

E mude a configuração padrão, para reconhecer a tradução bem como torna-la default para o nosso sistema:

ajuste a tradução em config/application.rb
1
2
3
4
5
6
7
8
9
  module CrudRspec
    class Application < Rails::Application
      config.time_zone = 'Brasilia'
      config.i18n.default_locale = :'pt-BR'
      config.i18n.load_path += Dir["#{Rails.root}/config/locales/**/*.{rb,yml}"]
      config.encoding = 'utf-8'
      I18n.enforce_available_locales = false
    end
  end

E também baixe a tradução das mensagens. Salve no diretorio config/locales/pt-BR com o nome de rails.pt-BR.yml. Baixe desse repositório:

https://raw.githubusercontent.com/svenfuchs/rails-i18n/master/rails/locale/pt-BR.yml

Dessa maneira podemos traduzir tudo que o active record gera automáticamente, é uma maneira muito flexível de se traduzir o nosso sistema. Rode novamente os testes e dessa vez, nossa spec passou. Hora de refatorar. Agora que colocamos a tradução dos campos do active record, não precisamos mais a tradução que está fixa nos campos do formulário de cadastro. Remova a tradução:

remova a tradução em app/views/products/new.html.erb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  <h2>New Product</h2>

  <%= form_for(@product) do |f| %>
    <%= render 'shared/errors', object: f.object %>
    <p>
      <%= f.label :name %><br />
      <%= f.text_field :name %>
    </p>
    <p>
      <%= f.label :description %><br />
      <%= f.text_field :description %>
    </p>
    <p>
      <%= f.submit "Criar produto" %>
    </p>
  <% end %>

Complete nossa spec com teste para descrição:

insira em spec/features/products/creating_products_spec.rb
1
2
3
4
5
6
7
  scenario "com descrição inválida não posso criar um produto" do
    fill_in 'Nome', with: 'Meu produto'
    fill_in 'Descrição', with: ''
    click_button 'Criar produto'

    expect(page).to have_content('Descrição é muito curto (mínimo: 15 caracteres)')
  end

Poderíamos colocar mais testes, mas não é esse o intuíto do post (Fica de incentivo para você).

O código fonte pode ser baixado aqui: https://github.com/marceloboth/crud-rspec. Baixando o código faça checkout para o branch criando_cadastro: git chechout criando_cadastro.

Iniciando Em Ruby on Rails Com Testes

Esse post é o primeiro de uma série que irão servir como um próximo passo a quem estiver iniciando com Ruby on Rails, algo além de “construa um blog em quinze minutos” e sim algo mais útil ao mundo real.

Nesse post veremos sobre como gerar nossa aplicação, configurar nossos testes, instalar uma biblioteca de front-end e deixar tudo pronto para iniciar o desenvolvimento.

Criando nossa aplicação

Criar a aplicação é um passo simples, onde usaremos os geradores do Rails para fazer o trabalho árduo de montar a estrutura e deixar tudo configurado, baseado no conceito de “Convenção sobre configuração”. No terminal, navege até o diretório que deseja criar o projeto e digite o seguinte comando:

Criar o projeto Rails
1
  rails new crud-rspec -T

O comando rails new com o argumento -T, é para sinalizar ao gerador, que não queremos o conjunto de testes padrão do Rails, já que vamos usar o Rspec. Para ver mais opções de configuração utilize o help do comando: rails new -h

Aguarde a finalização da construção…

Saida da linha de comando
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  rails new crud-rspec -T
      create
      create  README.rdoc
      create  Rakefile
      create  config.ru
      create  .gitignore
      create  Gemfile
      create  app
      create  app/assets/javascripts/application.js
      create  app/assets/stylesheets/application.css

      ..................
      ..................
  Your bundle is complete!
  Use `bundle show [gemname]` to see where a bundled gem is installed.
           run  bundle exec spring binstub --all
  * bin/rake: spring inserted
  * bin/rails: spring inserted

No seu bash ou outra ferramenta de linha de comando (recomendo zsh), navegue para o diretório que foi criado usando cd crud-rspec. Abra o diretório com o seu editor favorito.

O framework Rails nos impõe algumas convenções e uma estrutura inicial para seguir. Primeiramente temos o uso de uma arquitetura MVC onde temos nos models, que manipulam nossos dados, controllers que gerenciam o que deve ser executado e para onde entregar as informações e nossas views que fazem a interação, recebem as entradas e mostram as saidas ao usuário. Isso tudo está na pasta app do projeto. Esse é o local onde passaremos a maior parte do nosso desenvolvimento. Abaixo uma descrição básica de todos os diretórios criados.

app: Local do código principal da aplicação. Models, Controllers, Views, Helpers, CSS e código Javascript da nossa aplicação ficam nessa pastas.

bin: Armazena os scripts dos geradores do rails.

config: Configuração do banco de dados, intenacionalizações, especificidades de algumas gems (Devise,SimpleForm, etc), rotas da aplicação e muitas outras definições.

db: Mantém o esquema e as migrações da base de dados.

lib: Aqui ficam todos os códigos que não são diretamente ligados a aplicação. São armazenadas as tarefas rake, bem como tasks que são executadas fora do ambiente web.

log: Arquivos de log, simples assim :).

public: Páginas de erro (404, 500) e arquivos estáticos como por exemplo o favicon.ico e o robots.txt.

tmp: Guarda o cache e PID da aplicação.

vendor: Local onde são colocadas bibliotecas que não sejam gems.

A construtor de projetos do Rails poupa a nós um bom tempo, pois não precisamos nos preocupar com a arquitetura inicial do projeto, pois ele que cria as pastas e arquivos, de acordo com a nossas escolhas.

Configurar nossos testes

Vamos iniciar a configuração dos nossos testes. Primeiro passo é instalar a gem de testes Rspec e a gem para testes de aceitação Capybara, pra isso devemos adicionar elas ao nosso arquivo Gemfile, e devemos definir a qual grupo de gems elas pertencem (desenvolvimento, testes ou produção). Como nossos testes apenas são usados em teste e produção, ficará de seguinte maneira:

Adicione as gems ao Gemfile
1
2
3
4
  group :development, :test do
    gem 'rspec-rails'
    gem 'capybara'
  end

em seguida execute o camando de instalação de gems no terminal:

Execute o comando
1
  bundle install

Após instalada as gems, vamos gerar os arquivos de configuração do rspec e também os diretórios que servem de base para criar nossos testes:

Gerar configs
1
  rails generate rspec:install

Se observarmos a saída do nosso console

Saída do console
1
2
3
4
5
  rails generate rspec:install
      create  .rspec
      create  spec
      create  spec/spec_helper.rb
      create  spec/rails_helper.rb

veremos que o diretório rspec foi gerado, e os arquivos de configuração também.

Are you ready?

Criamos nosso projeto e também configuramos o rspec, poderiamos implementar nossa primeira funcionalidade, mas antes, vamos configurar uma biblioteca de front-end, no caso twitter bootstrap. Para isso adicione a gem do twitter bootstrap em nosso Gemfile:

Adicione ao Gemfile
1
2
  gem 'bootstrap-sass'
  gem 'autoprefixer-rails'

Rode novamente o bundle install para instalar a gem e na seqüência configure, da seguinte maneira:

  • Renomeie o arquivo app/assets/stylesheets/application.css para app/assets/stylesheets/application.css.scss

  • Adicione ao arquivo application.css.scss:

1
2
  @import "bootstrap-sprockets";
  @import "bootstrap";
  • Adicione ao app/assets/javascript/application.js //= require bootstrap-sprockets

  • Crie um controller com uma view para servir como o Index de nossa Web Application:

Crie o controlle e a view
1
  rails g controller home index

Isso criará uma série de arquivos, que são: o nosso controller, nossa view, arquivos de css e javascript, testes e também é feita uma alteração em nosso arquivo de rotas:

Arquivos gerados
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  rails g controller home index
      create  app/controllers/home_controller.rb
       route  get 'home/index'
      invoke  erb
      create    app/views/home
      create    app/views/home/index.html.erb
      invoke  rspec
      create    spec/controllers/home_controller_spec.rb
      create    spec/views/home
      create    spec/views/home/index.html.erb_spec.rb
      invoke  helper
      create    app/helpers/home_helper.rb
      invoke    rspec
      create      spec/helpers/home_helper_spec.rb
      invoke  assets
      invoke    coffee
      create      app/assets/javascripts/home.js.coffee
      invoke    scss
      create      app/assets/stylesheets/home.css.scss

Nosso primeiro teste

Agora que temos nossa estrutura gerada, nossos testes configurados e nosso front-end pronto, vamos definir a nossa página principal. Nosso primeiro passo é definir que a nossa rota raiz da aplicação seja o index do controlador home. Com isso feito vamos colocar um menu superior na página e um link para o cadastro de produtos que será implementado no próximo post. Crie um diretório denominado features dentro do diretório spec, e dentro da pasta criada crie um arquivo chamado reading_products_spec.rb. Nesse arquivo implemente o teste da seguinte maneira:

Escrevendo nosso teste spec/features/reading_products_spec.rb
1
2
3
4
5
6
7
  require "rails_helper"

  feature "Listando Produtos" do
    it "consigo acessar o link da página" do
      visit root_path
    end
  end

Antes de rodar o teste, desabilite o lançamento de warnings do rspec. Faça isso removendo a linha que contém o conteúdo --warnings do aquivo .rspec que está na raiz do projeto. Agora rode o teste com o comando rspec spec/features/reading_products_spec.rb. Um erro ocorrerá:

Erro ocorrido
1
2
3
4
  1) Listando Produtos consigo acessar o link da página
     Failure/Error: visit root_path
     NameError:
       undefined local variable or method `root_path' for #<RSpec::ExampleGroups::ListandoProdutos:0xa824e84>

Esse erro acontece pois estamos tentando visitar a raiz de nosso site e ela ainda não está configurada nas rotas. Abra o arquivo config/routes.rb, e deixe a sua implementação na seguinte forma:

Defina o root_path config/routes.rb
1
2
3
  Rails.application.routes.draw do
    root 'home#index'
  end

Salve e rode o teste novamente. Nosso teste passou, vamos a mais uma parte da implementação, que é onde vamos colocar o cabeçalho e o link para o recurso de listagem de cadastros. Ao teste adicione:

Encontrando o link de listagem spec/features/reading_products_spec.rb
1
2
3
4
5
6
  feature "Listando Produtos" do
    it "consigo acessar o link da página" do
      visit root_path
      expect(page).to have_link('Produtos')
    end
  end

Rodando nosso teste, teremos uma quebra, pois esse link não existe em nossa página inicial. Vamos colocar um menu, com o conjunto de estilos do Twitter Bootstrap a nossa página de layout app/views/layouts/application.html.erb:

Implemente a página de layout app/views/layouts/application.html.erb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  <body>

    <div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
      <div class="container">
        <div class="navbar-header">
          <a class="navbar-brand" href="#">Crud com RSPEC - Uhuu!</a>
        </div>
        <div class="collapse navbar-collapse">
          <ul class="nav navbar-nav">
            <li class="active"><a href="#">Início</a></li>
            <li><a href="#about">Produtos</a></li>
          </ul>
        </div>
      </div>
    </div>

    <div class="container">
      <%= yield %>
    </div>

  </body>

Rode os testes novamente, agora tudo vai passar. Temos nossa página inicial definida, com o uso de testes de integração. O que fizemos foi, adicionar uma rota raiz para nossa aplicação, colocar um menu e um link para o cadastro de produtos (que está sem caminho), e tudo isso guiado por testes, usando Rspec e Capybara.

Finalizamos nossa primeira parte, onde configuramos nossa aplicação com algumas ferramentas que o Rails e o Ruby nos proporcionam para desenvolvermos com Agilidade sem perder qualidade. Nos próximos posts vamos implementar um Crud usando Rspec, Capybara e Rails.

O código pode ser baixado no Github, somente observe que cada post está dividido por branch.