Browse documentationOpen

byrcsc/laravel-cartographer · 1.x

Testing.

Assert that the committed diagram matches the schema, test generation without writing files, and build graphs by hand for renderer tests.

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.

Assert the committed diagram is current

--stdout returns the exact bytes the command would have written, so a drift test is a string comparison:

use Illuminate\Support\Facades\Artisan;
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')));
});

The test needs a migrated database, which any test case using RefreshDatabase already has. It fails with a diff whenever a migration or a relation change lands without the diagram being regenerated.

This is the same check as the continuous integration job, moved into the suite. Pick one; running both is duplication.

For a set of diagrams, 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', ['--models' => 'Post', '--depth' => '1', '--columns' => 'keys']],
]);

Pass option values as strings, exactly as they would arrive from the command line.

Assert on the diagram without writing a file

--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.

Build a graph by hand

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 you cannot easily create in a migration: hostile table names, exotic column types, composite keys.

Testing relation metadata

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.

What needs a database

TestNeeds a migrated database
The full command, any optionsYes
SchemaReaderYes
GraphAssemblerYes — it takes a schema
RelationshipDetectorNo
MermaidRendererNo
ModelDiscoveryNo
AtomicFileWriterNo

GraphAssembler needs a DatabaseSchema, which you can also construct by hand if you would rather not migrate.