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