36

Just switched from Cucumber+Webrat to Cucumber+Capybara and I am wondering how you can POST content to a URL in Capybara.

In Cucumber+Webrat I was able to have a step:

When /^I send "([^\"]*)" to "([^\"]*)"$/ do |file, project|
  proj = Project.find(:first, :conditions => "name='#{project}'")
  f = File.new(File.join(::Rails.root.to_s, file))
  visit "project/" + proj.id.to_s + "/upload",
        :post, {:upload_path => File.join(::Rails.root.to_s, file)}
end

However, the Capybara documentation mentions:

The visit method only takes a single parameter, the request method is always GET.always GET.

How do I modify my step so that Cucumber+Capybara does a POST to the URL?

Clinton
  • 3,578
  • 3
  • 24
  • 33
  • 1
    Why don't fill your for with your file and submit it. In cucumber scenario, you need to be like a user and do only clik by click. A user can't made a post method without some form. – shingara Nov 03 '10 at 07:45
  • @shingara Thanks for the suggestion, I was abusing cucumber a little as there is no form for this particular action. It is for different clients (software) to dump content to the application. I want to simulate clients dumping data into the application. – Clinton Nov 03 '10 at 10:08
  • the capybara using is not good if you don't use some javascript. – shingara Nov 03 '10 at 10:10
  • @shingara - That is an interesting idea, attempt to post the data via javascript? – Clinton Nov 03 '10 at 10:11
  • no migrating from webrat to capybara it's only if you really need test some javascript in your code. If you haven't, it's not a good choice to migrate. – shingara Nov 03 '10 at 10:12
  • You can try to add a form somewhere in your app to do this post request. And use it to made your post. – shingara Nov 03 '10 at 10:13
  • works for me: http://stackoverflow.com/a/7272860/316700 – fguillen Apr 30 '13 at 14:35
  • To the naysayers: while it **should not** be possible for a user to post without a form, it is; I found a situation where hitting "back" causes Chrome to repeat the `POST`. Naturally I want to test that. (And of course, the user could use `curl`.) Sometimes we need to drop down an abstraction level. – Nathan Long Aug 22 '14 at 18:52

8 Answers8

24

More recently I found this great blog post. Which is great for the cases like Tony and where you really want to post something in your cuke:

For my case this became:

def send_log(file, project)
  proj = Project.find(:first, :conditions => "name='#{project}'")
  f = File.new(File.join(::Rails.root.to_s, file))
  page.driver.post("projects/" + proj.id.to_s + "/log?upload_path=" + f.to_path)
  page.driver.status_code.should eql 200
end
Clinton
  • 3,578
  • 3
  • 24
  • 33
  • 20
    Unfortunately I'm getting `undefined method 'post' for # (NoMethodError)`. – Ramon Tayag Jul 27 '11 at 01:49
  • 3
    Looks like the latest versions of capybara break this -- my gemfile currently restricts to an earlier version: gem 'capybara', "0.4.1.2" – Clinton Sep 08 '11 at 23:21
  • 1
    Yep, this happened for me too. Used to work on another computer a few months ago. I guess running an earlier version will fix it. – B Seven Oct 24 '12 at 01:59
21

You could do this:

rack_test_session_wrapper = Capybara.current_session.driver
rack_test_session_wrapper.submit :post, your_path, nil
  • You can replace :post which whatever method you care about e.g. :put or :delete.
  • Replace your_path with the Rails path you want e.g. rack_test_session_wrapper.submit :delete, document_path(Document.last), nil would delete the last Document in my app.
Mike
  • 9,292
  • 5
  • 41
  • 60
  • 5
    I get `undefined method `submit' for #` - Capybara 2.9.0 – Jaco Pretorius Sep 28 '16 at 15:41
  • Dont use Capybara for making HTTP POST requests. Its designed to emulate a user using a web browser. The user doesnt form HTTP POST requests, the user clicks buttons and links and submits forms and such. Use RSpec Request specs for testing responses from POST, PUT, DELETE requests. Check out this blog post: http://matthewlehner.net/rails-api-testing-guidelines/ – Todd Apr 08 '17 at 21:28
  • In my case, the user actually _does_ submit an HTTP request from their command line, which returns some data, including a unique URL, which they then visit in their browser. The integration test I need would perform a GET request, receive data including a URL, and then follows the URL in the browser and ensures the displayed data in the browser matches the data returned by the GET request. The problem with blanket statements like, "The user doesn't..." is that they assume you know what everyone's users do. – jangosteve Mar 20 '19 at 19:05
  • @jangosteve Todd merely said that Capybara was designed to emulate a user using a browser. Your users do not use a browser, do not expect Capybara to be able mimic them. – Arnaud Meuret Oct 17 '19 at 08:40
12

If your driver doesn't have post (Poltergeist doesn't, for example), you can do this:

session = ActionDispatch::Integration::Session.new(Rails.application)
response = session.post("/mypath", my_params: "go_here")

But note that this request happens in a new session, so you will have to go through the response object to assert on it.

As has been stated elsewhere, in a Capybara test you typically want to do POSTs by submitting a form just like the user would. I used the above to test what happens to the user if a POST happens in another session (via WebSockets), so a form wouldn't cut it.

Docs:

