I am trying to do some mocking in specs for controller. I have written the code that works just fine for case when I post valid article, but when I try to specify it shouldn't be saved, I am facing errors.
My code:
Article form:
class ArticleForm
include ActiveModel::Model
delegate :title, :body, :author_id, :tags, :id, :persisted?, :new_record?, to: :article
attr_accessor :article, :tags_string
validates :title, :body, :tags, presence: true
validates_length_of :title, within: 8..512
validates_length_of :body, within: 8..2048
validate :validate_prohibited_words
def initialize(article = Article.new)
@article = article
end
def save(article_params)
assign_params_to_article(article_params)
if valid?
@article.tags.each(&:save!)
@article.save!
true
else
false
end
end
...
end
Articles controller (only create action):
def create
@article_form = ArticleForm.new
if @article_form.save(article_params)
flash[:notice] = 'You have added a new article.'
redirect_to @article_form.article
else
flash[:danger] = 'Failed to add new article.'
render :new
end
end
_form:
= simple_form_for @article_form,
url: (@article_form.article.new_record? ? articles_path : article_path(@article_form.article) ) do |f|
= f.input :title, label: "Article title:"
= f.input :body, label: "Body of the article:", as: :text, input_html: { :style => 'height: 200px' }
= f.input :tags_string, label: "Tags:", input_html: { value: f.object.all_tags }
= f.button :submit, 'Send!'
Article controller spec:
require 'rails_helper'
RSpec.describe ArticlesController, type: :controller do
render_views
let!(:user) { create(:user) }
let!(:tag) { create(:tag) }
let(:tags_string) { 'test tag' }
let!(:article) { create(:article, :with_comments, tags: [tag], author_id: user.id) }
context 'user logged in' do
before { sign_in(user) }
describe 'POST artictles#create' do
let(:article_form) { instance_double(ArticleForm) }
let(:form_params) do
{
article_form:
{
title: 'title',
body: 'body',
tags_string: tags_string
}
}
end
context 'user adds valid article' do
it 'redirects to new article', :aggregate_failures do
expect(ArticleForm).to receive(:new).and_return(article_form)
expect(article_form).to receive(:save).with(hash_including(:author_id, form_params[:article_form]))
.and_return(true)
allow(article_form).to receive(:article) { article }
post :create, params: form_params
expect(response).to redirect_to(article)
end
end
context 'user adds invalid article' do
it 'renders new form', :aggregate_failures do
expect(ArticleForm).to receive(:new).and_return(article_form)
allow(article_form).to receive(:article) { article }
expect(article_form).to receive(:save).with(hash_including(:author_id, form_params[:article_form]))
.and_return(false)
post :create, params: form_params
expect(response).to render_template(:new)
end
end
end
end
end
Posting valid works fine, this is the error I get on 'invalid post':
Failures:
1) ArticlesController user logged in POST artictles#create user adds invalid article renders new form Got 1 failure and 1 other error:
1.1) Failure/Error: = simple_form_for @article_form, #<InstanceDouble(ArticleForm) (anonymous)> received unexpected message :model_name with (no args) # ./app/views/articles/_form.html.haml:2:in `_app_views_articles__form_html_haml__2443549359101958040_47338976410880' # ./app/views/articles/new.html.haml:2:in `_app_views_articles_new_html_haml___678894721646621807_47338976253400' # ./app/controllers/articles_controller.rb:26:in `create' # ./spec/controllers/articles_controller_spec.rb:51:in `block (5 levels) in <top (required)>' 1.2) Failure/Error: = simple_form_for @article_form, ActionView::Template::Error: undefined method `param_key' for #<Array:0x0000561bedbcd888> # ./app/views/articles/_form.html.haml:2:in `_app_views_articles__form_html_haml__2443549359101958040_47338976410880' # ./app/views/articles/new.html.haml:2:in `_app_views_articles_new_html_haml___678894721646621807_47338976253400' # ./app/controllers/articles_controller.rb:26:in `create' # ./spec/controllers/articles_controller_spec.rb:51:in `block (5 levels) in <top (required)>' # ------------------ # --- Caused by: --- # NoMethodError: # undefined method `param_key' for #<Array:0x0000561bedbcd888> # ./app/views/articles/_form.html.haml:2:in `_app_views_articles__form_html_haml__2443549359101958040_47338976410880'
I have made few tries to add missing methods by allowing the object to receive it, but is it a good thing to do? I would have to allow every single call later on (when I allow param_keys, it then asks for all the values for the _form - title, body and tags). Is there a way to get it working without specifying all the methods line by line?
Controller tests in Rails are used for "functional" testing. This means they test several layers of the of request that is being sent to application.
So basically you are trying to test that all the pieces that are involved in processing the request work. Because of this using mocks and stubs is not desired in these kinds of tests, because you are trying to make the test as "real" as possible. I would recommend instantiating a real ArticleForm
object instead of creating an instance double.
let(:article_form) { ArticleForm.new(article) }
This way you are also testing the ArticleForm
instance as well. There are cases where stubbing or mocking can make sense in a controller test as well, but in your case the article_form
instance is at the heart of the test, so using a mock for it, means awful lot of work and the test will get more complicated.
If you want to try out mocking, a better starting point could be view specs for example.