Skip to content

Testing Overview & MockHttpAdapter

@yildizpay/http-adapter ships a dedicated testing sub-path with purpose-built test doubles, spies, and no-op helpers. Import them without polluting your production bundle:

typescript
import {
  MockHttpAdapter,
  MockHttpClient,
  NoopInterceptor,
  SpyInterceptor,
  SpyObserver,
} from '@yildizpay/http-adapter/testing';

MockHttpAdapter

A full in-memory test double for HttpAdapter that implements HttpAdapterContract. Use it in unit tests to control exactly what adapter.send() returns — without making real HTTP calls.

Setting Up Responses

typescript
const adapter = new MockHttpAdapter();

// Always return this response
adapter.mockResolvedValue({ STATUS: 'SUCCESS', ORDER_ID: '123' });

// Always throw this error
adapter.mockRejectedValue(new ServiceUnavailableException(/* ... */));

// One-time responses consumed in FIFO order, with a fallback default
adapter
  .mockResolvedOnce({ STATUS: 'PENDING' })
  .mockResolvedOnce({ STATUS: 'SUCCESS' })
  .mockResolvedValue({ STATUS: 'UNKNOWN' });  // fallback after queue is exhausted

// Custom factory — receives the full Request object
adapter.mockImplementation((request) => ({
  STATUS: request.body?.type === 'REFUND' ? 'REFUNDED' : 'SUCCESS',
}));

Endpoint-Specific Mocking

Use onEndpoint() to scope responses and assertions to a single path. Endpoint-scoped responses take priority over global ones.

typescript
adapter.onEndpoint('/api/payments').mockResolvedValue({ STATUS: 'SUCCESS' });
adapter.onEndpoint('/api/refunds').mockRejectedValue(new NotFoundException(/* ... */));

// One-time queue per endpoint
adapter.onEndpoint('/api/payments')
  .mockResolvedOnce({ STATUS: 'PENDING' })
  .mockResolvedValue({ STATUS: 'SUCCESS' });  // fallback

Assertions

After running the code under test:

typescript
adapter.assertCalledTimes(2);
adapter.assertCalledWith('/api/payments', { method: HttpMethod.POST });
adapter.assertCalledWithBody(0, { AMOUNT: '100', CURRENCY: 'TRY' });
adapter.assertNthCalledWith(1, '/api/payments');
adapter.assertLastCalledWith('/api/refunds');
adapter.assertCallOrder('/api/payments', '/api/refunds');
adapter.assertNotCalled();

Convenience getters:

typescript
adapter.callCount;      // number
adapter.firstCall;      // Request | undefined
adapter.lastCall;       // Request | undefined
adapter.wasCalled();    // boolean
adapter.wasNotCalled(); // boolean

Endpoint scopes expose the same assertion API, scoped to their path:

typescript
const paymentsScope = adapter.onEndpoint('/api/payments');
paymentsScope.assertCalledTimes(1);
paymentsScope.assertCalledWith({ body: { MERCHANT_ID: 'M001' } });
paymentsScope.wasCalled(); // boolean

RequestMatcher — Partial Request Matching

All assertCalledWith variants accept an optional RequestMatcher for deep partial matching on method, body, headers, and queryParams. Only the fields you specify are checked; extra fields in the actual request are ignored.

typescript
adapter.assertCalledWith('/api/payments', {
  method: HttpMethod.POST,
  body: { AMOUNT: '100' },              // extra keys in actual body are ignored
  headers: { 'x-merchant-id': 'M001' },
});

Body matching uses deep partial equality: nested objects are matched partially, NaN is handled correctly via Object.is, Date instances are compared by value, and arrays are never confused with plain objects.

Strict Mode

Enable strict mode to fail fast whenever a call is made to an unregistered endpoint — even when a global default is configured. Useful for catching unexpected HTTP calls:

typescript
const adapter = new MockHttpAdapter({ strict: true });
adapter.onEndpoint('/api/payments').mockResolvedValue({ STATUS: 'SUCCESS' });

// This throws immediately — '/api/users' is not registered
await adapter.send(unregisteredRequest);

Reset

reset() clears all calls, queues, and default behaviours without invalidating existing onEndpoint() references:

typescript
beforeEach(() => adapter.reset());

Full Example

typescript
import { MockHttpAdapter } from '@yildizpay/http-adapter/testing';
import { HttpMethod, NotFoundException } from '@yildizpay/http-adapter';
import { PaymentService } from './payment.service';

describe('PaymentService', () => {
  let adapter: MockHttpAdapter;
  let service: PaymentService;

  beforeEach(() => {
    adapter = new MockHttpAdapter();
    service = new PaymentService(adapter);
  });

  afterEach(() => adapter.reset());

  it('creates a charge and returns the charge ID', async () => {
    adapter.onEndpoint('/v1/charges').mockResolvedValue({ id: 'ch_123', status: 'succeeded' });

    const result = await service.charge({ amount: 1000, currency: 'USD' });

    expect(result.chargeId).toBe('ch_123');
    adapter.assertCalledWith('/v1/charges', {
      method: HttpMethod.POST,
      body: { amount: 1000, currency: 'USD' },
    });
  });

  it('throws when the charge endpoint returns 404', async () => {
    adapter.onEndpoint('/v1/charges').mockRejectedValue(
      new NotFoundException('Charge not found', /* response */ null),
    );

    await expect(service.charge({ amount: 1000, currency: 'USD' }))
      .rejects.toThrow(NotFoundException);
  });
});

Released under the MIT License.