└── readme.md /readme.md: -------------------------------------------------------------------------------- 1 | # Rspec Best Practices 2 | A collection of Rspec testing best practices 3 | 4 | ## Table of Contents 5 | 6 | * [Describe your methods](#describe-your-methods) 7 | * [Use Context](#use-context) 8 | * [Only one expectation per example](#only-one-expectation-per-example) 9 | * [Test valid, edge and invalid cases](#test-valid-edge-and-invalid-cases) 10 | * [Use let](#use-let) 11 | * [DRY](#dry) 12 | * [Optimize database queries](#optimize-database-queries) 13 | * [Use factories](#use-factories) 14 | * [Choose matchers based on readability](#choose-matchers-based-on-readability) 15 | * [Run specific tests](#run-specific-tests) 16 | * [Debug Capybara tests with save_and_open_page](#debug-capybara-tests-with-save_and_open_page) 17 | * [Only enable JS in Capybara when necessary](#only-enable-js-in-capybara-when-necessary) 18 | * [Consult the logs](#consult-the-logs) 19 | * [Other tips](#other-tips) 20 | * [More Resources](#more-resources) 21 | * [Libraries](#libraries) 22 | 23 | ## Describe your methods 24 | 25 | Keep clear the methods you are describing using "." as prefix for class methods and "#" as prefix for instance methods. 26 | 27 | ```ruby 28 | # wrong 29 | describe "the authenticate method for User" do 30 | describe "the save method for User" do 31 | 32 | # correct 33 | describe ".authenticate" do 34 | describe "#save" do 35 | ``` 36 | 37 | ## Use context 38 | 39 | Use context to organize and DRY up code, keep spec descriptions short, and improve test readability. 40 | 41 | ```ruby 42 | # wrong 43 | describe User do 44 | it "should save when name is not empty" do 45 | User.new(:name => 'Alex').save.should == true 46 | end 47 | 48 | it "should not save when name is empty" do 49 | User.new.save.should == false 50 | end 51 | 52 | it "should not be valid when name is empty" do 53 | User.new.should_not be_valid 54 | end 55 | 56 | it "should be valid when name is not empty" do 57 | User.new(:name => 'Alex').should be_valid 58 | end 59 | end 60 | 61 | # correct 62 | describe User do 63 | let (:user) { User.new } 64 | 65 | context "when name is empty" do 66 | it "should not be valid" do 67 | expect(user.valid?).to be_false 68 | end 69 | 70 | it "should not save" do 71 | expect(user.save).to be_false 72 | end 73 | end 74 | 75 | context "when name is not empty" do 76 | let (:user) { User.new(:name => "Alex") } 77 | 78 | it "should be valid" do 79 | expect(user.valid?).to be_true 80 | end 81 | 82 | it "should save" do 83 | expect(user.save).to be_true 84 | end 85 | end 86 | end 87 | ``` 88 | 89 | ## Only one expectation per example 90 | 91 | Each test example should make only one assertion. This helps you on find errors faster and makes your code easier to read and maintain. 92 | 93 | ```ruby 94 | # wrong 95 | describe "#fill_gass" do 96 | it "should have valid arguments" do 97 | expect { car.fill_gas }.to raise_error(ArgumentError) 98 | expect { car.fill_gas("foo") }.to_raise_error(TypeError) 99 | end 100 | end 101 | 102 | # correct 103 | describe "#fill_gass" do 104 | it "should require one argument" do 105 | expect { car.fill_gas }.to raise_error(ArgumentError) 106 | end 107 | 108 | it "should require a numeric argument" do 109 | expect { car.fill_gas("foo") }.to_raise_error(TypeError) 110 | end 111 | end 112 | ``` 113 | 114 | ## Test valid, edge and invalid cases 115 | 116 | This is called Boundary value analysis, it’s simple and it will help you to cover the most important cases. Just split-up method’s input or object’s attributes into valid and invalid partitions and test both of them and there boundaries. A method specification might look like that: 117 | 118 | ```ruby 119 | describe "#month_in_english(month_id)" do 120 | context "when valid" do 121 | it "should return 'January' for 1" # lower boundary 122 | it "should return 'March' for 3" 123 | it "should return 'December' for 12" # upper boundary 124 | end 125 | context "when invalid" do 126 | it "should return nil for 0" 127 | it "should return nil for 13" 128 | end 129 | end 130 | ``` 131 | 132 | ## Use let 133 | 134 | When you have to assign a variable to test, instead of using a before each block, [use let](http://stackoverflow.com/questions/5359558/when-to-use-rspec-let). It is memoized when used multiple times in one example, but not across examples. 135 | 136 | ```ruby 137 | # wrong 138 | describe User, '#locate' 139 | before(:each) { @user = User.locate } 140 | 141 | it 'should return nil when not found' do 142 | @user.should be_nil 143 | end 144 | end 145 | 146 | # correct 147 | describe User 148 | let(:user) { User.locate } 149 | 150 | it 'should have a name' do 151 | user.name.should_not be_nil 152 | end 153 | end 154 | ``` 155 | 156 | ## DRY 157 | Be sure to apply good code refactoring principles to your tests. 158 | 159 | Use before and after hooks: 160 | ```ruby 161 | describe Thing do 162 | before(:each) do 163 | @thing = Thing.new 164 | end 165 | 166 | describe "initialized in before(:each)" do 167 | it "has 0 widgets" do 168 | @thing.should have(0).widgets 169 | end 170 | 171 | it "does not share state across examples" do 172 | @thing.should have(0).widgets 173 | end 174 | end 175 | end 176 | ``` 177 | 178 | Extract reusable code into helper methods: 179 | ```ruby 180 | # spec/features/user_signs_in_spec.rb 181 | require 'spec_helper' 182 | 183 | feature 'User can sign in' do 184 | scenario 'as a user' do 185 | sign_in 186 | 187 | expect(page).to have_content "Your account" 188 | end 189 | end 190 | 191 | # spec/features/user_signs_out_spec.rb 192 | require 'spec_helper' 193 | 194 | feature 'User can sign out' do 195 | scenario 'as a user' do 196 | sign_in 197 | 198 | click_link "Logout" 199 | 200 | expect(page).to have_content "Sign up" 201 | end 202 | end 203 | ``` 204 | 205 | ```ruby 206 | # spec/support/authentication_helper.rb 207 | module AuthenticationHelper 208 | def sign_in 209 | visit root_path 210 | 211 | user = FactoryBot.create(:user) 212 | 213 | fill_in 'user_session_email', with: user.email 214 | fill_in 'user_session_password', with: user.password 215 | click_button "Sign in" 216 | 217 | return user 218 | end 219 | end 220 | ``` 221 | 222 | ```ruby 223 | # spec/spec_helper.rb 224 | RSpec.configure do |config| 225 | config.include AuthenticationHelper, type: :feature 226 | # ... 227 | ``` 228 | 229 | ## Optimize database queries 230 | 231 | Large test suites can take a long time to run. Don't load or create more data than necessary. 232 | 233 | ```ruby 234 | describe User do 235 | it 'should return top users in User.top method' do 236 | @users = (1..3).collect { Factory(:user) } 237 | top_users = User.top(2).all 238 | top_users.should have(2).entries 239 | end 240 | end 241 | 242 | class User < ActiveRecord::Base 243 | named_scope :top, lambda { |*args| { :limit => (args.size > 0 ? args[0] : 10) } } 244 | end 245 | ``` 246 | 247 | ## Use factories 248 | 249 | Use factory_bot to reduce the verbosity when working with models. 250 | 251 | ```ruby 252 | # before 253 | user = User.create( :name => "Genoveffa", 254 | :surname => "Piccolina", 255 | :city => "Billyville", 256 | :birth => "17 Agoust 1982", 257 | :active => true) 258 | 259 | # after 260 | user = Factory.create(:user) 261 | ``` 262 | 263 | ## Choose matchers based on readability 264 | 265 | RSpec comes with a lot of useful matchers to help your specs read more like language. When you feel there is a cleaner way … there usually is! 266 | 267 | Here are some examples, before and after they are applied: 268 | 269 | ```ruby 270 | # before: double negative 271 | object.should_not be_nil 272 | # after: without the double negative 273 | object.should be 274 | 275 | # before: 'lambda' is too low level 276 | lambda { model.save! }.should raise_error(ActiveRecord::RecordNotFound) 277 | # after: for a more natural expectation replace 'lambda' and 'should' with 'expect' and 'to' 278 | expect { model.save! }.to raise_error(ActiveRecord::RecordNotFound) 279 | 280 | # before: straight comparison 281 | collection.size.should == 4 282 | # after: a higher level size expectation 283 | collection.should have(4).items 284 | ``` 285 | 286 | ## Run specific tests 287 | 288 | Running your entire test suite over and over again is a waste of time. 289 | 290 | Run example or block at specified line: 291 | ```sh 292 | # in rails 293 | rake spec SPEC=spec/models/demand_spec.rb:30 294 | 295 | # not in rails 296 | rspec spec/models/demand_spec.rb:30 297 | ``` 298 | 299 | Run examples that match a given string: 300 | ```sh 301 | # in rails 302 | rake spec SPEC=spec/controllers/sessions_controller_spec.rb \ 303 | SPEC_OPTS="-e \"should log in with cookie\"" 304 | 305 | # not in rails 306 | rspec spec/login_spec.rb -e "should log in with cookie" 307 | ``` 308 | 309 | In Rails, run only your integration tests: 310 | ```sh 311 | rake spec:features 312 | ``` 313 | 314 | ## Debug Capybara tests with save_and_open_page 315 | 316 | Capybara has a `save_and_open_page` method. As the name implies, it saves the page — complete with styling and images — and opens it in your browser so you can inspect it: 317 | ```ruby 318 | it 'should register successfully' do 319 | visit registration_page 320 | save_and_open_page 321 | fill_in 'username', :with => 'abinoda' 322 | end 323 | ``` 324 | 325 | ## Only enable JS in Capybara when necessary 326 | 327 | Only enable JS when your tests require it. Enabling JS slows down your test suite. 328 | 329 | ```ruby 330 | # only use js => true when your tests depend on it 331 | it 'should register successfully', :js => true do 332 | visit registration_page 333 | fill_in 'username', :with => 'abinoda' 334 | end 335 | ``` 336 | Unless the pages you are testing require JS, it's best to disable JS after you're done writing the test so that the test suite runs faster. 337 | 338 | ## Consult the logs 339 | When you run any rails application (the webserver, tests or rake tasks), ouput is saved to a log file. There is a log file for each environment: log/development.log, log/test.log, etc. 340 | 341 | Take a moment to open up one of these log files in your editor and take a look at its contents. To watch your test log files, use the *nix tool tail: 342 | 343 | ```sh 344 | tail -f log/test.log 345 | ``` 346 | 347 | Curious what -f means? Check the man page for the tail utility: man tail 348 | 349 | 350 | ## Other tips 351 | * When something in your application goes wrong, write a test that reproduces the error and then correct it. You will gain several hour of sleep and more serenity. 352 | * Use solutions like [guard](https://github.com/guard/guard) (using [guard-rspec](https://github.com/guard/guard-rspec)) to automatically run all of your test, without thinking about it. Combining it with growl, it will become one of your best friends. Examples of other solutions are [test_notifier](https://github.com/fnando/test_notifier), [watchr](https://github.com/mynyml/watchr) and [autotest](http://ph7spot.com/musings/getting-started-with-autotest). 353 | * Use [TimeCop](https://github.com/travisjeffery/timecop) to mock and test methods that relies on time. 354 | * Use [Webmock](https://github.com/bblimke/webmock) to mock HTTP calls to remote service that could not be available all the time and that you want to personalize. 355 | * Use a good looking formatter to check if your test passed or failed. I use [fuubar](http://jeffkreeftmeijer.com/2010/fuubar-the-instafailing-rspec-progress-bar-formatter/), which to me looks perfect. 356 | 357 | ## More Resources 358 | * [Why Our Code Smells](https://dl.dropboxusercontent.com/u/598519/Why%20Our%20Code%20Smells.pdf) 359 | * [Request Specs and Capybara](http://railscasts.com/episodes/257-request-specs-and-capybara) 360 | * [How I Test](http://railscasts.com/episodes/275-how-i-test) 361 | * [Dmytro best practices](http://kpumuk.info/ruby-on-rails/my-top-7-rspec-best-practices/) 362 | * [Great Rails/Rspec example code](https://github.com/awesomefoundation/awesomebits/tree/master/spec) 363 | 364 | ## Libraries 365 | * [RSpec](https://github.com/rspec/rspec) 366 | * [Factory bot](https://github.com/thoughtbot/factory_bot) 367 | * [Shoulda Matchers](https://github.com/thoughtbot/shoulda-matchers) 368 | * [Capybara](https://github.com/jnicklas/capybara) 369 | * [Database Cleaner](https://github.com/bmabey/database_cleaner) 370 | * [Spork](https://github.com/sporkrb/spork) 371 | * [Timecop](https://github.com/travisjeffery/timecop) 372 | * [Guard](https://github.com/guard/guard-rspec) 373 | * [Fuubar](https://github.com/jeffkreeftmeijer/fuubar) 374 | * [Webmock](https://github.com/bblimke/webmock) 375 | --------------------------------------------------------------------------------