← Back to Upcase

TDD: expect(:object).to receive(:method) test fails but works manually

(Dan Weaver) #1

I’m using Mandrill to send email and I’m testing that my LessonNotifier class receives a call to the send_mail method.

Works fine like this:

# teacher_makes_lesson_spec.rb
  scenario 'email is sent to family', js:true do
    fill_in 'Title', with: 'Lesson Title'
    expect(LessonNotifier).to receive(:send_email)
    click_on 'Post Lesson'
  end

That test passes so I thought I’d got the correct syntax for the expect().to receive(), but the following test fails:

scenario 'resends email to family', js:true do
    @lesson = create(:lesson)
    visit root_path
    expect(page).to have_content @lesson.title
    within("article#lesson-#{@lesson.id}") do
      expect(LessonNotifier).to receive(:send_email)
      click_on 'Resend'
    end
  end

with this error:

Failure/Error: expect(LessonNotifier).to receive(:send_email)
       (<LessonNotifier (class)>).send_email(any args)
           expected: 1 time with any arguments
           received: 0 times with any arguments

The Resend link in the second example is set to remote:true and goes to a resend action in the LessonsController that fires the same call to LessonNotifier as the create action does.

Although the test fails, the feature works when I manually test it in development. I added some log output to the resend action:

# lessons_controller.rb
  def resend
    puts params.inspect
    @lesson = Lesson.find(params[:id])
    puts @lesson
    LessonNotifier.send_email(@lesson)
  end

I see the output on the dev logs but I don’t see any puts output in the test log using Guard. I have an empty resend.js file to satisfy the controller action.

I’ve only just started working with the expect().to receive() syntax so I may have this all wrong.

Any ideas where I can start to solve this and get the test passing?

(Rafael George) #2

@weavermedia Can you show the code for the LessonNotifier. It is my understanding that feature specs need to work more as an integration spec meaning that you don’t need to mock up heavily on then. I rather would test the resend method and if that particular class the one that you are mocking is an external dependency from my system then I will mock it. Let me know if this is helpful.

(Dan Weaver) #3

@cored LessonNotifier has just one method, send_email, used to build and send an email via Mandrill:

# lesson_notifier.rb
class LessonNotifier

  def self.send_email(lesson, lesson_url, student, event)

    return if Rails.env.test?

    mandrill = Mandrill::API.new ENV["MANDRILL_APIKEY"]

    vars = [
      # build email vars here
    ]

    message = {
      # set message content from method params
    }

    result = mandrill.messages.send_template template_name, [{}], message
    puts 'MANDRILL RESULT (LESSON EVENT): ' + result.to_s

  end

end

You see that for now I return out of the method if it’s hit by a test. I know this isn’t ideal but I’m still working all this out.

What puzzles me is that one test works with expect().to receive() and the other test doesn’t.

(Geoff Harcourt) #4

Hi @weavermedia,

I think when you’re doing a JavaScript-enabled spec with js: true that there may be some threading issues that are causing your test to fail (a second process is responsible for the server when you’re performing specs through Capybara or Selenium).

I think you may be better off testing this particular expectation (that the form/API triggers a lesson email) in a Controller spec. In your feature spec you can instead test for things that the user could see, such as a flash notification informing them that an email is on the way.

One other note: I’ve found it’s useful to use a slightly different form for mocks and expectations. If you follow the Setup-Exercise-Verify-Teardown phases, it helps group the things that are setup vs. the things that are expectations in your tests. In a controller spec, that part might look like this:

    describe "POST /api/resend_email" do
       it "triggers an email redelivery" do
         # setup phase
         lesson = build_stubbed(:lesson)
         allow(Lesson).to receive(:find).with(1).and_return(lesson)
         allow(LessonNotifier).to receive(:send_email) #note this doesn't need to return anything

         # exercise phase
         sign_in_as(user)
         post api_resent_email_path lesson_id: 1

         # verification phase
         expect(LessonNotifier).to have_received(:send_email).with(lesson, lesson.url, user, etc)
         expect(response.status).to be_successful
      end
    end

Most Rails tests don’t need a teardown phase (something like Database Cleaner is actually your teardown phase if you use that), but if you had any steps to clean up you’d put those after your expectations or in an after block. The nice thing about this organization is that someone (including a future you) can easily look at this test and tell what it’s supposed to be testing. When your expectations are sometimes at the top of the test, that can be very confusing.

3 Likes
(Dan Weaver) #5

Hey @geoffharcourt - thanks for your reply. Golden nuggets in there!

I think it was indeed the js: true causing the issue. I switched to testing the resend method in the controller spec as you suggested and it works fine.

I still have to stub something to write a feature test since the flash message the user will see is based upon a successful send response from my email sender (Mandrill). But rather than trying to stub the mailer method I’m trying out the Webmock gem and stubbing the specific network request and returning a success object. Seems to be working well so far.

I’ve also taken on board your suggestions for the multi phase tests and I’ll certainly be trying to do that from now on, and refactoring any tests I have to revisit in the future too.

Thanks again.

1 Like
(Geoff Harcourt) #6

@weavermedia very glad that was helpful.

I’m a big fan of WebMock (and the related VCR gem).

Looks like you’re on the right track!