Henrik N
  • 14,212
  • 3
  • 72
  • 116
  • 1
    This seems to be the most up-to-date response and works with Webkit driver as well (which doesn't have the `post` method) – jwadsack Jul 11 '17 at 19:09
  • This is beautiful - needed to post though the api, to see something happen in the user's browser via websockets. THANKS! – Luke Rohde Jul 11 '18 at 02:43
  • The response object returned by post is just the status code, you can access the response object with `session.response` afterwards, which also has the #body method. Together with this piece of information, POSTing outside Capybara works perfectly! – oliverguenther Jan 11 '19 at 10:27
9

Capybara's visit only does GET requests. This is by design.

For a user to perform a POST, he must click a button or submit a form. There is no other way of doing this with a browser.

The correct way to test this behaviour would be:

visit "project/:id/edit" # This will only GET
attach_file "photo", File.open('cute_photo.jpg')
click_button 'Upload' # This will POST

If you want to test an API, I recommend using spec/request instead of cucumber, but that's just me.

Ariejan
  • 10,313
  • 6
  • 38
  • 39
  • This sounds right. I have a situation where I want to make sure users with a specific permission cannot POST to a URL. So I guess what you're saying is Capybara+Cucumber should not be used for that. – Tony Dec 20 '10 at 18:12
  • 4
    It may be useful to test a POST. For example, if can use it to test your code is resilient against bots trying to post things where they should not be allowed to. – evedovelli Oct 31 '13 at 23:59
  • Its needed when testing 3rd party callbacks that make post requests. – Ian Vaughan Jan 22 '20 at 19:05
5

I know the answer has already been accepted, but I'd like to provide an updated answer. Here is a technique from Anthony Eden and Corey Haines which passes Rack::Test to Cucumber's World object:

Testing REST APIs with Cucumber and Rack::Test

With this technique, I was able to directly send post requests within step definitions. While writing the step definitions, it was extremely helpful to learn the Rack::Test API from it's own specs.

# feature
  Scenario: create resource from one time request
    Given I am an admin
    When I make an authenticated request for a new resource
    Then I am redirected  
    And I see the message "Resource successfully created" 

# step definitions using Rack::Test
When /^I make an authenticated request for a new resource$/ do
  post resources_path, :auth_token => @admin.authentication_token
  follow_redirect!
end

Then /^I am redirected$/ do
  last_response.should_not be_redirect
  last_request.env["HTTP_REFERER"].should include(resources_path)
end

Then /^I see the message "([^"]*)"$/ do |msg|
  last_response.body.should include(msg)
end
Sam
  • 1,071
  • 1
  • 21
  • 37
simeonwillbanks
  • 1,449
  • 16
  • 18
  • 2
    Although it's not required for your example, it's better to use `page.driver.post` so it's in the same context as other steps. – Zubin Dec 02 '11 at 07:53
3

Although, not an exact answer to the question, the best solution for me has been to use Capybara for specs that simulate user interaction (using visit), and Rack Test for test API like requests. They can be used together within the same test suite.

Adding the following to the spec helper gives access to get, post and other Rack test methods:

RSpec.configure do |config|
  config.include Rack::Test::Methods

You may need to put the Rack Test specs in a spec/requests folder.

B Seven
  • 39,557
  • 59
  • 208
  • 346
  • This seems like an unconventional use of Capybara, and while it may allow the OP to achieve desired functionality, it may induce an antipattern into the test suite. RSpec Request specs are much better suited to testing the behavior of an HTTP POST to an application. – Todd Dec 20 '17 at 15:14
  • It's not that simple, anymore, as Rack::Test::Methods requires the instance method `app` to return an instance of the Rack application. https://github.com/rack-test/rack-test/blob/master/lib/rack/test/methods.rb – XtraSimplicity Mar 11 '19 at 22:00
1

With an application using RSpec 3+, you would not want to make an HTTP POST request with Capybara. Capybara is for emulating user behavior, and accepting the JS behavior and page content that results. An end user doesnt form HTTP POST requests for resources in your application, a user clicks buttons, clicks ajax links, drags n drops elements, submits web forms, etc.

Check out this blog post on Capybara and other HTTP methods. The author makes the following claim:

Did you see any mention of methods like get, post or response? No? That’s because those don’t exist in Capybara. Let’s be very clear about this...Capybara is not a library suited to testing APIs. There you have it. Do not test APIs with Capybara. It wasn’t designed for it.

So, developing an API or not, if you have to make an explicit HTTP POST request, and it does not involve an HTML element and some sort of event (click, drag, select, focusout, whatever), then it shouldn't be tested with Capybara. If you can test the same feature by clicking some button, then do use Capybara.

What you likely want is RSpec Request specs. Here you can make post calls, and any other HTTP method as well, and assert expectations on the response. You can also mock n stub objects and methods to assert expectations in regards to side effects and other behaviors that happen in between your request and the response.

# spec located in spec/requests/project_file_upload_spec.rb
require "rails_helper"

RSpec.describe "Project File Upload", type: :request do

  let(:project) { create(:project) }
  let(:file)    { File.new(File.join(::Rails.root.to_s, 'path/to/file.ext')) } # can probably extract this to a helper...

  it "accepts a file uploaded to a Project resource" do

    post "project/#{project.id}/upload", upload_path: file

    expect(response).to be_success
    expect(project.file?).to eq(true)
    # expect(project.file).not_to eq(nil)
    expect(response).to render_template(:show)
  end

end
Todd
  • 2,235
  • 1
  • 24
  • 35
0

As others have said, there’s no direct way of doing a POST with Capybara because it’s all about browser interaction. For API testing, I’d very highly recommend the rspec_api_documentation gem.

Marnen Laibow-Koser
  • 5,182
  • 1
  • 23
  • 30