Testing

Never let a test reach Bullhorn: it is a live CRM with real candidates, and every test run would cost a login.

In Laravel: Http::fake()

The client sends through Laravel’s HTTP client, so Http::fake() and Http::preventStrayRequests() work. The login has four requests; answer them with FakeTransport::login() and describe only your API calls:

use Darvis\BullhornApi\Testing\FakeTransport;
use Illuminate\Http\Client\Request;
use Illuminate\Support\Facades\Http;

beforeEach(function () {
    Http::preventStrayRequests();

    config([
        'bullhorn.client_id' => 'id',
        'bullhorn.client_secret' => 'secret',
        'bullhorn.username' => 'user',
        'bullhorn.password' => 'password',
    ]);
});

test('a candidate is created in Bullhorn', function () {
    Http::fake(function (Request $request) {
        $login = FakeTransport::login($request->url());

        if ($login !== null) {
            return Http::response($login->body, $login->status, $login->headers);
        }

        return Http::response(['changedEntityId' => 42]);
    });

    // ... the code under test ...

    Http::assertSent(fn (Request $request) => $request->method() === 'PUT'
        && str_ends_with($request->url(), 'entity/Candidate'));
});

Http::fake() keeps the first callback that matches: register one callback per test, not one for the login and another for the API.

Mock the client

To test a screen without HTTP at all, swap the client in the container:

use Darvis\BullhornApi\Client;

$this->mock(Client::class)
    ->shouldReceive('entity')
    ->andReturn(['data' => ['id' => 1, 'firstName' => 'Anna']]);

Client is not final for this reason.

Without Laravel: FakeTransport

use Darvis\BullhornApi\Client;
use Darvis\BullhornApi\Config;
use Darvis\BullhornApi\Http\Response;
use Darvis\BullhornApi\Testing\FakeTransport;

$transport = (new FakeTransport)
    ->respond('entity/Candidate/1', ['data' => ['id' => 1]])
    ->respond('PUT', new Response(400, '{"errorMessageKey":"errors.cannotPersistEntity"}'));

$client = new Client(new Config('id', 'secret', 'user', 'password'), $transport);
  • respond($needle, ...$answers) answers every request whose METHOD url contains $needle. Several answers are given in turn, and the last one repeats. An array is sent as JSON with status 200; a Throwable is thrown as a transport without a response. The rule added last is tried first.
  • The login answers by itself, with the constants on FakeTransport (BH_REST_TOKEN, REFRESH_TOKEN, REST_URL …).
  • $transport->requests holds every request; sent($needle) filters them.

To test a session that expires, answer a call with new Response(401) and then with the result: the client renews and repeats once.