I was wondering if someone could give a broad explanation (high level overview) of testing using Minitest and Mocha in Rails, and perhaps when to use which types of testing utilities.
I seem to be getting caught up not knowing when to use mock vs stub, expects vs stubs, and the order of operations when mocking and stubbing and knowing how to "break down" the functionality of code.
For an example (and yes, I understand I'm testing side effects, but in this case, I don't know what else to test):
Class to be tested:
# frozen_string_literal: true
class UploadJob < ApplicationJob
queue_as :upload
def perform(batch_id)
Logger.new.time('upload.time') do
Uploader.new(batch_id).upload_data
end
end
end
Initial test that didn't work:
# frozen_string_literal: true
require 'test_helper'
class TestUploadJob < ActiveSupport::TestCase
test 'UploadJob > perform > it should start uploading' do
Logger.stubs(:new).then.stubs(:time).with('upload.time').yields
Uploader.expects(:new).with(1).then.expects(:upload_data)
UploadJob.perform_now(1)
end
End result test that works:
# frozen_string_literal: true
require 'test_helper'
class TestUploadJob < ActiveSupport::TestCase
test 'UploadJob > perform > it should start uploading' do
logger_stub = stub
logger_stub.stubs(:time).with('upload.time').yields
Logger.stubs(:new).returns(logger_stub)
uploader_mock = mock
uploader_mock.expects(:upload_data)
Uploader.expects(:new).with(1).returns(uploader_mock)
UploadJob.perform_now(1)
end
To me, it felt more declarative in the first test to say the exact order things would be called in. In the second test, that works, first I'm stubbing or mocking a method call that comes after the call to <CLASS>.new.
What's a better way of thinking about this/approaching this problem of testing? Are there any good resources specific to testing with Minitest and Mocha, NOT Rspec, that would be suggested reads?
Thanks!
Related
I am trying to unit test a Plain Old Ruby Object that has a method which calls a class method on a Rails model. The Rails app is quite large (10s of seconds to load) so I'd prefer to avoid loading all of Rails to do my unit test which should run in under 1s.
Example:
class Foo
def bar
SomeRailsModel.quxo(3)
end
end
RSpec.describe Foo do
let(:instance) { Foo.new }
it 'calls quxo on SomeRailsModel' do
expect(SomeRailsModel).to receive(:quxo)
instance.bar
end
end
The problem here is that I need to require 'rails_helper' to load up Rails in order for app/models/some_rails_model to be available. This leads to slow unit tests due to Rails dependency.
I've tried defining the constant locally and then using regular spec_helper which kind of works.
Example:
RSpec.describe Foo do
let(:instance) { Foo.new }
SomeRailsModel = Object.new unless Kernel.const_defined?(:SomeRailsModel)
it 'calls quxo on SomeRailsModel' do
expect(SomeRailsModel).to receive(:quxo)
instance.bar
end
end
This code lets me avoid loading all of Rails and executes very fast. Unfortunately, by default (and I like this) RSpec treats the constant as a partial double and complains that my SomeRailsModel constant doesn't respond to the quxo message. Verifying doubles are nice and I'd like to keep that safety harness. I can individually disable the verification by wrapping it in a special block defined by RSpec.
Finally, the question. What is the recommended way to have fast unit tests on POROs that use Rails models without requiring all of Rails while also keeping verifying doubles functionality enabled? Is there a way to create a "slim" rails_helper that can just load app/models and the minimal subset of ActiveRecord to make the verification work?
After noodling a few ideas with colleagues, here is the concensus solution:
class Foo
def bar
SomeRailsModel.quxo(3)
end
end
require 'spec_helper' # all we need!
RSpec.describe Foo do
let(:instance) { Foo.new }
let(:stubbed_model) do
unless Kernel.const_defined?("::SomeRailsModel")
Class.new { def self.quxo(*); end }
else
SomeRailsModel
end
end
before { stub_const("SomeRailsModel", stubbed_model) }
it 'calls quxo on SomeRailsModel' do
expect(stubbed_model).to receive(:quxo)
instance.bar
end
end
When run locally, we'll check to see if the model class has already been defined. If it has, use it since we've already paid the price to load that file. If it isn't, then create an anonymous class that implements the interface under test. Use stub_const to stub in either the anonymous class or the real deal.
For local tests, this will be very fast. For tests run on a CI server, we'll detect that the model was already loaded and preferentially use it. We get automatic double method verification too in all cases.
If the real Rails model interface changes but the anonymous class falls behind, a CI run will catch it (or an integration test will catch it).
UPDATE:
We will probably DRY this up a bit with a helper method in spec_helper.rb. Such as:
def model_const_stub(name, &blk)
klass = unless Kernel.const_defined?('::' + name.to_s)
Class.new(&blk)
else
Kernel.const_get(name.to_s)
end
stub_const(name.to_s, klass)
klass
end
# DRYer!
let(:model) do
model_const_stub('SomeRailsModel') do
def self.quxo(*); end
end
end
Probably not the final version but this gives a flavor of our direction.
In RSpec I could stub method like this:
allow(company).to receive(:foo){300}
How can I stub a method with ActiveSupport::TestCase?
I have a test like this.
class CompanyTest < ActiveSupport::TestCase
test 'foobar' do
company = companies(:base)
#company.stubs(:foo).returns(300)
assert_nil(company.calculate_bar)
end
end
Minitest comes with a stub method out of the box, in case you don't wanna use external tools:
require 'minitest/mock'
class CompanyTest < ActiveSupport::TestCase
test 'foobar' do
company = companies(:base)
Company.stub :foo, 300 do
assert_nil(company.calculate_bar)
end
end
end
Minitest has some limited functionality for mocks, but I'd suggest using the mocha gem for these kinds of stubs.
The syntax for Mocha is exactly what you have on the commented out line:
class CompanyTest < ActiveSupport::TestCase
test 'foobar' do
company = companies(:base)
company.stubs(:foo).returns(300)
assert_nil(company.calculate_bar)
end
end
Enhancing #Farrukh answer:
If you want to verify the arguments passed, like allow(company).to receive(:foo).with(some_args).and_return(300),
You can use assert_called_with.
# requiring may not be needed, depending on ActiveSupport version
require "active_support/testing/method_call_assertions.rb"
include ActiveSupport::Testing::MethodCallAssertions # so we can use `assert_called_with`
assert_called_with(company, :foo, some_args, returns: 300) do
assert_nil(company.calculate_bar)
end
I'm writing some Rspec tests with capybara , and as part of that I need some methods of model.
I have created my model as:
class MyModel < ActiveRecord::Base
def method_name
#some stuff..
end
end
Now, I want to use MyModel in my Rspec test cases.
I tried to includeconfig.include Models in spec_helper.rb but it throws error
Uninitialized constant Models
And when I tried to include
include MyModel.new.method_name()
it throws error `include': wrong argument type nil (expected Module) (TypeError)
Without including any model class it runs test cases, but then my test cases are useless.
Here is my Rspec test case
require 'spec_helper'
describe "State Agency Page" do
let(:state_data) { FactoryGirl.build(:state_data) }
require 'mymodel.rb'
before {visit state_modifier_path}
it "should have breadcrumb", :js=>true do
page.should have_css('.breadcrumb')
end
end
Please provide any solution.
Thanks in advance.
I don't know what you mean by "your test cases are useless", but you seem to misunderstand the role of Ruby's include method.
With Rails, or the use of the rspec-rails gem with RSpec, your classes will be autoloaded when you reference the corresponding class constant (e.g. MyModel). So there generally is no need to do manual "loading" of individual models. Just make sure you have require 'spec_helper' at the beginning of your specs.
As for the errors you were getting with your attempts to use include, I suggest you find and read a Ruby reference to understand the semantics of the include method and why each attempt of yours failed in the way it did.
I have a bunch of Rails 3.1 controllers which all have very similar testing requirements. I have extracted out the common code (all Test::Unit style), e.g. the following three tests are completely reusable across all of them:
def create
new_record = { field_to_update => new_value }
create_params = { :commit => "Create", :record => new_record }
post :create, create_params
end
test "should_not_create_without_login" do
assert_no_difference(count_code) do create; end
assert_need_to_log_in
end
test "should_not_create_without_admin_login" do
login_as_non_admin
assert_no_difference(count_code) do create; end
assert_needs_admin_login
end
test "should_create" do
login_as_admin
assert_difference(count_code) do create; end
assert_redirected_to list_path
end
and I intended that it could go in an abstract class which inherits from ActionController::TestCase. Then each functional test would only need to override the abstract methods, ending up pleasingly small and clean, e.g.
class Admin::AvailabilitiesControllerTest < Admin::StandardControllerTest
tests Admin::AvailabilitiesController
def model ; Availability end
def id_to_change ; availabilities(:maybe).id end
def field_to_update; :value end
def new_value ; 'maybe2' end
def list_path ; admin_availabilities_path end
end
However, when I try this, it appears that the framework tries to run the test methods directly from the abstract class, rather than from the inherited class:
E
===================================================================================================
Error:
test_should_not_create_without_login(Admin::ControllerTestBase):
NoMethodError: undefined method `model' for test_should_not_create_without_login(Admin::ControllerTestBase):Admin::ControllerTestBase
test/lib/admin_controller_test_base.rb:7:in `count_code'
test/lib/admin_controller_test_base.rb:68:in `block in <class:ControllerTestBase>'
===================================================================================================
I've heard that other testing frameworks and gems can provide mechanisms for meta-programming of tests, so maybe I'm going about this in entirely the wrong way. But I've tried several things and looked at RSpec, coulda, shoulda, context, contest ... and I still can't see a way to achieve what I'm after. Any ideas? Thanks!
I finally figured this out - once I realised that this is a general Ruby Test::Unit issue rather a Rails testing issue, a quick google instantly revealed How do I inherit abstract unit tests in Ruby? which already had a good answer. Then the only missing piece was being able to use the syntactic sugar:
test "something should behave in a certain way" do
...
end
rather than
def test_something_should_behave_in_a_certain_way" do
...
end
I found the answer to this within the ActiveSupport codebase itself, under lib/active_support/test_case.rb:
extend ActiveSupport::Testing::Declarative
This module defines test as a class method (which is why extend is required rather than include).
So the complete solution looks like this:
# tests/functional/admin/availabilities_controller_test.rb
class Admin::AvailabilitiesControllerTest < ActionController::TestCase
tests Admin::AvailabilitiesController
include Admin::ControllerTests
# non-reusable tests and helper methods specific to this
# controller test go here
end
# lib/admin/controller_tests.rb
module Admin::ControllerTests
extend ActiveSupport::Testing::Declarative
test "this test can be reused by anything which includes this module" do
...
end
end
The downside of this module-based approach is that you can't override the included tests. I guess that's just a fundamental limitation of Test::Unit - maybe the best answer is to move to RSpec, but I don't know it well enough yet to be sure.
When I have many tests in one test class, I use Module to group the tests.
Since I'm very new to rails and my question is:
Is it correct way to group tests or I'm doing very stupid things without knowing other side-effects?
Here is the code:
require 'test_helper'
module AttributeValidationTest
extend ActiveSupport::Testing::Declarative
test "should not ...." do
# .....
end
# other tests here....
end
module AnotherGroupTest
extend ActiveSupport::Testing::Declarative
# tests.....
end
# may be another modules..
class MyModelTest < ActiveSupport::TestCase
include AttributeValidationTest
include AnotherGroupTest
end
Thanks.
The question is: what do you gain by grouping the tests in modules?
I, in my humble tradition, just group similar tests by placing them not far from each other, and by giving them the same name prefix, like:
def test_user_name_handles_strange_chars
def test_user_name_handles_empty_string
def test_user_name_...
(you can without problems use the new syntax, as test "name should handle strange....")
This helps me to test only parts of the functionality (as my full test suite takes about an hour):
cd test && ruby unit/user_test.rb -n /test_user_name_/