Minicurso de Ruby on Rails que ocorreu de 25/11/2014 até 28/11/2014 no Instituto Federal de Educação Ciência e Tecnologia do Sudeste de Minas Campus Barbacena
8. v = 1 # escopo local
@v = 2 # escopo da instância
@@v = 3 # escopo da classe
$v = 4 # escopo global
print v, @v, @@v, $v # 1234
ESCOPO DE VARIÁVEIS
9. NO = 2 # tudo que se inicia com letra maiúscula é uma constante
NO = 3 # warning: already initialized constant NO
# warning: previous definition of NO was here
CONSTANTES
10. s = 'abc'
s.reverse # => 'cba'
s # => 'abc'
s.reverse! # => 'cba'
s # => 'cba'
s.include? 'a' # => true
MÉTODOS
11. def splat(*args)
p args
end
def keywords(x:, y: 1)
print x, y
end
splat 1, 2, 3 # [1, 2, 3]
keywords x: 2 # 21
MÉTODOS E PARÂMETROS
14. def m(x)
if x > 0
puts x
elsif x == 0
puts "zero"
else
puts "-"
end
end
m 1 # +
m 0 # zero
m -1 # -
def u(x)
unless x.nil?
puts x
end
end
u 1 # 1
u nil
ESTRUTURAS DE CONTROLE
x = 0
until x == 5
print x
x += 1
end # 01234
x = 0
while x < 5
print x
x += 1
end # 01234
for x in 0..4
print x
end # 01234
15. a = []
a.push(a.length) while a.length < 3
a.push(a.length) until a.length == 5
p a if a.length > 4 # [0, 1, 2, 3, 4]
p a unless a.length < 5 # [0, 1, 2, 3, 4]
MODIFICADORES
16. [1, 2, 3].map { |i| i**2 } # => [1, 4, 9]
[1, 2, 3].each do |i|
puts i
puts i**2
end
BLOCOS
17. def block_yield
yield 'BLOCO'
end
def block_call(&b)
b.call 'BLOCO'
end
block_yield { |s| puts s.downcase } # bloco
block_call { |s| puts s.reverse } # OCOLB
BLOCOS E ARGUMENTOS
18. for x in 0..4
print x
end
(0..4).each { |x| print x} # 01234
FOR MAIS RUBISTA
19. FILOSOFIA DO RUBY
● Felicidade do programador
● Orientação à objeto
● Flexibilidade
23. class Pessoa
attr_accessor :nome, :idade
def to_s
"Nome: #{nome}; Idade: #{idade}"
end
end
eu = Pessoa.new
eu.nome = "Eu"
eu.idade=(101)
puts eu # Nome: Eu; Idade: 101
24. class Pessoa
attr_accessor :nome, :idade
def to_s
"Nome: #{nome}; Idade: #{idade}"
end
end
eu = Pessoa.new
eu.nome = "Eu"
eu.idade=(101)
puts eu # Nome: Eu; Idade: 101
self.nome self.idade
25. module M
def self.m(x)
puts x
end
end
M.m 1 # 1
MODULE
26. module M
class C
def m(x)
puts x
end
end
end
M::C.new.m 1 # 1
MODULE & NAMESPACE
27. module M
def m(x)
puts x
end
end
class C
include M
end
C.new.m 1 # 1
MODULE & MIXIN
28. CLASSES ABERTAS
TODAS as classes podem ser modificadas a
qualquer momento (também conhecido como
monkeypatch)
29. 10.length #NoMethodError: undefined method `length' for 10:Fixnum
class Fixnum
def length
to_s.length
end
end
10.length # => 2
CONTANTO ALGARISMOS DE UM NÚMERO
30. class C
def method_missing(meth, *args)
puts "#{meth} chamado com #{args}"
yield args if block_given?
end
end
o = C.new
z = o.matematica(5, 2) { |x, y| x * y } # matematica chamado com [5, 2]
puts z # 10
MÉTODOS FANTASMAS
31. DSL
● Linguagem dedicada a um domínio de
problema específico
○ HTML, CSS, SQL (DSLs externas)
● Ruby é uma GPL (linguagem de propósito
geral)
○ Permite a criação de DSL’s internas (Capybara,
RSpec)
32. # rspec
describe "true or false" do
it "should be true" do
expect(true || false).to be true
end
end
# capybara
visit '/'
fill_in 'e-mail', with: 'mail@mail.com'
click_on 'Ok'
37. Banco de Dados
Rails routes
2
ActionController
1
8 3
ActionView ActiveRecord
4
5
6
7
1. navegador envia a requisição
2. roteamento encontra o controller
3. controller requisita o model
4. model requisita os dados
5. BD retorna os dados
6. model retorna
7. controller requisita a renderização
8. view retorna a página
9. controller responde a requisição
HTTP com a página
9
ARQUITETURA
39. DIRETÓRIO app
● assets: js, css, imagens, etc.
● controllers: controladores do MVC
○ application_controller.rb: controller geral da aplicação
● helpers: métodos auxiliares para controllers e views
● mailers: classes para enviar e-mails
● models: modelos do MVC
○ concerns: comportamentos compartilhados por modelos
● views: Visões do MVC
○ layouts: layouts para os templates
40. DIRETÓRIO test
Testes da classes da aplicação
● fixtures: dados para os testes
● integration: testes de integração de todos
os componentes da aplicação
41. DIRETÓRIO config
● environments: configurações exclusivas para cada
ambiente de execução
○ Rails provê 3 por padrão: development, test e production
● initializers: rotinas de inicialização
● locales: traduções
● application.rb: configurações comuns para todos os
ambientes
● database.yml: configurações do banco de dados
● routes.rb: roteamento do rails
42. OUTROS DIRETÓRIOS
● bin: wrappers de executáveis
● db: rotinas relacionadas à persistencia
○ migrate: migrações de BDs relacionais
● lib: rotinas não específicas da aplicação
○ assets: assets não específicos da aplicação
○ tasks: tarefas personalizadas do rake
● log: logs da aplicação
● public: arquivos servidos diretamente pelo servidor
web
● tmp: arquivos temporários
● vendor: rotinas criadas por terceiros
○ assets: assets criados por terceiros
52. # …
require 'minitest/mock'
require 'capybara/rails'
# …
class ActionDispatch::IntegrationTest
include Capybara::DSL
end
# …
class ActionController::TestCase
include Devise::TestHelpers
end
test/test_helper.rb
53. $ bundle
$ rails g integration_test notes_listing
54. TESTES DE INTEGRAÇÃO
(ACEITAÇÃO)
● Testes de alto nível (caixa preta)
● Simulam a interação do usuário
● Baseados em cenários
55. class NotesListingTest < ActionDispatch::
IntegrationTest
setup do
visit '/users/sign_in'
fill_in 'Email', with: users(:eu).email
fill_in 'Password', with: '12345678'
click_on 'Log in'
end
teardown do
Capybara.reset_sessions!
end
test/integration/notes_listing_test.rb (continua...)
56. test 'listagem de notas' do
visit '/'
assert page.has_content? notes(:one).title
assert page.has_content? notes(:one).body
assert page.has_no_content? notes(:two).title
assert page.has_no_content? notes(:two).body
end
test/integration/notes_listing_test.rb (continua...)
57. test 'cor das notas' do
visit '/'
assert page.all('.note .well').first['style']
.include? "background-color: #{notes(:one).color}"
end
end
test/integration/notes_listing_test.rb
61. TESTES FUNCIONAIS
● Testa o resultado de uma funcionalidade
● Efeitos colaterais e resultados intermediários
não importam
62. class NotesControllerTest < ActionController::TestCase
test 'index' do
get :index
assert_response :success
assert_not_nil assigns(:notes)
end
end
test/controllers/notes_controller_test.rb
63. class NotesController < ApplicationController
def index
@notes = Note.all
end
end
app/controllers/notes_controller.rb
64. $ rails g model Note title:string body:
text color:string user:references
$ rake db:migrate
67. TESTES UNITÁRIOS
● Testam as menores unidades de
funcionalidade
● Não deve haver interação com outros
componentes
68. class NoteTest < ActiveSupport::TestCase
test "deve ter texto" do
n = notes(:one)
n.body = nil
assert_not n.save
end
test "deve ter uma cor válida" do
n = notes(:one)
n.color = 'Black'
assert_not n.save
end
end
test/models/note_test.rb
84. class NotesControllerTest < ActionController::TestCase
setup do
sign_in users(:eu)
end
# ...
test 'index deve mostrar somente as notas do usuário atual' do
get :index
assert_not assigns(:notes).include? notes(:two)
end
end
test/controllers/notes_controller_test.rb
85. class NotesController <
ApplicationController
before_action :authenticate_user!
def index
@notes = Note.where(user: current_user)
end
end
app/controllers/notes_controller.rb
86. class NotesControllerTest < ActionController::TestCase
# ...
test 'index deve retornar a nota mais recente primeiro' do
note = Note.last
note.touch
note.save
get :index
assert assigns(:notes).first
.updated_at > assigns(:notes).last.updated_at
end
end
test/controllers/notes_controller_test.rb
87. class NotesController < ApplicationController
# ...
def index
@notes = Note.where(user: current_user)
.order(updated_at: :desc)
end
end
app/controllers/notes_controller.rb
89. # ...
module JSHelper
def use_js
Capybara.current_driver = :webkit
@js = true
end
def teardown
super
if @js
Capybara.use_default_driver
@js = false
end
end
end
test/test_helper.rb (continua)
90. module SessionHelper
def log_in
visit '/users/sign_in'
fill_in 'Email', with: users(:eu).email
fill_in 'Password', with: '12345678'
click_on 'Log in'
end
def teardown
super
Capybara.reset_sessions!
end
end
test/test_helper.rb (continua)
91. # ...
class ActionDispatch::
IntegrationTest
# ...
include JSHelper
include SessionHelper
end
test/test_helper.rb (continua)
92. # ...
class ActiveRecord::Base
mattr_accessor :shared_connection
@@shared_connection = nil
def self.connection
@@shared_connection || retrieve_connection
end
end
ActiveRecord::Base.shared_connection =
ActiveRecord::Base.connection
test/test_helper.rb
93. class NotesListingTest < ActionDispatch::IntegrationTest
setup do
log_in
end
# ...
test/integration/notes_listing_test.rb
94. class NoteCreationTest < ActionDispatch::IntegrationTest
test 'criação de nota' do
use_js
log_in
visit '/'
click_on I18n.t('actions.create_note')
fill_in 'note_title', with: 'Nova Nota'
fill_in 'note_body', with: 'Novo Texto'
click_on 'SkyBlue'
click_on I18n.t('actions.save_note')
assert page.has_content?('Nova Nota'), 'Título não
encontrado'
assert page.has_content?('Novo Texto'), 'Texto não
encontrado'
assert page.all('.note .well').first['style']
.include?("background-color: SkyBlue"), "Cor errada"
end
test/integration/note_creation_test.rb (continua)
95. test 'erro na criação de nota' do
log_in
visit '/'
click_on I18n.t('actions.create_note')
click_on I18n.t('actions.save_note')
assert page.has_selector? 'div#errors'
end
end
test/integration/note_creation_test.rb
97. # ...
test 'new' do
get :new
assert_response :success
assert_not_nil assigns(:note)
end
test 'new deve instanciar uma nova nota' do
get :new
assert_not assigns(:note).persisted?
end
test 'nova nota deve ser dourada por padrão' do
get :new
assert 'Gold', assigns(:note).color
end
end
test/controllers/notes_controller_test.rb
98. class NotesController <
ApplicationController
# ...
def new
@note = Note.new(color: 'Gold')
end
end
app/controllers/notes_controller.rb
99. FORM HELPERS
● Helpers para criação de formulários
● Facilitam a criação de formulários para
ações em modelos
100. <%= form_for @note, html: {role: 'form'} do |n| %>
<% if @note.errors.any? %>
<!-- ... -->
</button>
<strong>
<%= t('errors.save_note', count: @note.errors.count) %>
</strong>
<ul>
<% @note.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<%= n.hidden_field :color %>
app/views/notes/new.html.erb (continua)
104. class C; end
obj = C.new
def obj.hey
puts 'hey'
end
# hey está definido na eigenclass de obj
obj.hey # hey
C.new.hey # NoMethodError: undefined method `hey' for #<C:0x...>
105. class Note < ActiveRecord::Base
# ...
class << self
attr_reader :allowed_colors
end
end
app/models/note.rb
106. COFFEESCRIPT
● Linguagem que compila para JS
● Simplifica o JS
● Inspirado em Ruby, Python e Haskell
107. @change_color = (color) ->
$('#note-card').css 'background-color', color
$('input[name="note[color]"]').val color
app/assets/javascripts/notes.js.coffee
108. pt-BR:
actions:
create_note: Criar nota
log_out: Sair
save_note: Salvar
errors:
save_note:
one: 1 erro
other: "%{count} erros"
placeholders:
body: Texto
title: Título
config/locales/pt-BR.yml
109. # ...
test 'create' do
post :create,
note: {title: 'Note', body: 'Text', color: 'Gold'}
assert_redirected_to controller: 'notes', action: 'index'
end
test 'create deve salvar a nota' do
assert_difference('Note.where(user: users(:eu)).count') do
post :create,
note: {title: 'Note', body: 'Text', color: 'Gold'}
end
end
test/controllers/notes_controllers_test.rb (continua)
110. test 'create deve renderizar o formulario de criação novamente
em caso de erro' do
post :create, note: {title: 'Note', color: 'Gold'}
assert_template :new
end
test 'create deve retornar um erro se a nota não for salva' do
post :create, note: {title: 'Note', color: 'Gold'}
assert_response :unprocessable_entity
end
end
test/controllers/notes_controllers_test.rb
111. STRONG PARAMETERS
● Permite controlar especificamente quais
atributos podem ser definidos em massa
112. class NotesController < ApplicationController
# ...
def create
@note = Note.new(note_params.merge(user:
current_user))
if @note.save
redirect_to notes_path
else
render :new, status: :unprocessable_entity
end
end
private
def note_params
params.require(:note).permit(:title, :body, :color)
end
end
app/controllers/notes_controller.rb
114. class NoteEditingTest < ActionDispatch::IntegrationTest
test 'editação de nota' do
use_js
log_in
visit '/'
click_on "actions-#{notes(:one).id}"
click_on "edit-#{notes(:one).id}"
fill_in 'note_title', with: 'Nova Nota'
fill_in 'note_body', with: 'Novo Texto'
click_on 'SkyBlue'
click_on I18n.t('actions.save_note')
assert page.has_content?('Nova Nota'), 'Título não
encontrado'
assert page.has_content?('Novo Texto'), 'Texto não
encontrado'
assert page.all('.note .well').first['style']
.include?("background-color: SkyBlue"), "Cor errada"
end
test/integration/note_editing_test.rb (continua)
115. test 'erro na edição de nota' do
use_js
log_in
visit '/'
click_on "actions-#{notes(:one).id}"
click_on "edit-#{notes(:one).id}"
fill_in 'note_body', with: ''
click_on I18n.t('actions.save_note')
assert page.has_selector? 'div#errors'
end
end
test/integration/note_editing_test.rb (continua)
117. class NotesControllerTest < ActionController::TestCase
# ...
test 'edit' do
get :edit, id: notes(:one).id
assert_response :success
assert_not_nil assigns(:note)
end
test 'edit deve retornar a nota correta para ser editada' do
get :edit, id: notes(:one).id
assert_equal notes(:one), assigns(:note)
end
test 'edit deve disparar uma exceção se a nota não for
encontrada' do
assert_raises(ActiveRecord::RecordNotFound) { get :edit, id: 1
}
end
end
test/controllers/notes_controller_test.rb
118. class NotesController < ApplicationController
# ...
def edit
@note = Note.find(params[:id])
end
# ...
app/controllers/notes_controller.rb
119. PARTIALS
● DRY
● Extração de partes comuns entre templates
122. class NotesControllerTest < ActionController::TestCase
# ...
test 'update' do
patch :update, id: notes(:one).id,
note: {title: 'Update Note',
body: 'Update Text', color: 'Pink'}
assert_redirected_to controller: 'notes', action: 'index'
end
test 'update deve atualizar a nota' do
patch :update, id: notes(:one).id,
note: {title: 'Update Note',
body: 'Update Text', color: 'Pink'}
notes(:one).reload
assert_equal 'Update Note', notes(:one).title
assert_equal 'Update Text', notes(:one).body
assert_equal 'Pink', notes(:one).color
end
test/controllers/notes_controller_test.rb (continua)
123. test 'update deve renderizar o formulario de edição novamente em
caso de erro' do
patch :update, id: notes(:one).id,
note: {title: 'Update Note', body: '', color: 'Pink'}
assert_template :edit
end
test 'update deve retornar um erro se a nota não for salva' do
patch :update, id: notes(:one).id,
note: {title: 'Update Note', body: '', color: 'Pink'}
assert_response :unprocessable_entity
end
test 'update deve disparar uma exceção se a nota não for
encontrada' do
assert_raises(ActiveRecord::RecordNotFound) { patch :update,
id: 1 }
end
end
test/controllers/notes_controller_test.rb
124. class NotesController < ApplicationController
# ...
def update
@note = Note.find(params[:id])
if @note.update(note_params)
redirect_to notes_path
else
render :edit, status: :
unprocessable_entity
end
end
# ...
app/controllers/notes_controller.rb
126. class NoteDeletingTest < ActionDispatch::IntegrationTest
test 'exclusão de nota' do
use_js
log_in
visit '/'
click_on "actions-#{notes(:one).id}"
page.accept_confirm do
click_on "delete-#{notes(:one).id}"
end
assert page.has_no_content? notes(:one).title
end
end
test/integration/note_deleting_test.rb
128. pt-BR:
actions:
create_note: Criar nota
delete_note: Excluir
# …
errors:
delete_note: Não foi possível excluir a nota
# …
messages:
are_you_sure?: Tem certeza?
# ...
config/locales/pt-BR.yml
129. class NotesControllerTest < ActionController::TestCase
# ...
test 'destroy' do
delete :destroy, id: notes(:one).id
assert_redirected_to controller: 'notes', action: 'index'
end
test 'destroy deve excluir a nota' do
assert_difference('Note.count', -1) { delete :destroy, id:
notes(:one).id }
end
test 'destroy deve disparar uma exceção se a nota não for
encontrada' do
assert_raises(ActiveRecord::RecordNotFound) { delete :
destroy, id: 1 }
end
test/controllers/notes_controller_test.rb (continua)
130. MOCKS & STUBS
● Mocks: simulam o comportamento de
objetos reais
● Stubs: simulam a execução de métodos
reais
131. test 'destroy deve colocar as mensagens de erro no flash alert'
do
note = MiniTest::Mock.new
errors = MiniTest::Mock.new
note.expect :id, notes(:one).id
note.expect :destroy, false
note.expect :errors, errors
errors.expect :full_messages, ['error']
Note.stub :find, note do
delete :destroy, id: notes(:one).id
assert_equal I18n.t('errors.delete_note'), flash[:alert]
assert_response :unprocessable_entity
end
end
end
test/controllers/notes_controller_test.rb
132. class NotesController < ApplicationController
# ...
def destroy
@note = Note.find(params[:id])
if @note.destroy
redirect_to notes_path
else
flash[:alert] = I18n.t('errors.delete_note')
redirect_to notes_path, status: :unprocessable_entity
end
end
# ...
app/controllers/notes_controller.rb
134. class NotesSearchingTest < ActionDispatch::IntegrationTest
setup do
log_in
end
test 'buscando por título exato' do
visit '/'
fill_in 'query', with: notes(:one).title
click_on 'search'
assert page.has_content? notes(:one).body
assert page.has_no_content? notes(:three).title
assert page.has_content? I18n.t('messages.n_notes_found',
count: 1)
end
test/integration/notes_searching_test.rb (continua)
135. test 'buscando por texto exato' do
visit '/'
fill_in 'query', with: notes(:one).body
click_on 'search'
assert page.has_content? notes(:one).title
assert page.has_no_content? notes(:three).title
assert page.has_content? I18n.t('messages.n_notes_found',
count: 1)
end
test 'buscando por título parcial' do
visit '/'
fill_in 'query', with: notes(:one).title[0..2]
click_on 'search'
assert page.has_content? notes(:one).title
assert page.has_content? notes(:three).title
assert page.has_content? I18n.t('messages.n_notes_found',
count: 2)
end
test/integration/notes_searching_test.rb (continua)
136. test 'buscando por texto parcial' do
visit '/'
fill_in 'query', with: notes(:one).body[0..2]
click_on 'search'
assert page.has_content? notes(:one).title
assert page.has_content? notes(:three).title
assert page.has_content? I18n.t('messages.n_notes_found',
count: 2)
end
test 'resultados no título e no texto de notas diferentes' do
visit '/'
fill_in 'query', with: 'Busca'
click_on 'search'
assert page.has_content? notes(:one).title
assert page.has_content? notes(:three).title
assert page.has_content? I18n.t('messages.n_notes_found',
count: 2)
end
test/integration/notes_searching_test.rb (continua)
140. class NotesControllerTest < ActionController::TestCase
# ...
test 'search' do
get :search, query: notes(:one).title
assert_response :success
assert_not_nil assigns(:notes)
end
test 'search deve buscar somente as notas do usuario atual' do
get :search, query: 'N'
assert_equal 2, assigns(:notes).count
end
test 'search deve buscar pelo titulo completo' do
get :search, query: notes(:one).title
assert_equal notes(:one), assigns(:notes).first
end
test/controllers/notes_controllers_test.rb (continua)
141. test 'search deve buscar pelo texto completo' do
get :search, query: notes(:one).body
assert_equal notes(:one), assigns(:notes).first
end
test 'search deve buscar pelo titulo parcial' do
get :search, query: notes(:one).title[0..2]
assert assigns(:notes).include? notes(:one)
assert assigns(:notes).include? notes(:three)
end
test 'search deve buscar pelo texto parcial' do
get :search, query: notes(:one).body[0..2]
assert assigns(:notes).include? notes(:one)
assert assigns(:notes).include? notes(:three)
end
test/controllers/notes_controllers_test.rb (continua)
142. test 'search deve retornar resultados de título e texto em notas
diferentes' do
get :search, query: 'Busca'
assert assigns(:notes).include? notes(:one)
assert assigns(:notes).include? notes(:three)
end
test 'search deve colocar a quantidade de resultados no flash
notice' do
get :search, query: notes(:one).title
assert_equal I18n.t('messages.n_notes_found', count: 1),
flash[:notice]
end
test 'search deve colocar a mensagem de nenhum resultados no
flash alert' do
get :search, query: 'abcd'
assert_equal I18n.t('messages.no_notes_found'), flash[:alert]
end
test/controllers/notes_controllers_test.rb
143. class NotesController < ApplicationController
# ...
def search
@notes = Note.where(user: current_user)
.where(' notes.title LIKE ? OR notes.body LIKE ?',
"%#{params[:query]}%", "%#{params[:query]}%")
.order(updated_at: :desc)
if @notes.count > 0
flash.now[:notice] = I18n.t('messages.n_notes_found',
count: @notes.count)
else
flash.now[:alert] = I18n.t('messages.no_notes_found')
end
end
# ...
app/controllers/notes_controller.rb
146. pt-BR:
# ...
messages:
are_you_sure?: Tem certeza?
n_notes_found:
one: 1 nota encontrada
other: "%{count} notas encontradas"
no_notes_found: Nenhuma nota encontrada :-(
placeholders:
body: Texto
search_notes: Pesquisar nas notas
# ...
config/locales/pt-BR.yml
147. REFERÊNCIAS
PERROTTA, Paolo. Metaprogramming Ruby. 1 ed. The
Pragmatic Bookshelf, 2010.
MATSUMOTO, Yukihiro. Ruby in a Nutshell. 1 ed. O’Reilly,
2001.
THOMAS, Dave; FOWLER, Chad; HUNT, Andy.
Programming Ruby 1.9 & 2.0: The Pragmatic Programmers
Guide. 4 ed. The Pragmatic Bookshelf, 2013.
BLACK, David A.. The Well-Grounded Rubyist. 1 ed.
Manning, 2009.
148. REFERÊNCIAS
About Ruby - https://www.ruby-lang.org/en/about/
The Philosophy of Ruby - http://www.artima.
com/intv/rubyP.html
RubyConf: History of Ruby - http://blog.nicksieger.
com/articles/2006/10/20/rubyconf-history-of-ruby/
Ruby on Rails Guides - http://guides.rubyonrails.org/