RSpec Best Practices

Ben and Joe discuss Joe's recommendations for RSpec best practices. You can read Joe's original blog post, peruse our style guide's testing section, or review our summarized best practices for RSpec. You might also want to read about the mystery...
This is a companion discussion topic for the original entry at https://thoughtbot.com/upcase/videos/rspec-best-practices

This was a really great discussion. As someone who is fairly new to TDD, I sometimes feel like Iā€™m not being efficient because Iā€™m not using all the bells and whistles of spec. These sorts of discussions give me grounding in why you would choose to use some of these ā€œextras.ā€ Thanks!

On another note, what caught my attention most was the @jferris comment about sinatra. I have recently adopted sinatra as my go-to tool for simple, well-constrained tasks where something like rails would be overkill. Most specifically, I use it when I need to create a fairly simple REST API, or a super lightweight UI. Iā€™d love to know what some of the sinatra anti-patterns I should be aware of.

I guess I could easily see sinatra-bloat occurring if you kept adding routes and embedding more and more code in the sinatra file. Iā€™ve been avoiding this by having each route just create an object that does all the work and returns the JSON data to the caller. In this way, my sinatra file, feels a bit like a rails routes file. I can easily go through, find the routes and look at a couple simple lines of code that tell me whatā€™s going on. Everything else is nicely tucked away in Ruby classes outside the sinatra file.

Does that seem like a reasonable approach? What aspects of sinatra should I be fearing or avoiding?

I have switched to the Rails API gem for RESTful services. Itā€™s fast, provides all the niceties of routes and request handling of Rails, and can provide Active Record and the great Rails features added to core classes without the rest of the Rails stack. Read the Rails API gem documentation for more rationale why to choose it.

Thanks for pointing that out. I didnā€™t know about this gem. Iā€™m sticking with my sinatra methodology for now (because itā€™s working and feels lightweight and maintainable) but for future projects I will seriously consider this approach. Thereā€™s a big win in using a file structure framework thatā€™s a standard vs. inventing my own.

I have yet to find a situation where a Sinatra app is more efficient than Rails. The only thing I use Sinatra for is for writing HTTP fakes for Rails test suites to hit.

Hereā€™s what Iā€™ve found:

  • For very simple applications, you spend too much time setting up Sinatra and writing boiler plate. Thereā€™s no generator, so you have to write everything from scratch. Itā€™s hard to be deploy-ready out of the box, because there are no environment configs or migrations. You need to create a schema by hand and figure out how to serve your assets efficiently.
  • For more complex applications, there isnā€™t enough infrastructure. Rails resource style routes are more efficient for declaring a lot of routes at once. The conventions for file locations and resource mapping save a lot of boiler plate and noise for each action you add, making Sinatra hurt more the larger your application gets. Things like the asset pipeline and migrations are a must-have for consistent deploys.

Basically, Sinatra takes too long to get started, so itā€™s not useful for little apps. However, it provides no support for larger apps, so Iā€™m not sure why people use it for anything but local demos and playing around. I doubt Iā€™ll ever deploy another Sinatra app.

@jferris one of the topics mentioned in this episode is avoiding using Mystery Guests in your tests by using plain old ruby methods. Am I still adhering to that principle using methods placed in another file, say in spec/support? Would you say the importance lies in naming those methods, so that even though I canā€™t see that method right there in the same file, if I name it user_with_one_post then I know what that is calling and therefore itā€™s not a Mystery Guest?

Improved naming definitely helps. The name ā€œMystery Guestā€ comes from [XUnitPatterns]( Obscure Test at XUnitPatterns.com Guest):

The test reader is not able to see the cause and effect between fixture and verification logic because part of it is done outside the Test Method.

In XUnitPatterns, parlance, ā€œfixtureā€ means the setup phase of the test, which is anything before the method-under-test is executed, and ā€œTest Methodā€ is an RSpec it block.

Their first recommendation is to try inlining as much setup as possible:

Using a Fresh Fixture built using Inline Setup is the obvious solution for Mystery Guest.

Basically, youā€™d ideally have user = something in your test somewhere, and the ā€œsomethingā€ would start out being explicit as possible.

XUnitPatterns also recommend extracting methods specifically for removing irrelevant information:

To avoid Irrelevant Information, we may want to hide the details of the construction behind one or more evocatively-named Creation Methods that append to the fileā€™s contents.

In Rails, you can use factory_girl, which is a library specifically for defining Creation Methods.

The last recommendation they have is to use well-named methods:

If we must use a Shared Fixture or Implicit Setup, we should consider using evocatively named Finder Methods (see Test Utility Method) to access the objects in the fixture. If we must use external resources such as files, we should put them into a special folder/directory and give them names that make it obvious what kind of data they hold.

Essentially, the recommendation there is to use a naked method call only if thereā€™s no other relevant information (any user will do) or thereā€™s no better option (you must extract the fixture to eliminate duplication, test slowness, or something else).

In this case, I agree with all of the recommendations made by XUnitPatterns.

Summary: Naming things well is always useful, but removing relevant test setup from the test should be avoided until thereā€™s a stronger reason to pull it out.

@jferris Nice episode. I wouldā€™ve liked to see some examples, but I understand your point of view.

One problem I get often is when I need to stub an instance of class, it always seems messy. One way of dealing with it would be to inject the dependency, but thatā€™s not always desirable. How do you deal with this situation?

I try to use dependency injection, but in cases where I actually stub class methods, I generally wrap those up in helper methods along these lines:

def stub_authenticator(auth_hash)
  double('authenticator').tap do |authenticator|
    Authenticator.stub(:new).with(authenticator).and_return(authenticator)
  end
end

def stub_find_user(attributes = {})
  build_stubbed(:user, attributes).tap do |user|
    User.stub(:find).with(user.to_param).and_return(user)
  end
end
2 Likes

It looks like a good way of abstracting this.

I prefer DI as well, but in places like controllers, it not easy to do.

Thank you so much for thisā€¦ I was a let whore for awhile and did exactly each one of those examples and then I would go back a few months down the line and be like WTH is this!!!

I am trying to improve my testing and I have a gist with some codeā€¦

Basically what I am testing if is a signed in or notā€¦ Is there a better way to communicate through the testing? I really how like in the thoughtbot rspec book he says not to hit the database when you dont need toā€¦

Please take a look and add comments so I can improve!!

@jferris I usually deploy Sinatra apps when the client does not need a backend ( basic static site with a mailer ). Basically if the client needs a backend I use rails, if it all static I use Sinatraā€¦

2019 and this is still 100% relevant, I wish all my Ruby friends understand this approach to best practices for RSpec. Good job @jferris.