I've been really loving using contexts, subjects and its with rspec to really clean up my test code. Typical example:
context "as a user" do
subject{ Factory :user }
its(:name){ should == "Bob" }
end
What I can't figure out though is how I could make this condition dynamic (ie. based on other objects). its appears to instance eval the attribute within the block so I lose access to everything around it. I'd love to do something like:
its(:name){ should == subject.contact.name }
But I can't see any way of achieving this. Does anyone know if there is some a method proxied through to this instance eval that gives access to the origin object? Or if there's any other way I can use methods outside the scope of the instance of the attribute that I'm checking?
additional info
It seems as if subject within the its block gets changed to the actual attribute (name in this case)
Interestingly, I have access to any of my let methods, but again, I don't have access to my original subject in question.
You are almost answering yourself, use a let assignment before you set the subject. Then you can reference it everywhere:
context "as a user" do
let(:user) { Factory(:user) }
subject { user }
its(:name) { should == user.contact.name }
end
I'm a context, subject, its lover too !
Not sure if there's a way to do exactly what you're asking for since the "subject" within the block becomes the return value of User#name.
Instead, I've used the let method along with the following spec style to write this kind of test:
describe User do
describe '#name' do
let(:contact) { Factory(:contact, name: 'Bob') }
let(:user) { Factory(:user, contact: contact) }
subject { user.name }
it { should == 'Bob' }
end
end
This of course makes some assumptions about what your contact represents (here it's an association or similar). You may also choose to stub the return value of User#contact instead of relying on FactoryGirl to set up a "real" association.
Regardless of the choices you make on those fronts, this strategy has worked well for me. I find it allows me to be more concise about what is under test, while preserving info (for other devs & future self) about where the expected return value is coming from.
You can just set an instance variable within the subject block:
context 'as a user' do
subject { #user = FactoryGirl.create(:user) }
its(:name) { should == #user.name }
end
Related
Consider the following:
describe MyModel do
context 'updates fields' do
subject { create(:my_model) }
before do
subject.save
subject.reload
end
context 'when changing foo.bar' do
before { subject.foo.bar = 3 }
it { is_expected.to be_multiple_bar }
end
context 'when changing baz.quux' do
before { subject.baz.quux = 3 }
it { is_expected.to be_multiple_quux }
end
end
end
Now, as you may expect, I want the before hook on line 4 to be invoked after the ones on lines 10 and 15.
I've tried 2 things:
I have tried using prepend_before, but that only works when they're defined in the same context, it doesn't allow you to prepend a hook before one that's defined in the supercontext
I have tried using before(:context) on line 10 and 15, and while this should put them in the right order, RSpec doesn't allow me to mutate the subject at that point yet. (And for good reason, I'm not trying to create a shared state here.)
I really don't want to resort to let(:append_before) { proc { #magic here } }, because it's ugly and hacky as hell. Besides, I think what I want is totally reasonable. Right now I copied the two lines over to all subcontexts, which I am not too happy with.
What is a better way to do this?
I am on RSpec 3.7, FactoryGirl 4.8.0 and Ruby 2.3.1
I don't know what your factory looks like, but instead to creating and persisting my_model, modifying, saving and reloading it, you should create it only once. This will also speed up your specs.
You could write something like this:
describe MyModel do
context 'updates fields' do
subject { create(:my_model, foo: {bar: bar}, baz: {quux: quux}) }
context 'when changing foo.bar' do
let(:bar) { 3 }
it { is_expected.to be_multiple_bar }
end
context 'when changing baz.quux' do
let(:quux) { 3 }
it { is_expected.to be_multiple_quux }
end
end
end
Lazy evaluation of both let and subject makes sure you all your parameters are set correctly depending on the context. In case you need/want to extend your factory to support that, check out http://www.rubydoc.info/gems/factory_bot/file/GETTING_STARTED.md
I hope that makes sense.
I am new to Ruby and Rspec, and so I happened to found this bit of code:
Here is my Specification:
RSpec.describe Surveyor::Answer, '03: Answer validations' do
context "for a free text question" do
let(:question) { double(Surveyor::Question, type: 'free_text') }
# NOTE: The rating validations should not apply for 'free_text' questions.
subject { described_class.new(question: question, value: 'anything') }
it { should be_valid }
end
Here is my Class:
module Surveyor
class Answer
def initialize(question_answer)
#question = question_answer[:question]
#answer = question_answer[:value]
end
def question_type
# I want to check what is the type of question here.
# 'free_text' or 'rating'
# if free_text
# print question type
# else
# do something
end
end
My question is how can I print(puts) the type of question (free_text/rating) in Answer class?
When I tried using print question_answer[:question]it only gave me #<Double Surveyor::Question>
So I could not use question_answer[:question][:type]
You can access the type in the constructor simply: question_answer[:question].type, or later in object level methods: #question.type.
You can't access it like question_answer[:question][:type] because the double method in the test creates a classic like object rather than a hash.
A tip: when a method accepts parameters as a single hash you can simply name that as options or params but if you have only 3-4 params, you can use separate variables for params instead of a hash
How can I change from should to expect for the it in this code:-
context 'class_name' do
before do
Fabricator(:some_company, :class_name => Company) do
name 'Hashrocket'
end
end
after { Fabrication.clear_definitions }
its(:name) { should == 'Hashrocket' }
it { should be_kind_of(Company) }
end
I can see that the its will probably be:
expect(name.to eq 'Hashrocket')
but what should the it { should be_kind_of(Company) } become given the implicit subject.
Would it be
expect(it).to be_kind_of(Company)
?
I don't have the project set up yet from github (it is large).
There's no reason to change the short-form it blocks to the expect syntax. One of the reasons for the new syntax is to avoid the monkeypatching required to add should to every object. The short-form it blocks already avoid this problem.
However, if you need to access the implicit subject, you can say
expect(subject).to be_kind_of(Company)
Although I prefer to name my subjects explicitly:
subject(:company) { Company.new }
it 'something' do
expect(company).to be_kind_of(Company)
end
I've just started working with rspec, and I use expect instead of should convention.
How can I transform this test example from CanCan from should to expect?:
require "cancan/matchers"
# ...
describe "User" do
describe "abilities" do
subject { ability }
let(:ability){ Ability.new(user) }
let(:user){ nil }
context "when is an account manager" do
let(:user){ Factory(:accounts_manager) }
it{ should be_able_to(:manage, Account.new) }
end
end
end
You actually don't have to replace this instance of should, per Using implicit `subject` with `expect` in RSpec-2.11, but if you want to, you'd have to give up the one-liner approach and use:
it "should be able to manage a new account" do
expect(ability).to be_able_to(:manage, Account.new)
end
in place of the current it clause. As an aside, there looks to be some extraneous code in this test.
Is there a way to group tests conditionally with rspec? By which I mean, is there a way to say "if this variable is some value, run this set of tests. If this variable is some other variable, run this other set of tests"?
Basic Example of where it would be needed (doesn't actually work, obviously, but should show you what I want). Assume the user to be tested is defined elsewhere and the current user being tested is #user. Although you may have better alternatives to that, that's fine.
before do
login_as_user(#user) #This logs them in and brings them to the homepage to be tested
page.visit("/homepage")
end
describe "Check the user homepage"
subject {page}
it {should have_content("Welcome, #{#user.name}!")}
if(#user.role=="admin")
it {should have_link("List Users"}
end
end
Keep in mind I have no control over the user being tested - I cannot create users on the fly, for example, and no given user is guaranteed to exist, and I don't know offhand what combination of roles a given user will have. So I do need some way to say "run this test only if these conditions are met", rather than a way to create situations where every test can be run.
You can use a let (or possibly let!) to define who the user being logged-in should be. (Obviously replace #regular_user & #admin_user with the appropriate factory/fixture/etc.)
before do
login_as_user(user)
page.visit "/homepage"
end
let(:user) { #regular_user }
describe "Check the user homepage" do
subject { page }
it { should have_content "Welcome, #{#user.name}!" }
context "when an administrator" do
let(:user) { #admin_user }
it { should have_link "List Users" }
end
end
Okay, apparently the issue was as simple as this: #user is an instance variable, which only exists when the tests are being executed. Dynamic generation of tests does not work with instance variables for that reason.
However, by declaring it as a local variable somewhere outside any test-style blocks (before, after, it or specify), you can have access to it for the conditional logic.
The solution was as simple as taking the # sign off the front of the user.
in the same kind of idea you can use static variables
In order to easily switch between environment, I even use the system global variables
in my spec_helper.rb, I've set the default settings
default_env = {
'SITE_ROOT' => "http://localhost:3000",
'ACCOUNT_ID' => 'user',
'ACCOUNT_PASSWORD' => 'password',
'SUBMIT_REAL_MONEY' => 'false'
}
# setting default env variables if no ENV ones exists
default_env.each do |const_name, value|
val = ENV[const_name].nil? ? value : ENV[const_name]
eval_str = "#{const_name}='#{val}'"
puts eval_str
eval eval_str
end
Then I can specify my settings in the command line calling my specs:
SITE_ROOT='https://my.production.site/' SUBMIT_REAL_MONEY=true rspec
And here is a spec behind a simple condition:
if SUBMIT_REAL_MONEY == 'true'
it { should respond_with 200 }
end