Skip to content

Florian418/probe

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

27 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Probe

Tests PHP 8.4+ License: MIT

A lightweight, modern PHP testing framework built from scratch.

Probe is a minimalist test runner for PHP 8.4+ that focuses on simplicity and readability. No bloat, no magic—just clean, understandable test code.


Features

  • Fluent assertions - Chainable ck() syntax for readable tests
  • Exception testing - Test expected failures with throws()
  • set_up/tear_down - Prepare and clean test environments
  • Standalone - No dependencies, just pure PHP
  • Self-contained - One class per test file, simple structure

Installation

Requirements

  • PHP 8.4+
  • Composer (optional)

Via Composer

composer require --dev flo/probe

Manual Installation

  1. Clone or download Probe
  2. Add to your project's tests/ directory
  3. Create bin/probe executable

Quick Start

1. Create a test file

Create tests/Example_test.php:

<?php
declare(strict_types=1);

namespace MyApp\Tests;

use Probe\Attributes\Test;
use Probe\Test_fixture;

class Example_test extends Test_fixture
{
    #[Test]
    public function addition_works()
    {
        $result = 2 + 2;
        ck($result)->eq(4);
    }

    #[Test]
    public function strings_can_be_checked()
    {
        ck("Hello World")->is_str()->contains("World");
    }
}

2. Run your tests

php bin/probe

Output:

> Example_test
OK - addition_works
OK - strings_can_be_checked
===
> Passed: 2
> Failed: 0
> Total: 2
===
\o/ ALL TESTS PASSED

Writing Tests

Test Structure

Every test class must:

  1. Extend Test_fixture
  2. Mark test methods with #[Test] attribute
  3. Use ck() for assertions
use Probe\Attributes\Test;
use Probe\Test_fixture;

class My_test extends Test_fixture
{
    #[Test]
    public function my_test_method()
    {
        // Your test code here
        ck($value)->eq($expected);
    }
}

Naming Conventions

  • Test files: *_test.php (e.g., User_test.php)
  • Test classes: *_test (e.g., User_test)
  • Test methods: Descriptive names (e.g., user_can_login, validates_email_format)

Assertions

Probe uses the ck() function for all assertions. Assertions are chainable for readability.

Type Checks

ck($value)->is_int();      // Value must be integer
ck($value)->is_float();    // Value must be float
ck($value)->is_str();      // Value must be string
ck($value)->is_bool();     // Value must be boolean
ck($value)->is_array();    // Value must be array
ck($value)->is_object();   // Value must be object
ck($value)->is_callable(); // Value must be callable

Instance Checks

ck($user)->is_instance_of(User::class);     // Must be instance of User
ck($request)->is_instance_of(Request::class); // Must be instance of Request

Equality

ck($actual)->eq($expected);    // Strict equality (===)
ck($actual)->ne($expected);    // Not equal (!==)

Numeric Comparisons

ck($value)->gt(10);    // Greater than (>)
ck($value)->ge(10);    // Greater or equal (>=)
ck($value)->lt(10);    // Less than (<)
ck($value)->le(10);    // Less or equal (<=)

String Assertions

ck($string)->contains("substring");         // String contains substring
ck($path)->starts_with("/api/");            // String starts with prefix
ck($email)->ends_with("@example.com");      // String ends with suffix
ck($email)->matches('/^[\w\-\.]+@([\w\-]+\.)+[\w\-]{2,4}$/'); // String matches regex
ck($string)->is_empty();                    // String is empty

Boolean and Special Values

ck($value)->is_true();     // Strictly TRUE (not truthy)
ck($value)->is_false();    // Strictly FALSE (not falsy)
ck($value)->is_null();     // Value is NULL
ck($value)->is_empty();    // Value is empty

Collections

ck($array)->has_count(3);                        // Array has exactly 3 elements
ck($config)->has_key('database');                // Array has key 'database'
ck($method)->in(['GET', 'POST', 'PUT', 'DELETE']); // Value is in array

Files

ck($path)->is_file();    // Path points to an existing file

Chaining

All assertions return $this, allowing you to chain multiple checks:

ck($age)->is_int()->gt(18)->le(65);
ck($email)->is_str()->contains("@")->ends_with(".com");
ck($path)->is_str()->starts_with("/api/")->matches('!^/api/v\d+/!');
ck($user)->is_object()->is_instance_of(User::class);

Testing Exceptions

Use throws() to test that code throws an exception:

#[Test]
public function division_by_zero_throws()
{
    $this->throws();  // Declare that an exception is expected

    $result = 10 / 0;  // This will throw DivisionByZeroError
}

#[Test]
public function invalid_user_throws()
{
    $this->throws();

    ck("")->is_int();  // This will throw Assertion_exception
}

Custom Exception Types

By default, throws() expects Assertion_exception. To expect a different exception:

#[Test]
public function throws_custom_exception()
{
    $this->throws(\InvalidArgumentException::class);

    throw new \InvalidArgumentException("Invalid input");
}

