Skip to content

Testing

Amir Iranmanesh edited this page Jul 31, 2026 · 1 revision

Testing

Payvand is built to be testable without a merchant account: every gateway honours WithBaseURL, and one of them talks to nobody at all.

The virtual gateway

payvand.Virtual runs the whole purchase → callback → verify → refund cycle in memory.

gw, _ := payvand.New(payvand.Virtual, payvand.Config{})

purchase, _ := gw.Purchase(ctx, payvand.PurchaseRequest{
    Amount:      payvand.Toman(15_000),
    OrderID:     "1001",
    CallbackURL: "https://shop.example/callback",
})

// The payer "comes back": the redirect the gateway produced is a valid callback.
r := httptest.NewRequest(http.MethodGet, purchase.Redirect.String(), nil)
cb, _ := gw.ParseCallback(r)

verified, err := gw.Verify(ctx, cb.VerifyRequest(payvand.Toman(15_000)))

Failure paths are options, so your recovery code gets exercised too:

payvand.New(payvand.Virtual, cfg, virtual.WithDecline(true))       // payer declined
payvand.New(payvand.Virtual, cfg, virtual.WithFailingVerify(true)) // verification fails
virtual.WithRedirectURL("http://localhost:8080/fake-bank")         // your own fake bank page

Point your staging configuration at virtual and the rest of the application does not change.

A fake provider

For gateway specific behaviour, serve the provider's own payloads from an httptest.Server and redirect the gateway to it:

server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    switch r.URL.Path {
    case "/v1/request":
        _, _ = w.Write([]byte(`{"result":100,"trackId":3355}`))
    case "/v1/verify":
        _, _ = w.Write([]byte(`{"result":100,"amount":150000,"refNumber":"9988"}`))
    }
}))
defer server.Close()

gw, _ := payvand.New(payvand.Zibal, cfg, payvand.WithBaseURL(server.URL))

WithBaseURL replaces both the API host and the payment page host, so you can assert on the redirect URL as well.

What to cover in your own code

Case Why
a repeated callback payers refresh; the handler must be idempotent
ErrAlreadyVerified a refreshed page must not turn a paid order into a failure
a callback with an unknown token someone probing your endpoint
verification failure after a successful callback Succeeded is only a hint
ErrAmountMismatch the alert path is the one you never want to discover in production
a timeout during Verify the payment may still have settled; the recovery job must handle it

The package's own tests

Every gateway has a test package driven by internal/testutil, which starts a fake provider and fails the test on any unexpected path — a wrong endpoint becomes an immediate, readable failure.

make test        # go test -race ./...
make cover       # coverage summary
make lint        # gofmt check + go vet
make ci          # everything the pipeline runs

No network, no credentials, no build tags: go test ./... works on a fresh clone.

Sandboxes

Some providers offer a real sandbox, reachable with payvand.WithSandbox(true):

Provider What sandbox mode does
Zarinpal switches to sandbox.zarinpal.com
Zibal replaces the merchant id with the literal zibal
Pay.ir replaces the API key with test
IDPay sends the X-SANDBOX: 1 header
BitPay.ir uses the public demo API key

For the bank acquirers there is no public sandbox: you need a test terminal from the acquirer, and until then the virtual gateway plus a fake server is the honest substitute.

Next: Migration Guide · Extending

Clone this wiki locally