Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Novaunit Laravel Package

quotevelocity/novaunit

NovaUnit brings a PHPUnit-friendly testing layer to Laravel Nova, letting you unit test resources, fields, actions, filters, lenses, and metrics without a browser. Write expressive assertions for Nova components right inside your existing test suite.

View on GitHub
Deep Wiki
Context7

Getting Started

Install it as a dev dependency, pinned to an exact version.

composer require --dev quotevelocity/novaunit:1.x.y

No caret. The package works by reaching into Nova's internals, which are explicitly outside Nova's backwards-compatibility promise. A floating constraint means a Nova patch bump can silently change how your test harness fails.

Before you run that command, confirm two things:

  1. The canonical Composer name. The description says quotevelocity/novaunit, the repo owner is qvelocity. Verify with composer show -a quotevelocity/novaunit (or a Packagist lookup) rather than guessing — a wrong namespace either fails loudly at install or, worse, pulls a fork you didn't intend.
  2. Nova credentials on every runner. Your local machine is authenticated against Nova's credential-gated repository; your CI is a separate machine. Sort this out now, not after you've written 40 tests that only pass locally.

Where to look first: the package's tests/ directory, not the README. At nine stars with a single maintainer, the test suite is the documentation. It shows the real assertion method names, what the base test case does during setup, and which Nova internals get touched. Skim it once before writing anything — the fluent helpers are the kind of API where guessing a method name costs you ten minutes per guess.

Create a dedicated suite and a one-file base case.

<!-- phpunit.xml -->
<testsuite name="Nova">
    <directory>tests/Nova</directory>
</testsuite>
// tests/Nova/TestCase.php
namespace Tests\Nova;

// Confirm the exact base class path in the package source.
abstract class TestCase extends \QuoteVelocity\NovaUnit\TestCase
{
    use \Tests\CreatesApplication; // only if you need the app container
}

That file is your escape hatch. If you ever drop the package, you rewrite one parent class instead of 200 test files.

First use case, in this order:

  1. A resource smoke test. Assert one of your custom fields is registered on a resource. It's the cheapest possible proof the harness boots at all — if this fails, the problem is installation, not your code.
  2. Then a real target. Pick the Action with the highest blast radius: authorization logic plus a destructive side effect. That single test proves the harness can exercise both permission checks and database effects, which is the entire reason you adopted it.

Run it isolated as you iterate:

php artisan test --filter=RefundOrderTest --testsuite=Nova

Implementation Patterns

Mirror Nova's taxonomy on disk. One test class per component, grouped by primitive:

tests/Nova/
  Resources/OrderResourceTest.php
  Fields/StatusSelectTest.php
  Actions/RefundOrderTest.php
  Filters/OverdueFilterTest.php
  Lenses/RecentOrdersLensTest.php
  Metrics/RevenueMetricTest.php
  Cards/HealthCardTest.php

This isn't cosmetic — it lets you set different coverage policies per primitive later ("every destructive Action is non-negotiable, Cards are best-effort") and lets you run a single directory when debugging.

Actions: write three tests, not one. They fail for different reasons, and you want the failure message to tell you which.

public function test_refund_denied_without_permission(): void
{
    $user = User::factory()->create();
    $this->assertFalse((new RefundOrder)->authorizedToRun(
        NovaRequest::create('/', 'POST'), collect([$order = $this->order()])
    ));
}

public function test_refund_returns_success_response(): void
{
    $response = (new RefundOrder)->handle(
        NovaRequest::create('/', 'POST', ['reason' => 'duplicate']),
        collect([$order = $this->order()])
    );

    $this->assertSame('Order refunded', $response->message);
}

public function test_refund_mutates_order_state(): void
{
    // No mocking of the model here — the mutation is the thing under test.
    (new RefundOrder)->handle($request, collect([$order]));
    $this->assertSame('refunded', $order->fresh()->status);
}

The authorization test is the only one that prevents an incident. Don't let it get merged into the handler test "for brevity."

Fields: assert on your logic, never on Nova's. The valuable questions are: does the field resolve its options from the right source, does it serialize the correct meta, does a reactive field emit the dependency Nova needs to re-render? Skip anything that just confirms Nova registered a field you declared — that's Nova's test to own.

