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

Kahlan Laravel Package

crysalead/kahlan

Kahlan is a BDD testing framework for PHP inspired by RSpec and Jasmine. It offers a readable describe/it syntax, built-in mocks and stubs, and code coverage support, making expressive unit testing simple to set up for PHP projects.

Deep Wiki
Context7

Getting Started

Kahlan is a BDD-style PHP test framework built around describe() / it() closures and a $this-based spec scope. It is not a PHPUnit drop-in; its distinguishing feature is monkey patching — replacing methods, static methods, final classes, and even built-in functions like time() or file_get_contents() in-place at runtime.

Minimal setup

composer require --dev crysalead/kahlan
vendor/bin/kahlan --init        # scaffolds spec/ and kahlan-config.php (if supported by your version)

If --init is unavailable, create spec/ manually and run:

vendor/bin/kahlan --spec=spec --src=src

First spec — put this in spec/CalculatorSpec.php:

<?php

use App\Calculator;

describe('Calculator', function () {

    beforeEach(function () {
        $this->calculator = new Calculator();
    });

    it('adds two integers', function () {
        expect($this->calculator->add(2, 3))->toBe(5);
    });

    it('rejects non-numeric input', function () {
        expect(function () {
            $this->calculator->add('a', 3);
        })->toThrow(new InvalidArgumentException());
    });
});

Where to look first

  1. The $this scope inside beforeEach / it — this is how state is shared between specs.
  2. allow() and expect(...)->toReceive() — Kahlan's mocking surface.
  3. The monkey-patching docs for static methods and functions, which is where Kahlan earns its keep over PHPUnit.
  4. kahlan-config.php — filters, coverage reporters, and custom CLI behavior.

Implementation Patterns

Subject + lifecycle hooks

Use beforeEach to build the subject and $this->subject as a convention. Hooks nest: parent beforeEach runs before child beforeEach, and afterEach runs child-first.

describe('UserService', function () {

    beforeEach(function () {
        $this->repo = Double::instance(['implements' => UserRepository::class]);
        $this->subject = new UserService($this->repo);
    });

    describe('::register()', function () {
        beforeEach(function () {
            $this->payload = ['email' => '[email protected]'];
        });

        it('persists the user', function () {
            expect($this->repo)->toReceive('save')->once();

            $this->subject->register($this->payload);
        });
    });
});

Stubs vs. expectations

  • allow($obj)->toReceive('m')->andReturn($x) — stub, no assertion.
  • expect($obj)->toReceive('m')->once()->with($arg) — assertion on invocation.
allow($logger)->toReceive('info')->andReturn(null);
// ...
expect($logger)->toReceive('info')->once()->with('user saved');

Counters include ->once(), ->never(), ->times(3), and ->ordered() when call order matters.

Monkey patching functions and statics

it('uses a frozen clock', function () {
    allow('time')->toBe(1_700_000_000);

    expect((new Post())->createdAt())->toBe(1_700_000_000);
});

it('short-circuits a static dependency', function () {
    allow(\App\Clock::class)->toReceive('now')->andReturn(new DateTimeImmutable('2024-01-01'));

    expect((new Report())->generatedAt())
        ->toEqual(new DateTimeImmutable('2024-01-01'));
});

This is the main reason teams pick Kahlan: no runkit, uopz, or refactoring a final class just to test it.

Doubles for interfaces and abstract collaborators

$double = Double::instance([
    'implements' => LoggerInterface::class,
    'extends'    => AbstractNotifier::class,
]);

$double = Double::instance(['methods' => ['__construct']]); // when you need the real constructor

Prefer Double for interface-heavy design, and allow($instance) when you need to patch a concrete/final implementation.

Data-driven specs

Kahlan has no @dataProvider; generate specs in a loop:

foreach ([[1, 1, 2], [2, 3, 5]] as [$a, $b, $expected]) {
    it("adds {$a} + {$b}", function () use ($a, $b, $expected) {
        expect((new Calculator())->add($a, $b))->toBe($expected);
    });
}

Configuration and CI

kahlan-config.php can return an options array or register filters against the runner. Typical CI invocation:

vendor/bin/kahlan --spec=spec --src=src --reporter=dot --no-colors --coverage=4

Composer script:

"scripts": {
    "test": "kahlan --spec=spec --src=src",
    "test:coverage": "kahlan --spec=spec --src=src --coverage=4"
}

For coverage filters, use Kahlan\Filter\Filters::apply($this, 'coverage', ...) in kahlan-config.php to plug in the Xdebug driver and an HTML/Clover reporter.

Custom matchers

Implement a matcher class against Kahlan's matcher contract and register it, then call it like a built-in (expect($x)->toBeMyThing()). Wrap domain-specific assertions (money, UUIDs, DTOs) here instead of repeating compound expectations in every spec.

Gotchas and Tips

  • Opcache breaks monkey patching. Kahlan rewrites source through a stream wrapper; cached opcodes bypass it. Set opcache.enable_cli=0 in the test environment.
  • Classes must not be loaded before patching. If a class was required in a bootstrap or by another test, allow('Fqcn') will silently have no effect. Keep patched classes out of early bootstraps, or use Kahlan\box() / Monkey::patch() deliberately.
  • Patches are per-spec, except in beforeAll. allow() is restored after each it, but a patch installed in beforeAll leaks across every spec in that describe. Prefer beforeEach unless you explicitly want block-level state.
  • toEqual vs toBe. toBe is ===; toEqual is == (structural for arrays, property-based for objects). Use toBe for scalars and identity, toEqual for value objects.
  • toReceive is strict about arguments. ->with($arg) compares values; be explicit about object identity vs. equality, or the assertion fails for reasons unrelated to your code.
  • $this binding is closure-scoped. Spec callbacks are rebound to Kahlan's scope, but a closure created via Closure::bind, a static closure, or a callback passed into a third-party API may lose it. Hoist state into local variables ($calc = $this->calculator;) before handing closures to external code.
  • Static analysis friction. PHPStan/Psalm do not know $this is a Kahlan scope. Add @var annotations or use locals; some teams ship a small stub file.
  • Focus is a trap. fit / fdescribe silently skips the rest of the suite. Treat a green run with focused specs as a red flag, and grep for them in CI.
  • pending() and skip() do not fail the build. They are useful during TDD, easy to forget before release; report pending specs in the summary and review them.
  • Xdebug for coverage, PCOV if your version supports it. Coverage slows suites considerably; run it in a separate CI job and keep --src narrow so vendor code is not measured.
  • Version pinning matters. Kahlan 5.x targets modern PHP (8.1+); older PHP projects need Kahlan 4.x. Check the composer.json PHP constraint before upgrading either PHP or Kahlan.
  • Not a PHPUnit migration path. There is no TestCase, no setUp()/tearDown(), no PHPUnit assertions, and no native data providers. Porting a large PHPUnit suite is a rewrite; a common pattern is Kahlan for new BDD-style specs alongside an existing PHPUnit suite, sharing the same vendor/autoload.php.
  • Debugging failed patches: confirm the class is not loaded (class_exists($fqcn, false)), run with --reporter=verbose, and isolate the offending spec with --grep or a focused it before assuming the patch API is wrong.
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.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings