I was recently working with a client who has an application that accepts incoming webhooks from GitHub. We track receipt of these webhooks and display state information to the user.
We test this functionality with request specs, which provide unit-level guarantees. We also want to use a Capybara feature spec to cover the entire workflow, not just individual units in the system.
Previously, we’ve written about stubbing external services in tests. We pursue that strategy elsewhere for testing this application, but it isn’t a fit for this problem. In this situation, we need the webhooks triggered outside of the application to be received by the application after a user begins one task and before they proceed to the next step in the application workflow.
First, let’s add some code in to support responding to webhooks, using Faraday:
# spec/support/fake_github_webhook.rb
require "faraday"
class FakeGitHubWebhook
def initialize(fixture:, host:, path:, port:)
@fixture = fixture
@host = host
@path = path
@port = port
load_fixture
construct_connection
end
def send
connection.post do |request|
request.url path
request.headers = headers
request.body = JSON.generate(body)
end
end
private
attr_accessor(
:body,
:connection,
:fixture,
:headers,
:path,
:session
)
def construct_connection
@connection = Faraday.new(url: "http://#{@host}:#{@port}")
end
def fixture_path
"#{Rails.root}/spec/fixtures/github_webhooks/#{fixture}"
end
def load_fixture
fixture_json = JSON.parse(File.read(fixture_path))
@headers = fixture_json.fetch("headers")
@body = fixture_json.fetch("body")
end
end
This application needs to receive several different webhook calls. In support of that, this class will load a JSON-formatted fixture file to populate request headers and the request body.
To create the fixture, we can look at the webhooks configured against the
application and see example successful requests in GitHub. If your GitHub
repository has webhooks configured for it, you can see those by going to
Settings
then Webhooks & Services
in your repository. You can then see
Recent Deliveries
, which will look similar to this:
Click on one of the hash values and you’ll see headers and the request payload, like so:
Copy that data and make a JSON fixture file like the following, in
spec/fixtures/github_webhooks/ping.json
:
{
"headers": {
"Content-Type": "application/json",
"User-Agent": "GitHub-Hookshot/9a55c2d",
"X-GitHub-Delivery": "ae35f100-e900-11e4-8a9e-df298010fa27",
"X-GitHub-task": "ping"
},
"body": {
"zen": "Keep it logically awesome.",
"hook_id": 4648018,
"hook": {
"url": "https://api.github.com/repos/example-user/test-repository/hooks/4648018",
"test_url": "https://api.github.com/repos/example-user/test-repository/hooks/4648018/test",
"ping_url": "https://api.github.com/repos/example-user/test-repository/hooks/4648018/pings",
"id": 5555,
"name": "web",
"active": true,
"tasks": [
"*"
]
}
}
}
The repository name and GitHub user name are important in the sense that they
need to line up with our test expectations. We’re using example-user
and
test-repository
, respectively. You may see other information in the headers
and payload you want to sanitize.
If you’re hand-rolling these fixture files, validate them with a
JSON linter. I decided to make the process easy by installing jsonlint
locally, which let me test the fixture files like so:
% npm install jsonlint -g
% jsonlint spec/fixtures/github_webhooks/ping.json
Now, we’re ready to write a Capybara feature spec using FakeGitHubWebhook
:
require "rails_helper"
feature "User opens task", :js do
scenario "and can complete GitHub steps" do
user = create(:user, username: "example-user")
task = create(:task)
ping_verification = task.steps.first.verification
sign_in(user: user)
visit task_path(task)
click_on t("links.begin_task")
expect(page).to have_content(ping_verification.error_text)
fake_github_webhook("ping").send
visit current_url
expect(page).to have_content(ping_verification.success_text)
end
def fake_github_webhook(fixture)
FakeGitHubWebhook.new(
fixture: "#{fixture}.json",
host: Capybara.current_session.server.host,
path: "/github/callbacks",
port: Capybara.current_session.server.port,
)
end
end
This test is declared as a JavaScript test so Capybara will provide access
to a session backed by a running instance of the application at 127.0.0.1
instead of an unreachable one at www.example.com
. Similarly, we need to
provide the port because Capybara is going to run the test instance of the
application on a new port and our FakeGitHubWebhook
will need to reach it.
We setup our test and start walking through the application workflow. Then, we
can use our FakeGitHubWebhook
Faraday client to send in a webhook notification
to the application at the spot in the workflow where we expect to have it. Now,
we have an proper end-to-end test, including having the application receive
requests from outside.