›
byrcsc/laravel-cartographer · 1.x
Test committed diagrams and graph behavior without unnecessary file writes.
The package ships no test helpers, no fakes, and no assertions. It is an Artisan command that reads a database and writes a file, so it is tested the way you would test any other command.
There are three useful things to test in a consuming application, and the first is worth having in every project that commits a diagram.
cartographer:check is the drift test, so putting it in the suite is one
assertion on an exit code:
use Illuminate\Support\Facades\Artisan;
it('keeps the committed ERDs in sync with the schema', function (): void {
expect(Artisan::call('cartographer:check'))->toBe(0, Artisan::output());
});The test needs a migrated database, which any test case using RefreshDatabase
already has. It covers every diagram your config declares, the main one and
every group, and the failure message is the command's own
output: which files drifted, and a short diff for each.
This is the same check as the continuous integration job, moved into the suite. Pick one; running both is duplication. Prefer the CI job if your suite runs against a database that is not the one you generate from.
If you want the comparison in your own hands, --stdout returns the exact bytes
cartographer:erd would have written:
use Illuminate\Support\Facades\File;
it('keeps the committed ERD in sync with the schema', function (): void {
Artisan::call('cartographer:erd', ['--stdout' => true]);
expect(Artisan::output())->toBe(File::get(base_path('docs/erd.md')));
});For a set, compare each scope against its file:
it('keeps every committed ERD in sync', function (string $file, array $options): void {
Artisan::call('cartographer:erd', $options + ['--stdout' => true]);
expect(Artisan::output())->toBe(File::get(base_path($file)));
})->with([
['docs/erd.md', []],
['docs/erd/publishing.md', ['--group' => 'publishing']],
]);Pass option values as strings, exactly as they would arrive from the command line.
This is more code than the exit-code assertion and it drifts from your config,
because the dataset restates what groups already says. Reach for it when you
want to assert on a scope that is deliberately not committed.
--stdout also keeps generation tests off the filesystem, and suppresses the
warnings and the summary line so the output is only the diagram:
it('draws the publishing subsystem', function (): void {
$exitCode = Artisan::call('cartographer:erd', [
'--models' => 'Post',
'--depth' => '1',
'--columns' => 'keys',
'--format' => 'mmd',
'--stdout' => true,
]);
expect($exitCode)->toBe(0)
->and(Artisan::output())
->toStartWith("erDiagram\n")
->toContain('posts }o--|| users : "author"')
->toContain('bigint author_id FK');
});Assert on specific lines rather than the whole diagram. A test that pins every line fails on the next migration, which teaches the team to regenerate the fixture instead of reading the failure.
When you do want the whole thing pinned, use a snapshot. The package's own suite does exactly that for its renderer.
To test the file-writing path instead, point --output at a temporary path and
clean it up:
it('writes the diagram to the configured path', function (): void {
$path = sys_get_temp_dir().'/erd-'.bin2hex(random_bytes(6)).'/erd.md';
Artisan::call('cartographer:erd', ['--output' => $path]);
expect(File::exists($path))->toBeTrue();
File::deleteDirectory(dirname($path));
});Missing directories are created for you, so the temporary path does not need preparing.
Renderer tests need no database. Graph, GraphEntity, GraphEdge, and the
schema objects are plain readonly classes:
use Byrcsc\Cartographer\Graph;
use Byrcsc\Cartographer\GraphEdge;
use Byrcsc\Cartographer\GraphEntity;
use Byrcsc\Cartographer\MermaidRenderer;
use Byrcsc\Cartographer\SchemaColumn;
use Byrcsc\Cartographer\SchemaIndex;
use Byrcsc\Cartographer\TableSchema;
it('marks the primary key', function (): void {
$posts = new TableSchema(
name: 'posts',
columns: [new SchemaColumn('id', 'bigint', false)],
indexes: [new SchemaIndex('primary:id', ['id'], unique: true, primary: true)],
foreignKeys: [],
);
$graph = new Graph(
[new GraphEntity('posts', App\Models\Post::class, $posts)],
[],
);
expect((new MermaidRenderer)->render($graph, 'keys', 'mmd'))
->toContain('bigint id PK');
});Use this shape for edge cases that are awkward to create in a migration: hostile table names, exotic column types, composite keys.
RelationshipDetector needs no database either, because building a relation
runs no query:
use Byrcsc\Cartographer\RelationshipDetector;
it('points author at the users table through author_id', function (): void {
$relation = collect((new RelationshipDetector)->detect(App\Models\Post::class))
->firstWhere('name', 'author');
expect($relation->type)->toBe('belongs_to')
->and($relation->relatedModel)->toBe(App\Models\User::class)
->and($relation->foreignKey)->toBe('author_id');
});This is a compact way to assert that a foreign key rename reached every model that depends on it.
| Test | Needs a migrated database |
|---|---|
| The full command, any options | Yes |
SchemaReader | Yes |
GraphAssembler | Yes, it takes a schema |
RelationshipDetector | No |
MermaidRenderer | No |
ModelDiscovery | No |
AtomicFileWriter | No |
GraphAssembler needs a DatabaseSchema, which you can also construct by hand
if you would rather not migrate.