webmock icon indicating copy to clipboard operation
webmock copied to clipboard

FEATURE: Allow asserting on requests in tests

Open johngallagher opened this issue 1 year ago • 4 comments

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_requested RSpec 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_made and we'd be good

Weaknesses

  • #requests_made needs 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 HashCounter class to store requests in an array (see comment for details)
  • Add an extra convenience parsed_json_body method onto Webmock::RequestSignature
  • Add #requests_made onto the registry

johngallagher avatar Sep 14 '24 19:09 johngallagher

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.

johngallagher avatar Sep 14 '24 19:09 johngallagher

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:

image

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 avatar Sep 15 '24 14:09 johngallagher

@johngallagher all tests in master branch are passing now, therefore feel free to marge master branch to this one.

bblimke avatar Sep 18 '24 23:09 bblimke

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:

Tests on master branch is passing now, if you would like to rebase your branch.

bblimke avatar Sep 30 '24 18:09 bblimke