Writing ‘good code’ is always a challenge for every project. Unfortunately, we always associate it with development and not tests. What about automation test code? Ever notice that you have write more automation code than the actual lines of development code?
Improper automation code leads to slow running specs, huge LOC in your spec files and a whole lot of confusion when you open a spec file. So, how does one avoid these problems?
Here are a few guidelines that I learned from my experience:
Tip #1: Follow Rspec style guide
That was obvious, wasn’t it? As surprising as it may seem, we invariably do not follow in detail.
- How often do you see more than one expectation in an example? bad
- How often do you see lots of stubs and mocks in test code? bad – keep it as simple as you can.
- How often do you see the use of context, especially in controllers? good
A lot of them are mentioned in the Style guide and we shall repeat below a few which require more detail.
Tip #2: Independent tests
We could use the word idempotent here but as a thumb rule there should not be any dependencies across specs and if we run the test multiple times in succession, it should give the same result So, it’s very important that every spec run in clean database environment. Use database cleaner gem properly, so that you don’t end of having to seed the database before each test! So, if you don’t want roles and countries being deleted,
config.before(:suite) do DatabaseCleaner.strategy = :truncation, {:except => %w[roles countries]} end
Tip #3: Use expect and not should syntax
It’s just nice and clean! Use the Transpec gem helps you to convert from should
syntax to expect
syntax
expect(User.all.count).to eq(1)
instead of
User.all.count.should == 1
Tip#4: Use the new syntax for object creation
Use the new FactoryGirl syntax that is supported in factory_girl4. This helps the code be clean of any harsh gem dependent syntax of class names!
create(:user)
instead of
FactoryGirl.create(:user)
Tip #5: Use let
variables instead of instance variables
This makes code look cleaner. For better understanding about let
read the answer given by Myron. Here is an example
describe Project do let(:project) { build(:project) } it "project should be valid" do expect(project.valid?).to eq(true) end end
Now, when the example executes it defines a method called project
if its not defined already or simply returns the value. Fast and efficient.
Tip #6: Use correct expectations for a spec
Obvious as it seems – the test we write should test the write stuff – not just make the test pass! Here is an example of making a test case pass.
context "#edit" do it "user should be able to edit his profile" do get :edit, id: user.id expect(response).to be_success end end
So, your expectation should ideally be
describe UsersController do context "#edit" do #render_views it "user should be able to edit his profile" do get :edit, id: user.id expect(response).to render_template("edit") end end end
Here #edit
is a method defined in UsersController
. So I defined a context
in my specs.
Do remember that by default all controller specs stub the template rendering unless and until you include render_views
Tip #7: Use shared examples.
When you want to test the same functionality across different subjects, for example different kinds of users, use shared examples.
context "#edit" do it "admin should be able to edit his profile" it "distributor should be able to edit his profile" end
You can write as
context "#edit" do shared_examples "profile_editing" do it "user should be able to edit his profile" end context "admin" do include_examples "profile_editing" it "should be able see to list of distributors" end context "distributor" do include_examples "profile_editing" it "should not able to see list of distributors" end end
A word of caution – you may lost readability if you use too many shared examples in single file.
Tip #8: Use conditional hooks
Sometimes if you want to execute some piece of code only for one example but not for all examples in a context then you can use conditional hooks.
context "#update" do before(:each, run: true) do # do something end it "example1", run: :true it "example2" end
Now before(:each)
will run only for first example but not for second example.
Tip #9: Profiling your examples
rspec gives you an option to examine how much time does a example group take to run. Simply run specs with the –profile flag.
rspec ./spec/models/user_spec.rb:51 --profile # output: 0.22431 seconds ./spec/models/user_spec.rb:51
Tip #10: Tagging
When you have a large test suite and you changed some code, how do you ensure that your change didn’t breaks existing functionality? One way is to run your specs locally then push then your Continuous Integration server that runs all specs. This is time consuming if you want to test only a specific set of tests.
Tag them! Run your module specified specs by using tags. For example, suppose we have a Commission feature and we written model specs, controller specs and integration specs for it. We can easily tag each file with commission:true
. So when we change something related to commissions, we can run only commission related specs!. We can tag our scenarios as below
# spec/models/commission_spec.rb describe Commission, commission: true do # do something end
rspec –tag commission
Note: We can tag entire context
or a single example in the spec file. For more information please follow this link
All these tips are a results of practical problems I have faced and overcome and a result of finding out the best we can do our own testing. More suggestions and tips are welcome!
Pretty useful tips indeed while doing your daily RSpec stuff ! “Conditional hooks tip” (#8) can prove to be very helpful in many scenarios.
Transpec automatically converts your specs to the latest RSpec syntax with static and dynamic code analysis.
LInk is : https://github.com/yujinakayama/transpec
Great article. It also gives a good overall look into RSpec for those of us not using it… yet.
In Tip #5. Shouldn’t this block:
it “project should be valid” do
expect(project.valid?).to eq(true)
end
be:
it “should be valid” do
expect(project.valid?).to be_true
end
?
I don’t know much about RSpec, so sorry if I’m wrong.
/Lasse
Actually, I think the better way of saying it is:
it “should be valid” do
expect(project).to be_valid
end
You’re right. Didn’t remember be_valid 🙂
@lasse bunk It’s all about choice of choosing matcher in your spec. Both you and @andrew kalek are correct.
Tip #11: example titles
Don’t use should in examples titles:
See how they’re done in the official docs: https://www.relishapp.com/rspec
Thanks for writing this blog post. Learned some new tricks 🙂 and I will use them in future!
This post is full of bad examples.
1. `context “#edit”`
Contexts are, well, contexts. Such as “user is admin”. And not “#edit”. This one should be `describe “#edit”`. Such a seemingly subtle misuse brings to the the issue #2.
2. `it “admin is able to edit his profile”`
That does not read well at all. Rspec is a lot about making test code read better. Otherwise why not just use minitest? `it` is about object under test (controller in this case), not the user. So your example should instead look like:
`context “user is admin” do
it “allows to edit profile”
`
3. shared_examples. This one is particularly bad. There is a code duplication and you say: I know what to do, I’ll use shared_examples. Except the duplication is still there. You test code runs twice for essentially the same functionality. This is bad for a number of reasons, such as slower test suite and more coupling.
Whether there were no shared_examples in rspec you might have instead asked yourself a question: WHY is there a duplication? Could that be me doing something wrong? And from there it would be easier to see that actually those tests should be changed into:
`context “user can edit profile”
it “renders the view”
…
context “user is not allowed to edit profile”
it “redirects to home page”
`
and the authorization logic (which roles can do what) should be tested in isolation elsewhere. Much faster. No duplication.
shared_examples are useful for testing different implementations of an interface (for example, Zip and Gzip could implement imaginary Archiver interface with #pack and #unpack). But I am not sure this use case outweighs the fact just how easy it is to shoot yourself in the foot with in all other cases when you don’t actually need them.
Also, last time I checked, failure output from shared spec was pointing out to its definition, and not where it failed in particular. Makes feedback cycle one step longer.
4. conditional hooks (the way you put it) is an anti pattern. Use `context` that clearly state the difference in test setup. So that next person who stumbles across this code knows what is going on.
I think you guys should give minitest a go. It does not have lots of fancy stuff and that makes it easier to concentrate on what is actually important: better code.
+1
To add to this:
5) the overuse of the word “should” in descriptions. instead of:
describe UsersController do
context “#edit” do
#render_views
it “user should be able to edit his profile” do
get :edit, id: user.id
expect(response).to render_template(“edit”)
end
end
end
let’s write correctly as:
describe UsersController do
describe “#edit” do
it “renders edit view” do
get :edit, id: user.id
expect(response).to render_template(“edit”)
end
end
end
The use of should in all our descriptions isn’t DRY. This reads nicely too. “UsersController #edit renders edit view” as opposed to: “UsersController #edit user should be able to edit his profile”
In regards to Contexts, I feel it easiest to read that as “when”. For example, “User #create when admin it creates an admin account, when non-admin it creates a non-admin account”, looks like so in code:
describe User do
describe “#create” do
context “admin” do
it “creates an admin account” do
end
end
context “non-admin” do
it “creates a non-admin account” do
end
end
end
end
I do like some examples here!
1) profiling is great! I will add you can just append -p, so rspec -p will print out the top 10 slowest tests and their times
2) let is always good, there’s let! too, let’s not forget that
3) expect vs should is a great. Besides the fact it reads better, it’s also used because .should binds itself to every object without actually having access to every object. There are times you will get errors because of this. “expect” doesn’t bind.
I will add a few more to your list:
11) use “described_class” when instead of using the class name. E.g. User.all == described_class.all
12) Use “trait” in your factories
13) use “.send” to test complex private methods
14) use shoulda-matchers gem for brevity, especially when testing models and associations
15) use .should_receive to catch method calls and conform to singularity of specs
etc. Good post! Thanks for sharing.
how can you write the test for mutliple traits and associations? I have a code as such
I have this 3 factories file ,
bank.rb has 2 traits (stdchtd, americaexpress)
interest.rb has 2 traits (blr, mlr)
mortgage.rb has 3 traits but is also associated to bank.rb and interest.rb
so e.g my mortgage.rb file looks like this
FactoryGirl.define do
factory :mortgage_loan do
association :interest_rate_reference, factory: :interest_rate_reference
association :bank, factory: :bank
trait :legacy_id_266 do
legacy_id 266
bank_id 1
name “Maxi Home”
loan_purpose “both”
is_conventional 1
is_islamic 0
interest_rate_reference_id 7
is_featured 0
end
trait :legacy_id_260 do
legacy_id 260
bank_id 1
name “Maxi Home”
loan_purpose “both”
is_conventional 1
is_islamic 0
interest_rate_reference_id 6
end
end
end
How do i dynamically call it in my features file. I tried doing this but got an Error
scenario “Test Database using Update Result” do
create(:interest_rate_reference, :blr)
create(:interest_rate_reference, :mlr)
create(:bank, :maybank)
create(:bank, :cimb)
create(:mortgage_loans, :legacy_id_260)
click_button(“Update Results”)
end
Can we create watsapp group with Rspec + Selenium Ruby