Melhores práticas: Testando controladores Rails com RSpec

Postado originalmente em codecrate.com

Rails é principalmente um framework de desenvolvimento web e por isso é natural que os controladores sejam um aspecto integral de sua aplicação. Os controladores no Rails normalmente aceitam solicitações HTTP como sua entrada e entregam de volta uma resposta HTTP como saída. Esta é uma parte muito importante da construção de aplicativos da web e, portanto, o teste de unidade de seus controladores deve ser uma das partes mais fundamentais do seu conjunto de testes. Eu sou um grande fã do RSpec para testar aplicativos Rails, e descobri que, embora a gem rspec-rails seja um ótimo ponto de partida, há uma falta geral de melhores práticas para testar unidades de controle de forma adequada.

Asserções essenciais

Vamos começar com o básico. O que você deve testar? A saída dos controladores Rails é a resposta HTTP e, portanto, é essencial que cada ação do controlador tenha testes que afirmam as propriedades principais da resposta HTTP. Por exemplo:

  • Qual foi o código de status da resposta?
  • Qual foi o tipo de conteúdo da resposta?
  • O controlador renderizou o modelo esperado?
  • O controlador renderizou o layout Rails esperado?
  • O controlador definiu alguma mensagem flash?
  • Alguma informação foi inserida ou excluída da sessão?
  • O controlador redirecionou o usuário para um novo URL?

Felizmente, a maioria dessas afirmações são de uma linha, graças à excelente joia dos deveres correspondentes .

Contextos estruturados

Usar contextos RSpec de maneira eficaz é um aspecto crítico para escrever ótimas especificações de controlador. Betterspecs.org faz um bom trabalho fornecendo o básico, e aqui estão algumas dicas adicionais que se aplicam especificamente às especificações do controlador que ajudarão a tornar seus testes muito mais expressivos.

NOTA: Os blocos de configuração e afirmação (antes / dele) foram deixados em branco como um exercício para o leitor. As implementações devem ser muito diretas e foram omitidas para enfatizar a estrutura / linguagem usada para estruturar os blocos de contexto.

Permutações de entrada

Crie um novo contexto para cada conjunto de entradas significativas . Normalmente, isso é usado para parâmetros de consulta variáveis, mas também funciona bem quando você precisa exercitar caminhos de código para sessão HTTP ou uso de cabeçalho.

describe 'GET #index' do
context
'when params[:filter_by] == first_name' do
it
'filters results by first_name'
end
context
'when params[:filter_by] == last_name' do
it
'filters results by last_name'
end
end

Parâmetros válidos vs inválidos

Mesma premissa da dica anterior, mas vale ressaltar por ser um caso de uso muito comum.

describe 'POST #create' do
context
'with valid params' do
it
'redirects to show page'
end
context
'with invalid params' do
it
're-renders #new form'
end
end

Acesso autenticado

Use contextos separados para acesso autenticado e não autenticado.

describe 'GET #index ' do
context
'when user is logged in' do
it
'renders the listing page'
end
context
'when user is logged out' do
it
'redirects to login page'
end
end

Acesso autorizado

Crie um novo contexto para cada função de usuário que acessa o endpoint (ex: admin vs usuário padrão).

describe 'GET #show' do
context
'as content owner' do
it
'renders the permalink template'
end
context
'as an admin' do
it
'renders the permalink template'
end
context
'as a guest user' do
it
'displays access forbidden message'
end
end

Evite contextos aninhados

Não aninhe contextos para compartilhar uma configuração comum. Apenas não faça isso. Nunca .

Isso merece uma postagem inteira no blog e será um tópico de discussão futura …

Os controladores devem renderizar os modelos

Agora, aqui está uma pegadinha com a integração RSpec-Rails. Por alguma razão, a configuração RSpec-Rails padrão desabilita a renderização de modelos para especificações de controlador. Na minha opinião, esta é uma recomendação muito pobre que leva a uma falsa sensação de segurança. Já vi isso acontecer mais de uma vez em que seus testes de controlador passarão, sua cobertura de código será 100%, mas, como um problema de indentação HAML, explodirá na produção. BOOM .

Os controladores Rails operam em requisições HTTP e o corpo da resposta é uma parte crítica de seu trabalho . Para corrigir isso, certifique-se de habilitar a render_viewsconfiguração em seu arquivo rails_helper.rb.
https://github.com/rspec/rspec-rails#controller-specs

RSpec.configure do |config|
config
.render_views
end

Eu não incentivar o uso de vista specs quando um ponto de vista tem codepaths condicionais, e eu também recomendo o uso de integração / solicitação de especificações quando é necessário middleware teste Rack, mas é desnecessário para escrever essas especificações adicionais para cada ação controlador único e vista. Por que forçar os desenvolvedores a escrever testes de integração separados que duplicam mais de 90% das especificações do controlador padrão quando posso simplesmente renderizar as visualizações dentro das especificações do controlador e terminar com isso? Sou totalmente a favor da separação de interesses, mas essa é uma simples questão de pragmatismo versus dogma.

E, se houvesse uma maneira de detectar a cobertura de código das visualizações , eu realmente adoraria ter os controladores não realizando essa chamada de renderização e poderia confiar inteiramente nas especificações das visualizações. Dessa forma, qualquer caminho de código não renderizado em visualizações poderia ser capturado por processos de integração contínua. Até que isso seja possível, acho muito mais seguro habilitar a renderização por padrão e permitir que controladores específicos desativem a renderização, se necessário.

Exemplo completo

E aqui está um exemplo simples de uma ação do controlador com contextos estruturados e as afirmações mínimas esperadas. Aproveitar!

describe PostsController do
describe
'GET #index' do
context
'when user is logged in' do
with
:user
before
do
sign_in user

get
:index
end
it
{ is_expected.to respond_with :ok }
it
{ is_expected.to respond_with_content_type :html }
it
{ is_expected.to render_with_layout :application }
it
{ is_expected.to render_template :index }
end
context
'when user is logged out' do
before
do
get
:index
end
it
{ is_expected.to redirect_to new_session_path }
it
{ is_expected.to set_the_flash(:warning).to('Please log in.') }
it
{ is_expected.to set_session(:return_to).to(posts_path) }
end
end
end

NOTA: o withmétodo auxiliar RSpec vem da útil gem factory_girl_rspec e o respond_with_content_typematcher da gem shoulda- keep -respond-with-content-type .