›
›
›
  1. docs
  2. ›
  3. byrcsc/laravel-cartographer
1.x
Browse documentationOpenClose

Getting started

  • Introduction
  • Installation and setup
  • Quick start

Core concepts

  • How a diagram is built
  • Model discovery
  • Relationship detection
  • Schema introspection
  • Scoping a diagram

Operations

  • Diagrams per subsystem
  • Exporting images
  • Themes and fonts
  • Renderer limits
  • Keeping the diagram current
  • Continuous integration

Reference

  • Configuration
  • Console commands
  • Diagram syntax
  • PHP API
  • Testing
  • Troubleshooting

Getting started

  • Introduction
  • Installation and setup
  • Quick start

Core concepts

  • How a diagram is built
  • Model discovery
  • Relationship detection
  • Schema introspection
  • Scoping a diagram

Operations

  • Diagrams per subsystem
  • Exporting images
  • Themes and fonts
  • Renderer limits
  • Keeping the diagram current
  • Continuous integration

Reference

  • Configuration
  • Console commands
  • Diagram syntax
  • PHP API
  • Testing
  • Troubleshooting

byrcsc/laravel-cartographer · 1.x

Testing.

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.

Assert the committed diagram is current

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.

Without the check command

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.

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 that are awkward to 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.

What to read next

  • Continuous integration for the same drift check as a build step.
  • PHP API for the constructors used above.
  • Console commands for option names and values.
PreviousPHP APINextTroubleshooting
View source

On this page

  1. Assert the committed diagram is current
  2. Without the check command
  3. Assert on the diagram without writing a file
  4. Build a graph by hand
  5. Testing relation metadata
  6. What needs a database
  7. What to read next