webmock

package module
v0.0.0-...-a92c67f Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 17, 2026 License: BSD-3-Clause Imports: 9 Imported by: 0

README

go-ruby-webmock/webmock

webmock — go-ruby-webmock

Docs License Go Coverage

A pure-Go (no cgo) reimplementation of the deterministic core of Ruby's webmock gem — an HTTP request matcher, stub registry, and recorded-request historywithout any Ruby runtime.

It is the webmock engine for go-embedded-ruby, but a standalone, reusable module — a sibling of go-ruby-openbao and go-ruby-net-http.

What it is — and isn't. Everything the webmock gem does around the wire is deterministic and needs no interpreter, so it lives here as pure Go: deciding whether a request matches a stub (method, URI incl. query, headers, body — exact / regexp / hash / proc), picking the stubbed response (including sequential responses, to_raise, to_timeout), counting requests for assert_requested / have_requested, and refusing unstubbed traffic when the network is disabled. Intercepting the real transport is the host's job: rbgo swaps in its Net::HTTP and feeds every outgoing request through [Registry.Match]. This engine opens no socket of its own.

Features

Faithful port of the webmock gem's matching core:

  • Stub builderStubRequest(method, uri) (:any via ""/"ANY") and StubRequestRe(method, *regexp.Regexp) for a regexp URI, chained with .With(...), .ToReturn(...), .ToRaise(err), .ToTimeout().
  • .with(...) constraintsHeader/HeaderRe/Headers, Body/BodyRe/BodyForm (hash body decoded from form or JSON per Content-Type), Query/QueryValues (order-insensitive), and Block (a func(Request) bool predicate, the gem's with { |req| … }).
  • Faithful URI semantics — scheme-agnostic unless the stub gives a scheme, default-port normalization (:80/:443), root-path defaulting, and order-insensitive query matching. Header names match case-insensitively.
  • Responsesto_return(status:, body:, headers:) with sequential responses (the last repeats once exhausted), plus to_raise (*RaiseError, errors.Unwrap-able) and to_timeout (ErrTimeout). Zero status defaults to 200; a stub with no response answers an empty 200.
  • Registry & historyRegister/StubRequest, Match(request) (the most recently registered matching stub wins), a recorded Requests() history, and Reset().
  • AssertionsRequested(method, uri, …) / RequestedRe(…) counts and AssertRequested(method, uri, times, …) returning an *AssertionError, mirroring assert_requested / have_requested.
  • Net-connect policyDisableNetConnect() (the default) makes an unmatched request return a webmock-style diff (*NoStubError); AllowNetConnect() returns ErrNetConnectAllowed so the host performs the real request.
  • Package-level Defaultwebmock.StubRequest / Match / AssertRequested / DisableNetConnect mirror the global WebMock module.

CGO-free, dependency-free (stdlib only), 100% test coverage, gofmt + go vet clean, and green across the six 64-bit Go targets (amd64, arm64, riscv64, loong64, ppc64le, s390x — big-endian) plus js/wasm and wasip1/wasm.

Install

go get github.com/go-ruby-webmock/webmock

Usage

package main

import (
	"fmt"

	"github.com/go-ruby-webmock/webmock"
)

func main() {
	reg := webmock.NewRegistry() // net connections disabled by default

	reg.StubRequest("GET", "www.example.com").
		With(webmock.Header("Accept", "application/json")).
		ToReturn(webmock.StubResponse{Status: 200, Body: `{"ok":true}`})

	resp, err := reg.Match(webmock.Request{
		Method:  "GET",
		URI:     "http://www.example.com/",
		Headers: map[string][]string{"Accept": {"application/json"}},
	})
	if err != nil {
		// *webmock.NoStubError (diff), webmock.ErrTimeout,
		// *webmock.RaiseError, or webmock.ErrNetConnectAllowed.
		return
	}
	fmt.Println(resp.Status, resp.Body) // 200 {"ok":true}

	// Assert how many times it was hit.
	_ = reg.AssertRequested("GET", "www.example.com", 1,
		webmock.Header("Accept", "application/json"))
}
Sequential responses, raise, timeout
reg.StubRequest("GET", "flaky.test").
	ToReturn(webmock.StubResponse{Body: "first"}, webmock.StubResponse{Body: "second"}).
	ToRaise(errors.New("boom")) // then this; the last behaviour repeats

Value model

gem this package
stub_request(:get, "host") reg.StubRequest("GET", "host")
stub_request(:any, /re/) reg.StubRequestRe("", regexp.MustCompile("re"))
.with(headers:, body:, query:) { |r| … } .With(Header/Body/Query/Block(…))
.to_return(status:, body:, headers:) .ToReturn(StubResponse{Status, Body, Headers})
.to_return(a, b, …) (sequential) .ToReturn(a, b, …) (last repeats)
.to_raise(Err) / .to_timeout .ToRaise(err)*RaiseError / .ToTimeout()ErrTimeout
assert_requested(:get, uri, times: n) reg.AssertRequested("GET", uri, n, …)
WebMock.disable_net_connect! / allow_net_connect! reg.DisableNetConnect() / reg.AllowNetConnect()
unregistered-request error *NoStubError (webmock-style diff)
the intercepted Net::HTTP the host feeds Request into Registry.Match

Tests & coverage

The suite is deterministic and needs no network: it constructs Request values and asserts the Match outcome and recorded history directly. Every branch — no-match diff, sequential-response exhaustion, raise / timeout stubs, assertion-count mismatch, and the net-connect-disabled path — is exercised, so every lane (three host OSes, six arches under qemu, both wasm targets) holds coverage at 100%.

COVERPKG=$(go list ./... | paste -sd, -)
go test -race -coverpkg="$COVERPKG" -coverprofile=cover.out ./...
go tool cover -func=cover.out | tail -1   # 100.0%

WebAssembly

Being pure Go (CGO=0), this library also compiles to WebAssembly — both GOOS=js GOARCH=wasm (browser / Node.js) and GOOS=wasip1 GOARCH=wasm (WASI). CI builds both targets on every push, alongside the six 64-bit native/qemu arches.

GOOS=js     GOARCH=wasm go build ./...   # browser / Node
GOOS=wasip1 GOARCH=wasm go build ./...   # WASI (wasmtime, wasmer, wasmedge, …)

License

BSD-3-Clause — see LICENSE. Copyright the go-ruby-webmock/webmock authors.

Documentation

Overview

Package webmock is a pure-Go (CGO-free) reimplementation of the deterministic core of Ruby's webmock gem — an HTTP request matcher, stub registry, and recorded-request history — without any Ruby runtime.

webmock lets a test declare stubs ("when a GET goes to www.example.com with these headers and body, answer 200 with this body") and then matches live requests against them, recording every request so the test can assert how many times an endpoint was hit. Everything webmock does around the wire — deciding whether a request matches a stub, picking the stubbed response, counting requests, refusing unstubbed traffic when the network is disabled — is deterministic and needs no interpreter, so it lives here as pure Go. The interception of the real HTTP transport is the host's job: rbgo binds this engine to its Net::HTTP replacement and feeds every outgoing request through Registry.Match.

It is the webmock engine for [go-embedded-ruby](https://github.com/go-embedded-ruby/ruby), but a standalone, reusable module — a sibling of go-ruby-openbao/openbao and go-ruby-net-http/net-http.

Value model

A Request is the request under test (Method, URI, Headers, Body). A StubResponse is a canned answer (Status, Headers, Body). A Stub pairs a request matcher with an ordered list of behaviours (return a response, raise an error, time out), built fluently:

reg := webmock.NewRegistry()
reg.StubRequest("GET", "www.example.com").
	With(webmock.Header("Accept", "application/json")).
	ToReturn(webmock.StubResponse{Status: 200, Body: `{"ok":true}`})

resp, err := reg.Match(webmock.Request{
	Method:  "GET",
	URI:     "http://www.example.com/",
	Headers: map[string][]string{"Accept": {"application/json"}},
})
// resp.Status == 200; err == nil

When no stub matches, Registry.Match returns a *NoStubError carrying a webmock-style diff (unless net connections have been re-enabled with Registry.AllowNetConnect, in which case it returns ErrNetConnectAllowed so the host can perform the real request). Registry.Requested and Registry.AssertRequested mirror the gem's assert_requested / have_requested.

Index

Constants

This section is empty.

Variables

View Source
var Default = NewRegistry()

Default is the process-wide registry backing the package-level helpers, so a host can call StubRequest / Match / AssertRequested the way Ruby uses the global WebMock module. Tests that want isolation should use their own NewRegistry instead.

View Source
var ErrNetConnectAllowed = errors.New("webmock: no stub registered; real net connection allowed")

ErrNetConnectAllowed is returned by Registry.Match when no stub matches but real net connections have been enabled with Registry.AllowNetConnect. The host should perform the real request itself; this engine opens no socket.

View Source
var ErrTimeout = errors.New("webmock: stubbed request timed out")

ErrTimeout is returned by Registry.Match when the matched stub is a to_timeout stub. It mirrors the gem raising a timeout on the bound transport.

Functions

func AllowNetConnect

func AllowNetConnect()

AllowNetConnect enables passthrough of unstubbed requests on Default.

func AssertRequested

func AssertRequested(method, uri string, times int, opts ...Option) error

AssertRequested asserts a request count on Default.

func DisableNetConnect

func DisableNetConnect()

DisableNetConnect forbids unstubbed requests on Default.

func Requested

func Requested(method, uri string, opts ...Option) int

Requested counts matching recorded requests on Default.

func Reset

func Reset()

Reset clears stubs and history on Default.

Types

type AssertionError

type AssertionError struct {
	Desc     string
	Expected int
	Got      int
}

AssertionError reports a request-count assertion mismatch, mirroring the gem's assert_requested failure message.

func (*AssertionError) Error

func (e *AssertionError) Error() string

type NoStubError

type NoStubError struct {
	Request    Request
	Registered []*Stub
}

NoStubError is returned by Registry.Match when no registered stub matches a request and net connections are disabled. Its message reproduces webmock's "unregistered request" diff: the offending request, a suggested stub snippet, and the list of registered stubs.

func (*NoStubError) Error

func (e *NoStubError) Error() string

type Option

type Option func(*matcher)

Option is a .with(...) constraint applied to a stub or an assertion. Options mirror webmock's with(headers:, body:, query:) and block matchers.

func Block

func Block(fn func(Request) bool) Option

Block constrains the request with an arbitrary predicate, mirroring webmock's with { |req| ... } block matcher.

func Body

func Body(s string) Option

Body constrains the request body to an exact string.

func BodyForm

func BodyForm(fields map[string]string) Option

BodyForm constrains the request body to a hash of fields, decoded from the body as URL-encoded form data or JSON per the request's Content-Type.

func BodyRe

func BodyRe(re *regexp.Regexp) Option

BodyRe constrains the request body to match a regexp.

func Header(name, value string) Option

Header constrains a request header to an exact value (case-insensitive name).

func HeaderRe

func HeaderRe(name string, re *regexp.Regexp) Option

HeaderRe constrains a request header to match a regexp.

func Headers

func Headers(h map[string]string) Option

Headers constrains several request headers to exact values at once.

func Query

func Query(q map[string]string) Option

Query constrains the request query string to the given parameters, order-insensitively.

func QueryValues

func QueryValues(v url.Values) Option

QueryValues constrains the request query string to the given url.Values, order-insensitively (allowing repeated keys).

type RaiseError

type RaiseError struct {
	Err error
}

RaiseError is returned by Registry.Match when the matched stub is a to_raise stub. It wraps the exception the stub was told to raise, so errors.Is / errors.Unwrap reach the underlying error.

func (*RaiseError) Error

func (e *RaiseError) Error() string

func (*RaiseError) Unwrap

func (e *RaiseError) Unwrap() error

Unwrap exposes the raised exception to errors.Is / errors.As.

type Registry

type Registry struct {
	// contains filtered or unexported fields
}

Registry holds registered stubs, the recorded request history, and the net-connect policy. It is the deterministic engine a host drives: every intercepted request goes through Registry.Match. A Registry is safe for concurrent use.

func NewRegistry

func NewRegistry() *Registry

NewRegistry returns an empty registry with net connections disabled, matching webmock's default of refusing unstubbed requests.

func (*Registry) AllowNetConnect

func (r *Registry) AllowNetConnect()

AllowNetConnect re-enables passthrough of unstubbed requests, mirroring WebMock.allow_net_connect!.

func (*Registry) AssertRequested

func (r *Registry) AssertRequested(method, uri string, times int, opts ...Option) error

AssertRequested returns nil if method/uri was requested exactly times, or an *AssertionError otherwise, mirroring assert_requested(..., times:).

func (*Registry) DisableNetConnect

func (r *Registry) DisableNetConnect()

DisableNetConnect forbids unstubbed requests, mirroring WebMock.disable_net_connect! (the default).

func (*Registry) Match

func (r *Registry) Match(req Request) (StubResponse, error)

Match resolves a request against the registered stubs. On a match it records the request and returns the next stubbed outcome: a StubResponse, or one of ErrTimeout / a *RaiseError for to_timeout / to_raise stubs. When several stubs match, the most recently registered one wins (webmock semantics). With no match it returns ErrNetConnectAllowed if net connections are enabled, or a *NoStubError diff otherwise.

func (*Registry) Register

func (r *Registry) Register(s *Stub) *Stub

Register adds a pre-built stub and returns it for chaining.

func (*Registry) Requested

func (r *Registry) Requested(method, uri string, opts ...Option) int

Requested counts recorded requests matching method/uri and the given constraints, mirroring a_request(...).should have_been_made / the count behind assert_requested.

func (*Registry) RequestedRe

func (r *Registry) RequestedRe(method string, re *regexp.Regexp, opts ...Option) int

RequestedRe is like Registry.Requested but matches the URI by regexp.

func (*Registry) Requests

func (r *Registry) Requests() []Request

Requests returns a copy of the recorded request history.

func (*Registry) Reset

func (r *Registry) Reset()

Reset clears stubs and history, mirroring WebMock.reset!.

func (*Registry) StubRequest

func (r *Registry) StubRequest(method, uri string, opts ...Option) *Stub

StubRequest builds and registers a stub, mirroring stub_request(method, uri).

func (*Registry) StubRequestRe

func (r *Registry) StubRequestRe(method string, re *regexp.Regexp, opts ...Option) *Stub

StubRequestRe builds and registers a stub whose URI is matched by regexp.

type Request

type Request struct {
	Method  string
	URI     string
	Headers map[string][]string
	Body    string
}

Request is an outgoing HTTP request presented to the registry for matching. It is the value model a host (rbgo) fills in from its intercepted Net::HTTP request. Header names are matched case-insensitively, mirroring HTTP and the webmock gem.

type Stub

type Stub struct {
	// contains filtered or unexported fields
}

Stub is a registered request matcher paired with an ordered list of behaviours. Successive matches walk the list; once exhausted the last behaviour repeats, mirroring webmock's sequential to_return semantics.

func NewStub

func NewStub(method, uri string, opts ...Option) *Stub

NewStub builds an unregistered stub for method (":any" via "" or "ANY") and a string URI pattern, with optional .with(...) constraints. Register it on a Registry with Registry.Register, or use Registry.StubRequest to do both.

func NewStubRe

func NewStubRe(method string, re *regexp.Regexp, opts ...Option) *Stub

NewStubRe is like NewStub but matches the whole request URI against a regexp, mirroring stub_request(:any, /regex/).

func StubRequest

func StubRequest(method, uri string, opts ...Option) *Stub

StubRequest builds and registers a stub on Default.

func StubRequestRe

func StubRequestRe(method string, re *regexp.Regexp, opts ...Option) *Stub

StubRequestRe registers a regexp-URI stub on Default.

func (*Stub) ToRaise

func (s *Stub) ToRaise(err error) *Stub

ToRaise appends a behaviour that makes Registry.Match return a *RaiseError wrapping err, mirroring to_raise.

func (*Stub) ToReturn

func (s *Stub) ToReturn(resps ...StubResponse) *Stub

ToReturn appends one or more responses. Called with several responses (or chained) it builds a sequence returned on successive matches; called with none it appends a default empty 200. A zero Status defaults to 200.

func (*Stub) ToTimeout

func (s *Stub) ToTimeout() *Stub

ToTimeout appends a behaviour that makes Registry.Match return ErrTimeout, mirroring to_timeout.

func (*Stub) With

func (s *Stub) With(opts ...Option) *Stub

With appends further .with(...) constraints to the stub's matcher.

type StubResponse

type StubResponse struct {
	Status  int
	Headers map[string][]string
	Body    string
}

StubResponse is a canned response a stub returns when its matcher fires. It mirrors webmock's to_return(status:, body:, headers:).

func Match

func Match(req Request) (StubResponse, error)

Match resolves a request against Default.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL