Chainlink
Chainlink

Reputation: 730

Multiple Expectations in Rails Rspec test

I'm trying to test a ruby controller method, and I'm expecting multiple things to change in the database.

context "With an unknown user" do
  let(:unknown_phone_number) { "0000000000" }
  subject {post :create, twiml_message(unknown_phone_number, "YES", "To" => twilio_phone_number) }

  it { response.should change(Card, :count).by(1) }
  it { should change(User, :count).by(1) }
  it { should change(Customer, :count).by(1) }```
end

Gives the error

NoMethodError: undefined method `call' for #<ActionController::TestResponse:0x007fbd5d643030> ./spec/controllers/api/v1/sms_controller_spec.rb:25:in `block (4 levels) in <top (required)>'```

What am I missing, or am I barking up the completely wrong tree?

Version info: Rails 1.9.3 Rspec 2.11.0

Update: Based on Answer

it "registers an unknown user" do
    unknown_phone_number = "0000000000"
    expect {
      post :create, twiml_message(unknown_phone_number, "YES", "To" => twilio_phone_number)
    }.to change(Card, :count).by(1)

    open_last_text_message_for unknown_phone_number
    current_text_message.should have_body "Welcome to OnTab. You are now registered with a test account. To get started text TAB to open a tab."
  end

  it "registers an unknown user" do
    unknown_phone_number = "0000000000"
    expect {
      post :create, twiml_message(unknown_phone_number, "YES", "To" => twilio_phone_number)
    }.to change(Customer, :count).by(1)

    open_last_text_message_for unknown_phone_number
    current_text_message.should have_body "Welcome to OnTab. You are now registered with a test account. To get started text TAB to open a tab."
  end

  it "registers an unknown user" do
    unknown_phone_number = "0000000000"
    expect {
      post :create, twiml_message(unknown_phone_number, "YES", "To" => twilio_phone_number)
    }.to change(User, :count).by(1)

    open_last_text_message_for unknown_phone_number
    current_text_message.should have_body "Welcome to OnTab. You are now registered with a test account. To get started text TAB to open a tab."
  end

But this runs the post action three times. It there any way to do this, but only execute the POST once?

Upvotes: 5

Views: 5745

Answers (1)

Aaron K
Aaron K

Reputation: 6961

You probably want this type of test as an integration or request spec. Controllers should do more about asserting messages passed to models and setup of the view / return state. Though, to answer the question, the response object doesn't have a call method on it. It is the returned state - things have already changed. You want to assert that the post does the changing.

The expect change matcher needs to be attached to the block form of expect:

it do
  expect{ post :create, ... }.to change(User, :count).by(1)
end

Your current subject is not an expectation block, but the return value of the post action. So the implicit subject will not work for change. Also, as I stated above, you cannot use response.should for that same reason. You need to use the block form.

For this type of test, you will not be able to use the implicit subject, due to how RSpec handles expect{}. versus should.

Also, arguably, to make the subject an expectation block would change the responsibilities and roles. In general the object under test is that, an object. By setting it to an action you're changing this and will likely confuse others down the road. This may seem odd in this case, but that is due to Rails and how you interact with controller objects.

Update Based on Question Change:

Yes, it will make three post requests. I'm going to assume you only want to make one request because of "performance". That can be an issue but there are many things you can do to help mitigate performance instead of simply making one request.

One philosophy for speeding up tests is to stub Active Record in controller tests, that keeps things short and fast. Integration specs are then used for the type of testing you are talking about (read http://www.andylindeman.com/2012/11/11/rspec-rails-and-capybara-2.0-what-you-need-to-know.html). Yes, integration tests are slower normally; that is the price of doing a full stack test.

RSpec has a philosophy of 'test one thing'. You want to re-execute the object / method under test each time. This ensures that there is no cross test contamination which can lead to faulty results. As it is written, I would say you are already violating this be testing a database update and testing a text message expectation.

context 'registering a new user' do
  it 'saves the user in the database' do
    unknown_phone_number = '000'
    new_card = mock_model(Card)
    expect(Card).to receive(:new).and_return(new_card)

    expect(new_card).to receive(:save).and_return(true)

    post :create #...
  end

  it 'sends a welcome message to the phone' do
    # You can stub the text class here and assert on messages passed
    # As you probably trust that if the message is passed the text message code
    # does what it was designed to do (you have unit tests for it right?)
  end
end

Upvotes: 2

Related Questions