public function test_status_field_lists_valid_transitions_only(): void
{
    $field = StatusSelect::make('Status')->setResource($this->paidOrder());

    // Verify the exact helper name against the package source before relying on it.
    $this->assertFieldHasOptions($field, ['paid', 'refunded']);
}

Filters: use a real database, small fixtures. A filter's query logic is the code; mocking the query builder tests your mock. Seed three or four records in setUp, apply the filter through the package's request helper, assert on returned IDs.

protected function setUp(): void
{
    parent::setUp();
    $this->seedOverdueFixtures(); // 4 records: 2 overdue, 2 not
}

public function test_overdue_filter_returns_only_overdue_orders(): void
{
    $query = (new OverdueFilter)->apply(NovaRequest::create('/'), Order::query());
    $this->assertEqualsCanonicalizing($this->overdueIds, $query->pluck('id')->all());
}

Metrics, Cards, Lenses: best effort. Call calculate() against a known fixture set and assert the number/array. Lenses are worth one test on query() returning the right scope. Don't chase render-detail coverage — the assertion surface is thin and the maintenance cost is not.

Keep the database opt-in. Don't apply RefreshDatabase at the base-class level. Field, card, and most action-response tests are pure in-memory assertions; they should stay in milliseconds. If your Nova suite migrates on every test, contributors will start skipping it, and the whole value proposition dies.

Pest teams: prove interop with one test before standardizing. Pest runs on PHPUnit so it usually works, but the package doesn't advertise first-class Pest ergonomics. Try:

// tests/Pest.php
uses(Tests\Nova\TestCase::class)->in('Nova');

If that's clean, great. If not, keep Nova tests in a clearly bounded PHPUnit-only directory rather than running two idioms side by side and making contributors guess which to use.

Retrofit opportunistically, not as a project. Next time you're already editing an HTTP-level Nova test, rewrite that one into a component test — it gets faster and more precise. Never schedule a big-bang conversion; your existing feature tests remain a valid safety net.

Split CI by suite. The Nova suite should be the fastest thing in your pipeline and should run on every PR. That's the point: a quality gate cheap enough to run constantly, on the code with the highest blast radius.

Gotchas and Tips

The harness and the thing it tests break together. This is the structural risk. On a Nova upgrade, your safety net is most likely to fail at exactly the moment you need it. Put NovaUnit in the upgrade runbook as an explicit line item — "upgrade Nova → upgrade NovaUnit → repair component tests → run suite" — and expect repair work. Check whether upstream has a compatible release before you sink a day into fixing tests against internals that have moved.

Pin exact, and write down your fork trigger now. Zero dependents and one maintainer means nobody is obligated to fix a break. Decide today what would make you fork: e.g. "two blocking issues open for 30 days" or "no release within N weeks of a Nova major." Forking is viable — MIT, dev-only, no distribution — but only if the trigger is written down somewhere other than someone's head.

Keep your base class thin. Only app-wide setup belongs in tests/Nova/TestCase.php. Don't subclass package internals below the base case. When the swap happens, you change one extends and every test still compiles.

Don't assert Nova's behavior. Field registration, navigation wiring, route protection, authorization middleware, full request lifecycles — none of that is your coverage budget. NovaUnit tests component behavior in isolation. If you report to stakeholders that this replaces end-to-end coverage, the first integration bug that slips through will cost you the suite's credibility.

Debugging a failing fluent assertion: dump() the resolved component object before the assertion. The helpers obscure what they're actually inspecting, and nine times out of ten the problem is a component built with the wrong resource or missing setup, not a wrong assertion. If it passes locally and fails in CI, suspect Nova bootstrapping — service provider registration or a config cache — before you suspect the assertion itself.

Watch the release date. Verify the version you install actually exists on Packagist and check its composer.json constraints for the supported Nova/PHP/PHPUnit matrix. Docs lag on packages at this adoption level; the manifest doesn't.

Add your own stubs if the package doesn't ship any. A php artisan make:nova-test command (or a hand-rolled stub in stubs/) that scaffolds the base class and the three-test Action shape pays for itself within a week, and it's the cheapest way to encode your conventions when the package's docs won't.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment