FEATURE: Allow asserting on requests in tests
Context
@jamesshore has created the concept of "Testing With Nullables".
I’ve figured out another way. A way that doesn’t use end-to-end tests, doesn’t use mocks, doesn’t ignore infrastructure, doesn’t require a rewrite. It’s something you can start doing today, and it gives you the speed, reliability, and maintainability of unit tests with the power of end-to-end tests.
A small snippet:
it("reads command-line argument, transform it with ROT-13, and writes result", () => {
const { output } = run({ args: [ "my input" ] });
assert.deepEqual(output.data, [ "zl vachg\n" ];
});
function run({ args = [] } = {}) {
const commandLine = CommandLine.createNull({ args });
const output = commandLine.trackOutput();
const app = new App(commandLine);
app.run();
return { output };
}
I've been experimenting with these techniques in various codebases and love the code that results.
Why
This style of assertions, whilst not adhering to James' full pattern, is a thin layer on top of what Webmock is already doing - @jamesshore calls it "Output Tracking"
Example
Let's say we're doing an API request to Cloudflare to get IP blocking rules.
Before
it "makes a get request to Cloudflare to get the rules" do
stub_request(:get, "www.cloudflare.com/api/v2/rules").to_return_json(body: [{...}])
subject.call
expect(WebMock).to have_requested(:get, "www.cloudflare.com/api/v2/rules").
with(query: {"ip" => "2.5.4.3"})
end
Strengths
- Leans into the Ruby and RSpec metaprogramming conventions
- Reads like English
Weaknesses
- Maintaining a
#have_requestedRSpec matcher means more code - Custom matchers means writing readable failure messages
- RSpec magic means that if the custom matcher fails it can be difficult to debug
After
it "makes a get request to Cloudflare to get the rules" do
stub_request(:get, "www.cloudflare.com/api/v2/rules").to_return_json(body: [{...}])
subject.call
expect(requests_made.count).to eq(1)
expect(requests_made.first.method).to eq(:get)
expect(requests_made.first.uri.host).to eq("www.cloudflare.com")
expect(requests_made.first.query).to eq(ip: "2.5.4.3")
end
Strengths
- Plain old Ruby
- Works with Minitest with no extra code or magic needed
- Allows asserting on order of requests
- Opens up options for extra helper methods
- Reduces coupling to Webmock - we could use another mocking library, implement
#requests_madeand we'd be good
Weaknesses
#requests_madeneeds a mixin to work- Can be more verbose (this can be mitigated by writing helper methods)
After - Cleaner
it "makes a get request to Cloudflare to get the rules" do
stub_request(:get, "www.cloudflare.com/api/v2/rules").to_return_json(body: [{...}])
subject.call
expect(cloudflare_requests_made.count).to eq(1)
expect(cloudflare_requests_made.first.method).to eq(:get)
expect(cloudflare_requests_made.first.query).to eq(ip: "2.5.4.3")
end
# private method for more readability and resilience
def cloudflare_requests_made
requests_made.select { |r| r.uri.host == "www.cloudflare.com" }
end
How
- Adjust the
HashCounterclass to store requests in an array (see comment for details) - Add an extra convenience
parsed_json_bodymethod ontoWebmock::RequestSignature - Add
#requests_madeonto the registry
Gah @bblimke all the tests are failing and I realised I need to do some documentation (I'll do that in a separate PR if that's OK?)
I'll come back to this and debug.
I'm getting a ton of unrelated CI failures locally - no idea what's going on here.
These tests are all failing on master branch:
Happy to pair with you some time @bblimke to make this all green again.
I'm going to leave this for now as I'm not best placed to do a deep dive into the codebase...
@johngallagher all tests in master branch are passing now, therefore feel free to marge master branch to this one.
I'm getting a ton of unrelated CI failures locally - no idea what's going on here.
These tests are all failing on
masterbranch:
Tests on master branch is passing now, if you would like to rebase your branch.