set_up and tear_down

Use set_up() and tear_down() to prepare and clean up your test environment.

set_up()

Called before each test in the class:

class Database_test extends Test_fixture
{
    private Database $db;

    protected function set_up(): void
    {
        // Create a fresh database for each test
        $this->db = new Database(':memory:');
        $this->db->migrate();
    }

    #[Test]
    public function can_insert_user()
    {
        $this->db->insert('users', ['name' => 'John']);
        ck($this->db->count('users'))->eq(1);
    }

    #[Test]
    public function can_delete_user()
    {
        $this->db->insert('users', ['name' => 'Jane']);
        $this->db->delete('users', 1);
        ck($this->db->count('users'))->eq(0);
    }
}

tear_down()

Called after each test (even if the test fails):

protected function tear_down(): void
{
    // Clean up resources
    $this->db->close();
    unset($this->db);
}

Execution Order

For each test method, Probe executes:

  1. reset() - Reset exception expectations
  2. set_up() - Prepare test environment
  3. Test method - Your actual test
  4. tear_down() - Clean up (always runs, even on failure)

Notes

  • set_up() and tear_down() are optional
  • Each test gets a fresh environment (set_up is called before EACH test)
  • If set_up() throws an exception, the test fails and tear_down() still runs
  • If tear_down() throws an exception, a warning is shown but the test result is preserved

Running Tests

Basic Usage

Run all tests in the tests/ directory:

php bin/probe

AI Mode

When running tests from an AI agent (Claude Code, etc.), use --ai to get a compact, token-efficient output:

php bin/probe --ai

All tests pass:

OK 42/42

Some tests fail:

FAIL 2/42
NOK Checker_test::eq_strict > Expected 5 === "5" [Checker_test.php:34]
NOK User_test::validates_email > Expected string to contain "@" [User_test.php:18]

No headers, no per-test OK lines — only what matters.

Test Discovery

Probe automatically discovers:

  • All .php files in tests/ directory (recursively)
  • All classes with methods marked with #[Test]
  • Executes tests and reports results

Exit Codes

  • 0 - All tests passed
  • 1 - One or more tests failed

Use this in CI/CD pipelines:

php bin/probe || exit 1

Example: Complete Test Class

<?php
declare(strict_types=1);

namespace MyApp\Tests;

use Probe\Attributes\Test;
use Probe\Test_fixture;
use MyApp\User;

class User_test extends Test_fixture
{
    private User $user;

    protected function set_up(): void
    {
        $this->user = new User('john@example.com', 'password123');
    }

    #[Test]
    public function user_has_email()
    {
        ck($this->user->email)->eq('john@example.com');
    }

    #[Test]
    public function user_can_change_password()
    {
        $this->user->changePassword('newpass456');
        ck($this->user->verifyPassword('newpass456'))->is_true();
    }

    #[Test]
    public function empty_email_throws()
    {
        $this->throws(\InvalidArgumentException::class);
        new User('', 'password');
    }

    #[Test]
    public function weak_password_fails()
    {
        $this->throws();
        $this->user->changePassword('123');
    }

    protected function tear_down(): void
    {
        unset($this->user);
    }
}

Output Format

Successful Test Run

> User_test
OK - user_has_email
OK - user_can_change_password
OK - empty_email_throws
OK - weak_password_fails
===
> Passed: 4
> Failed: 0
> Total: 4
===
\o/ ALL TESTS PASSED

Failed Test Run

> User_test
OK - user_has_email
NOK - user_can_change_password
OK - empty_email_throws
===
> Passed: 2
> Failed: 1
> Total: 3
===
> Tests échoués

User_test::user_can_change_password
> Expected TRUE, got FALSE
[42] - /path/to/User.php
===
X_X SOME TESTS FAILED

What's in v0.3.0

  • Auto-discovery of test files
  • #[Test] attribute
  • Fluent assertions with ck()
  • Exception testing with throws()
  • set_up() / tear_down() lifecycle
  • Extended type checks (float, object, callable, is_instance_of)
  • String assertions (starts_with, ends_with, matches)
  • Array assertions (has_key, in)
  • AI mode (--ai) — compact, token-efficient output for AI agents

Ideas

  • Data providers — run the same test logic with multiple datasets
  • Code coverage
  • Custom assertion failure messages

Philosophy

Probe is built with these principles:

  1. Simplicity - Easy to understand, no magic
  2. Readability - Tests should read like documentation
  3. Zero config - Works out of the box
  4. Modern PHP - Leverage modern PHP features
  5. Educational - Learn by reading the source code

License

MIT — see LICENSE.


Acknowledgments

Inspired by:

  • PHPUnit - The standard PHP testing framework
  • Pest - Modern, elegant testing
  • xUnit - The foundation of test frameworks

Built with ❤️ by flo418 as a learning journey into test framework design.


Happy Testing o/

About

php, testing, test-framework

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages