Testing
There are two ways to test a source, and which one you want depends on what the test is really about. If it is about your own code, mock the source. If it is about the integration, fake the HTTP and let the origin run.
Mock the source
Mock it on the model and the origin is never reached:
$mock = (new WeatherSource())->fill(['temperature' => 22.5, 'humidity' => 60]); $city->mockSource(WeatherSource::class, $mock); $city->source('weather')->temperature; // 22.5, no HTTP, no row
The mock only replaces the real source while running tests, and nothing is written to the sources table. This is the one to reach for when a controller, a Livewire component or a report happens to read a source along the way: you are not testing the integration, you are testing what the page does with a number.
Fake the HTTP
When what you want to test is the origin, the mapping or the failure handling, fake the client instead. An origin built on http() goes through Laravel's HTTP client, so the usual fake applies:
Http::fake(['api.example.com/*' => Http::response(['id' => 'L-1'], 200)]); $property->source('listing')->save(); Http::assertSent(fn ($request) => $request->method() === 'POST' && $request->url() === 'https://api.example.com/listings' && $request['price'] === 180000); $this->assertSame('L-1', SourceRecord::firstOrFail()->external_id);
What is worth asserting
Most of what breaks in an integration is caught by asserting these three.
The payload that leaves, which is the mapping, and the thing a provider will break for you:
$this->assertEquals([ 'title' => 'A flat with a view', 'price' => 180000, 'rooms' => 3, ], $sent);
That a refused write stores nothing:
Http::fake(['api.example.com/*' => Http::response(['error' => 'nope'], 422)]); $this->assertTrue($property->source('listing')->trySave()->failed()); $this->assertSame(0, SourceRecord::count());
That what was already stored survives that refusal, which is the invariant the whole package is built on:
$property->source('listing')->fetch(); // row with the live payload Http::fake(['api.example.com/*' => Http::response([], 500)]); try { $property->source('listing')->save(); } catch (OriginException $e) { // the job would retry here } $this->assertEquals($live, SourceRecord::firstOrFail()->attributes);
Protected mappers
The mapping lives in the Source, in methods that are protected on purpose. Reflect on them rather than making them public for the test:
protected function invokeMethod(object $target, string $method, mixed ...$arguments): mixed { $reflection = new ReflectionMethod($target, $method); $reflection->setAccessible(true); return $reflection->invoke($target, ...$arguments); } $this->assertSame(['rooms' => 3], $this->invokeMethod($source, 'buildFeatures'));
A helper on your base TestCase keeps that out of every test body.
Statuses and pointers
The pieces added for services that answer late are testable without any HTTP at all, because the status is just a column:
// a row the provider had not finished with $source->persist(OriginStatus::Processing); $this->assertSame(1, SourceRecord::processing()->count()); Http::fake(['api.example.com/*' => Http::response(['published' => true])]); $source->reconcile(); $this->assertSame(OriginStatus::Saved, $source->record()->status);
And a pipeline is two rows, which means you can assert the second origin received the first one's payload:
$scraped = ListingSource::make()->setExternalId('L-1')->fetch(); $article->source('translation')->useArgument('scraped', $scraped)->fetch(); $this->assertSame(['content' => 'the scraped body'], $receivedArguments['scraped']);
The package's own suite
Larasources runs 101 tests on Orchestra Testbench over SQLite, and the CI matrix covers every supported PHP and Laravel combination plus PHP nightly as an early warning. Worth knowing for one reason: a schema test that passes on SQLite is not a schema test. Indexes, foreign keys and nullable changes behave differently on MySQL, and the migrations are verified there separately.