← Back to Upcase

Stubbing out ActiveRecord models in Service tests


(Ian Drysdale) #1

I’m following a TDD approach to building our app, and creating a whole bunch of service objects, keeping models strictly for data management.

Many of the services I’ve built interface with models. Take for example MakePrintsForRunner:

class MakePrintsForRunner

  def initialize(runner)
    @runner = runner
  end

  def from_run_report(run_report)
    run_report.photos.each do |photo|
      Print.create(photo: photo, subject: @runner)
    end
  end

end

I appreciate the create method could arguably be abstracted into the Print model, but let’s keep it as is for now.

Now, in the spec for MakePrintsForRunner I’m keen to avoid including spec_helper, since I want my service specs to be super fast.

Instead, I stub out the Print class like this:

describe RunnerPhotos do
  
  let(:runner) { double }
  let(:photo_1) { double(id: 1) }
  let(:photo_2) { double(id: 2) }
  let(:run_report) { double(photos: [photo_1, photo_2]) }
  
  before(:each) do
    @service = RunnerPhotos.new(runner)
  end
  
  describe "#create_print_from_run_report(run_report)" do
    
    before(:each) do
      class Print; end
      allow(Print).to receive(:create)
      @service.create_print_from_run_report(run_report)
    end
    
    it "creates a print for every run report photo associating it with the runners" do
      expect(Print).to have_received(:create).with(photo: photo_1, subject: runner)
      expect(Print).to have_received(:create).with(photo: photo_2, subject: runner)
    end
  end

end

And all goes green. Perfect!

… Not so fast. When I run the whole test suite, depending on the seed order, I am now running into problems.

It appears that the class Print; end line can sometimes overwrite print.rb's definition of Print (which obviously inherits from ActiveRecord) and therefore fail a bunch of tests at various points in the suite. One example is:

NoMethodError:
  undefined method 'reflect_on_association' for Print:Class

This makes for an unhappy suite.

Any advice on how to tackle this. While this is one example, there are numerous times where a service is directly referencing a model’s method, and I’ve taken the above approach to stubbing them out. Is there a better way?


(Rafael George) #2

Well you already see which is the problem; in this case you are defined the Print class again. What I do with my service objects is to use DI through the constructor. So in your case the constructor should end like this def initialize(runner, print_repository) I name the AR models variables repository because in that manner is how I’m using it.

Then in your spec/test you just have to send it a double expecting the method call for the create; and that’s exactly what you are doing. Another thing that I normally do is just to wrap a method for the create behaviour inside the AR model that way nobody knows how to interact with the internals of the model attribute and if I ever change an attribute name I just have to go to one place to change it.

Hope this helps you,

Cheers


(Samnang Chhun) #3

I solve the problem by:

# spec/spec_helper.rb
Rails.application.eager_load! if ENV["FORCE_EAGER_LOAD"].present?

Here is when you run a whole spec:

$ FORCE_EAGER_LOAD=t bundle exec rspec spec

(Rafael George) #4

@samnang What did you think about my approach on solving the problem?


(Samnang Chhun) #5

@Rafael_George I think it’s a good idea to inject dependencies, but in my experiences dependencies that are AR models, I don’t usually inject them because they don’t usually have polymorphic on those models, and I don’t want in controllers know too much on instantiating service objects and inject all AR models that they need. You could solve it by using optional parameters with AR models as default values, but I still I don’t like that approach.

Wrapping a method is a good idea, but be careful if you move it into models and you shouldn’t have any conditional logics which are business logic of the application into that layer. Application business logics shouldn’t be in AR models.


(Rafael George) #6

@samnang Can you elaborate a little bit more regarding the polymorphic part; I don’t think I follow you. Normally what I do with my service is to add a factory method to it something like

  class Service 
      def self.builder(dependencies)
          new(dependencies).do_something
      end 
  end

In that way the controller will not know anything about how the service will get instantiate it. I also don’t like to use default parameters because the client won’t know which are the actuals dependencies for that particular service.

Regarding the wrapping part; totally agree my rule is not to wrap methods that will have conditional in it inside AR. But wrapping creation method and other internal API thing from AR is always a plus for me.

Thanks for your insight into this.


(Samnang Chhun) #7

@Rafael_George what I mean by “polymorphic parameter” here, for example my service objects depends on JSONParser, in this case I would prefer to use dependency injection because service object should not couple to a specific parser, latter I could pass in CSVParser, then my service object should just work. For AR model, I usually just use it directly without injecting it.