A new project with the RSpec 3.2 and Rails 4.2.
As a good citizen I'm trying to do the "right" thing with the new RSpec (disable monkey patching, do not infer the type of examples etc).
The problem I have is that I want to test this little class:
# app/models/session.rb
class Session # NOTE: This is a simple transient class, almost plain Ruby class
include ActiveModel::Model
attr_accessor :email, :password
end
with something like:
# spec/models/session_spec.rb
require 'spec_helper'
RSpec.describe Session, type: :model do
it { should validate_presence_of :email }
it { should validate_presence_of :password }
end
Obviously, when I run the spec I'm getting the uninitialized constant Session (NameError) which makes sense as the 'spec_helper' does not do anything to load that particular class.
I could have user the rails_helper instead but that is simply too much for that little class since it has nothing to do with rails at all.
Willing to do "the right" I'm puzzled with the following questions
Does the Session spec even need to be marked with type: :model?
Should I modify the spec_helper to change the $LOAD_PATH?
If not, is there a way to autoload session.rb file but without loading up the whole Rails?
Should the transient Session class be under app/models? I treat it as a model, it's just not persistent.
The easiest thing to do is of course to just use rails_helper, but for what I need to do that is unnecessary and I'd love to keep the spec file fast.
The point of distinguishing between rails_helper and spec_helper is that you want just a minimal setup with spec_helper, so I would recommend against modifying $LOAD_PATH there. The only advantage you get is that specs run quicker if you run only specs that use only spec_helper, for example when you run just the single spec file with rspec spec/models/session_spec.rb. If you run the entire suite and require rails_helper in any spec, it will generally not run any faster.
Basically, you have two options:
If you don't bother about the single spec file running a bit slower than it could, and you don't want to require the Session model manually, I would say it's perfectly fine to use rails_helper for the sake of simplicity.
If you actually do care about the single spec file running a bit faster, you can require_relative '../app/models/session' manually at the top of the spec file. Autoloading is Rails magic, and you don't want Rails for speed reasons; therefore, you need to do it manually.
You don't need to mark the spec as type: :model unless you have specific spec helpers that are only included for type: :model and you intend to use them. However, it makes sense to mark them that way when you treat it like a regular model and use rails_helper.
Related
I a have some code I'd like to refactor out of my step definitions and put them inside.. helpers?
Oh and please also say how to include them, I am really having a hard time finding any solid info on that.
Straight from the rspec documentation here: https://www.relishapp.com/rspec/rspec-core/docs/helper-methods/define-helper-methods-in-a-module#include-a-module-in-all-example-groups
Include a module in all example groups
Given a file named "include_module_spec.rb" with:
require './helpers'
RSpec.configure do |c|
c.include Helpers
end
RSpec.describe "an example group" do
it "has access to the helper methods defined in the module" do
expect(help).to be(:available)
end
end
When
I run rspec include_module_spec.rb
Then
the examples should all pass
You may also benefit from a a support/helpers folder Or equivalent which is covered pretty well here: How to include Rails Helpers on RSpec
I have a large model that takes time to initialize in my RSpec tests.
I want it potentially available to every example, but want to only load it if an example requires it.
This seems like the perfect use for let()'s lazy loading - only load it when you need it.
In any particular spec file I can do
require "spec_helper"
feature "foo" do
let(:big_class) { MyBigClass.new(bar) }
...
end
This will make big_class available to every example in that spec file.
Is there a way to make this more global so that EVERY spec file and example can use it? I couldn't find a good way to initialize let inside the spec helper.
You may simply define a shared context and include it in every example. Regarding your particular question, it should look like following:
RSpec.shared_context "Global helpers" do
let(:big_class) { MyBigClass.new(bar) }
end
RSpec.configure do |config|
config.include_context "Global helpers"
end
However, it's rarely a good idea to include a shared context in all examples, and that big_class helper from your question really looks like something domain-specific. You can steer the shared context inclusion by metadata, for example when you want to include given shared context in feature specs only (they all have :type => :feature metadata set by default), you can do it this way:
RSpec.configure do |config|
config.include_context "Feature spec helpers", :type => :feature
end
You might consider other approaches:
Use mock objects instead of real ones.
Refactor the initializer and extract the slow operation to another method
Mock objects of course bring their own set of drawbacks; they can become stale and make tests more brittle. But for some tests that is not an issue.
Refactoring initializers is a favorite of mine. E.g.
MyBigObject.new(args)
becomes
MyBigObject.new(args).setup
or :load_data or :connect_to_database_on_the_moon or whatever is taking a long time. You get the picture.
Obviously this means changing your code, but I find that often works out to be helpful in other ways, and it certainly makes testing easier.
You don't want to use let. From the docs:
Use let to define a memoized helper method. The value will be cached across
multiple calls in the same example but not across examples.
You'll end up instantiating MyBigClass lots of times. I would recommend creating a global helper method somewhere in spec_helper.rb (or similar) that used memo-ization on it's own to return the cached value if it's already been setup.
Also be very careful with all this as you're violating the rule of isolated tests. Might be fine for what you're doing, but it's a red flag.
Use global before hook with an instance variable
From the docs:
RSpec.configure do |c|
c.before(:example) { #big_class = MyBigClass.new(bar) }
end
RSpec.describe MyExample do
it { expect(#big_class).to eql(MyBigClass.new(bar)) } # This code will pass
end
For more details check the suggestions in this answer
This website lists the various generators available to create spec files for specific rails files.
I know how to generator a spec file for a model.
Ex: rails generate rspec:model Customer creates this file:
#spec/models/customer_spec.rb
require 'rails_helper'
RSpec.describe Customer, type: :model do
pending "add some examples to (or delete) #{__FILE__}"
end
I created a service: MYPORO. It is a PORO (plain old ruby object), located at app/services/my_poro.rb.
How do I generate a spec file in order to create tests for the MyPoro class and its instances? The biggest issue I have is I don't know what type to provide, because it is not a :model. I would just manually create the file, but again: I don't know what :type to provide inside the file.
No "Type" Required for POROs
The biggest issue I have is I don't know what type to provide, because it is not a :model. I would just manually create the file, but again: I don't know what :type to provide inside the file.
You don't need to specify a type for plain Ruby objects. The :type tells the RSpec Rails extensions loaded by the rails_helper module (installed by rspec-rails) how to run certain types of tests for models, routes, and so forth.
I generally still load rails_helper instead of spec_helper for these types of specs, but in most cases you should be able to simplify to something like:
require 'spec_helper'
describe Customer do
pending "insert tests on your non-Rails Customer class"
end
touch spec/services/my_poro_spec.rb ?
You don't need generators for everything. ;)
I am currently testing a Rails(4.2) helper with Rspec(3) successfully. However, the test file setup is a bit cumbersome. How can I streamline the require and/or include lines?
# spec/helpers/nav_helper_spec.rb
require 'spec_helper'
require_relative '../../app/helpers/nav_helper' # this seems bulky
describe NavHelper do
include NavHelper # this seems repetitive
...
end
Thanks in advance!
If you have a "default" setup you probably have a rails_helper in addition to your spec_helper. If you don't mind loading all of the Rails directories in this one spec (a bit of a performance hit) you can require that instead of the spec_helper (cleaning up the requires). But there's nothing wrong with including only what you need, it will run faster.
Rspec will also mix in the helper for you if it knows the spec type. You can either include config.infer_spec_type_from_file_location! in your spec helper, or include the type in the describe declaration:
describe NavHelper, type: :helper do
Either way you'll be able to use something like expect(helper.method_name).to eq(result) without explicitly including the module.
I have a collection of parsers that differ significantly in logic, but have the exact same methods and outputs.
I have devised somewhat of a master Rake and I am curious if what I have come up with could lead to some unexpected or weird behavior.
Essentially my parse.rake looks something like:
require 'app/models/provider'
require 'config/environment.rb'
Provider.all.each do |provider|
require "lib/tasks/#{provider.name}.rb"
Kernel.const_get(provider.name).provider = provider
namespace provider.name do
task :provider_task do #Parse method
Kernel.const_get(provider.name).provider_task()
end
end
end
Since classes are constants in ruby, Kernel.const_get is what I used to access class methods from a varname. My classes look like (ABC.rb):
Class ABC
cattr_accessor :provider
def self.provider_task()
#Parse and add stuff to environment
end
end
With this setup, I can easily invoke rake ABC:provider_task to run a specific parser. Also the cattr_accessor allows me to easily refer to the actual provider model. Thoughts?
I have used this with no issues for a few weeks. After reviewing with some colleagues, I found a few who previously used a similar approach with no issues. Only potential danger is naming conflicts with your ruby classes.