简体   繁体   中英

Rails, SRP, and Unit Tests

Preamble

This is my first time attempting to build an app with SRP in mind and really trying to use tests to drive the code for the site, rather than starting with my data architecture (ActiveRecord) and then building the app to fit.

But I'm running into problems. I've subscribed to and watched a lot of Destroy All Software screencasts and in theory I like what he preaches but I'm having trouble making it work in practice.

Problem at hand

I know the main feature of my app will be to search profiles based on location. So I write a simple Cucumber feature for that (purposely leaving off routes/controllers/etc for the time being to simplify the task at hand).

search.feature:

Feature: Search for profiles

  Scenario: By zipcode
    When I search for 90210
    Then I will see profiles near 90210

search_steps.rb:

When /^I search for 90210$/ do
  @profiles = ProfileSearch.new.near_location(90210).all
end

Then /^I will see profiles near 90210$/ do
  pending # express the regexp above with the code you wish you had
end

No problems, now on to specs:

profile_search_spec.rb:

require_relative '../../app/services/profile_search'

describe ProfileSearch do  
  it "finds nearby profiles"
  it "does not find far away profiles"
end

profile_search.rb:

class ProfileSearch
end

I've used a ProfileSearch class for a few reasons.

  1. It seems like the right thing to do to move as much business logic as possible outside of ActiveRecord (the Single Responsibility Principle).
  2. POROs make for faster tests (no loading Rails).
  3. I plan to use ElasticSearch or Solr in the near future and would like for the interface to remain the same.

I'm not really sure what to do next. ProfileSearch obviously depends on the Profile model, and I'm quite certain that this will be ActiveRecord.

So the question is do I start spec'ing out and building Profile and just start loading Rails in my tests? This seems like the easiest option but something about it seems wrong. I feel like I would be desiging and building behaviour that my app hasn't specifically called for yet. I would have to think about fields and relations and storage, etc., all of which my app currently shouldn't care about.

Or should I use stubs/mocks for all calls to Profile in my ProfileSearch spec and make sure the correct methods are being called? This also seems wrong because I wouldn't really be testing the behaviour then, and I'd have to rewrite the test when switching to Solr or ElasticSearch even though the same behaviour would be expected.

Or I should actually create a working Profile model that doesn't use ActiveRecord for the time being but responds correctly to all the right methods as Uncle Bob demonstrated when building his wiki thing? This seems like it might be the best approach theoretically but knowing I will be using ActiveRecord in the future it also seems quite redundant.

Or... f*ck it and throw everything in the ActiveRecord model :\\

There are so many patterns, principles, and best practices floating around in my head I don't know WTF to do.

What would you do?

First you need to choose your testing approach:

  • Cucumber is designed for BDD (outside-in) testing. Cucumber scenarios are meant to exercise your entire stack (web page, controllers, models, database — or whatever your stack is), not to be used as unit tests as you seem to be doing above. So if the first thing you write is a Cucumber scenario, you'll need to call all of your layers into being to make that first scenario pass. This is what I do, whether with Cucumber (my favorite) or RSpec feature specs. After you've done that you're free to refactor to meet whatever design criteria you like, and you can test-drive detailed requirements with unit tests.

  • Alternatively, you could pick the trickiest part of your stack, unit-test that into existence, and unit-test up and down the layers and maybe eventually acceptance-test until you have a whole app. On one hand, that might be a good idea to prove that you can get the tricky parts to work before you invest in the easy parts. On the other, I find that when I guess at component interfaces without a working acceptance test I always guess wrong, so I don't do it this way.

Either way, your goal is a ProfileSearch class that returns Profile instances but doesn't promise that they're ActiveRecord objects or anything else persistence-specific. How you handle those classes in tests depends on the type of test:

  • If you use acceptance specs (Cucumber or RSpec feature specs), they won't use stubs, they will use all real implementation classes, and they'll load Rails, because that's their point and that's how they're designed. Sorry, they'll be slow. Don't write any more of them than you need to specify your important scenarios.

  • In unit tests of classes that use ProfileSearch , you can stub it out and return POROs for speed. (If you're using rspec-rails, require 'spec_helper' and not rails_helper in those specs to avoid loading Rails when you run them by themselves.)

  • The easiest way to make unit tests of ProfileSearch itself pass is probably to just make Profile an ActiveRecord model and have ProfileSearch use Profile 's ActiveRecord functionality and return real instances of Profile . Those specs will need to require 'rails_helper' too.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM