How to test/mock Trailblazer operations that interact with external gems?

902 views Asked by At

I'm loving all the object oriented beauty of Trailblazer!

I have an operation that interacts with a gem (called cpanel_deployer) to do something externally on the web. (It adds an addon domain to a cpanel.)

class Website::Deploy < Trailblazer::Operation
  attr_reader :website, :cpanel

  def process(params)
    real_cpanel_add_domain
    website.cpanel = cpanel
    website.save
  end

  private

  def setup!(params)
    @cpanel = Cpanel.find(params[:cpanel_id])
    @website = website.find(params[:website_id])
  end

  def real_cpanel_add_domain
    cp_domain = CpanelDeployer::Domain.new(website.domain)
    cp_panel = CpanelDeployer::Panel.new(cpanel.host, cpanel.username, cpanel.password)

    res = cp_panel.add_domain(cp_domain)

    raise StandardError unless res
  end

end

The cpanel_deloyer gem is already tested, so I don't need to retest it's functionality here. But in order to test the operation, I want to make sure CpanelDeployer::Panel#add_domain is called with correct args. So I'm thinking I should mock CpanelDeployer::Panel.

I believe it's bad practice to try to use any_instance_of. According to thoughtbot, it's usually considered code smell... They recommend using dependency injection. Is there a good way to use dependency injection within a trailblazer operation? Is there another best practice for this kind of situation?

2

There are 2 answers

0
Josh On

One option is to stub :new on the gem's classes and return test doubles. Here's what that looks like:

  describe Website::Deploy do

    let(:cpanel) { Cpanel::Create.(cpanel: {
      host: 'cpanel-domain.com', username: 'user', password: 'pass'
    }).model }

    let(:website) { Website::Create.(website: { domain: 'domain.com' }).model }

    it 'works' do
      fake_cp_domain = double(CpanelDeployer::Domain)
      fake_cp = double(CpanelDeployer::Panel)

      expect(fake_cp).to receive(:add_domain).with(fake_cp_domain).and_return(true)

      expect(CpanelDeployer::Domain).to receive(:new)
        .with(website.domain)
        .and_return(fake_cp_domain)

      expect(CpanelDeployer::Panel).to receive(:new)
        .with(cpanel.host, cpanel.username, cpanel.password)
        .and_return(fake_cp)

      Website::Deploy.(cpanel_id: cpanel.id, website_id: website.id)
    end
  end

This seems pretty cumbersome... Is there a better way?

1
tycooon On

Honestly, I don't really understand what real_cpanel_add_domain is doing, because it seems to me that it just assigns two local variables and then calls add_domain on one of them, how would that affect anything?

Speaking about dependency injection, I guess you can get domain and panel classes from params, defaulting to CpanelDeployer::Domain and CpanelDeployer::Panel, but passing some stubs in your specs.

I'm not a big fan of stubbing new method, because it not always works as expected.