Parcourir la source

Enhanced assertions, debugging helpers, and full stability

phpdevcommunity il y a 1 jour
commit
782a06e6df

+ 3 - 0
.gitignore

@@ -0,0 +1,3 @@
+/.idea/
+/vendor/
+composer.lock

+ 21 - 0
LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 DepoHub
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 225 - 0
README.md

@@ -0,0 +1,225 @@
+# PHP UniTester
+
+**UniTester** is a lightweight, zero-dependency unit testing library for PHP. It is designed for developers who want to write tests at the lowest level of PHP, without the overhead or complexity of massive frameworks.
+
+---
+## 🇬🇧 English Documentation
+
+### 💡 Philosophy
+
+- **Zero Dependency**: UniTester relies on no external libraries. Install it, and it works. No version conflicts, no bloated `vendor`.
+- **Simplicity**: No magic, no complex annotations. Just PHP code.
+- **Performance**: Built to be fast and to help you test your own code's performance.
+- **Clarity**: Explicit error messages that tell you exactly what went wrong (types, JSON content, etc.).
+
+### 📦 Installation
+
+```bash
+composer require depo/unitester
+```
+
+### 🚀 Usage
+
+1.  Create a `tests/` directory at your project root.
+2.  Create a test class extending `Depo\UniTester\TestCase`.
+3.  Implement the `execute()` method to list your tests.
+
+Example:
+
+```php
+<?php
+
+namespace Tests;
+
+use Depo\UniTester\TestCase;
+
+class MathTest extends TestCase
+{
+    protected function setUp(): void
+    {
+        // Executed before all tests in the class
+    }
+
+    protected function tearDown(): void
+    {
+        // Executed after all tests in the class
+    }
+
+    protected function execute(): void
+    {
+        $this->testAddition();
+        $this->testPerformance();
+    }
+
+    public function testAddition()
+    {
+        $this->assertEquals(4, 2 + 2, "Addition should be correct");
+    }
+
+    public function testPerformance()
+    {
+        $this->assertExecutionTimeLessThan(10, function() {
+            // Critical code
+        });
+    }
+}
+```
+
+Run tests:
+
+```bash
+php vendor/bin/unitester tests/
+```
+
+### 🛠 API Reference
+
+#### Basic Assertions
+All assertions accept an optional last `$message` parameter.
+
+- `assertEquals($expected, $actual)`: Loose equality (`==`).
+- `assertStrictEquals($expected, $actual)`: Strict equality (`===`).
+- `assertNotEquals($expected, $actual)`
+- `assertNotStrictEquals($expected, $actual)`
+- `assertTrue($condition)`
+- `assertFalse($condition)`
+- `assertNull($value)`
+- `assertNotNull($value)`
+- `assertEmpty($value)`
+- `assertNotEmpty($value)`
+- `assertInstanceOf($expectedClass, $object)`
+
+#### Array & String Assertions
+- `assertCount($expectedCount, $haystack)`: Checks size of array or Countable.
+- `assertArrayHasKey($key, $array)`: Checks for key existence.
+- `assertStringContains($haystack, $needle)`
+- `assertStringStartsWith($haystack, $needle)`
+- `assertStringEndsWith($haystack, $needle)`
+- `assertStringLength($string, $length)`
+
+#### Performance Assertions (Low Level)
+- `assertExecutionTimeLessThan(float $ms, callable $callback)`: Verifies callback runs under X ms.
+- `assertMemoryUsageLessThan(int $bytes, callable $callback)`: Verifies callback consumes less than X bytes.
+
+#### File System Assertions
+- `assertFileExists($path)`
+- `assertFileNotExists($path)`
+- `assertDirectoryExists($path)`
+- `assertIsReadable($path)`
+- `assertIsWritable($path)`
+
+#### Debugging Helpers
+- `$this->log(string $message)`: Prints a message to the console during tests.
+- `$this->dump($value)`: Prints formatted variable content.
+- `fail(string $message)`: Manually fails the test.
+
+---
+## 🇫🇷 Documentation en Français
+
+**UniTester** est une librairie de tests unitaires pour PHP, légère et sans aucune dépendance. Elle est conçue pour les développeurs qui souhaitent écrire des tests au plus près du langage, sans la complexité des gros frameworks.
+
+### 💡 Philosophie
+
+- **Zéro Dépendance** : UniTester ne dépend d'aucune librairie externe. Vous l'installez, et ça marche. Pas de conflits de versions, pas de `vendor` obèse.
+- **Simplicité** : Pas de magie, pas d'annotations complexes. Juste du code PHP.
+- **Performance** : Conçu pour être rapide et tester la performance de votre propre code.
+- **Clarté** : Des messages d'erreur explicites qui vous disent exactement ce qui ne va pas (types, contenu JSON, etc.).
+
+### 📦 Installation
+
+```bash
+composer require depo/unitester
+```
+
+### 🚀 Utilisation
+
+1.  Créez un dossier `tests/` à la racine de votre projet.
+2.  Créez une classe de test qui étend `Depo\UniTester\TestCase`.
+3.  Implémentez la méthode `execute()` pour lister vos tests.
+
+Exemple :
+
+```php
+<?php
+
+namespace Tests;
+
+use Depo\UniTester\TestCase;
+
+class MathTest extends TestCase
+{
+    protected function setUp(): void
+    {
+        // Exécuté avant l'ensemble des tests de la classe
+    }
+
+    protected function tearDown(): void
+    {
+        // Exécuté après l'ensemble des tests de la classe
+    }
+
+    protected function execute(): void
+    {
+        $this->testAddition();
+        $this->testPerformance();
+    }
+
+    public function testAddition()
+    {
+        $this->assertEquals(4, 2 + 2, "L'addition doit être correcte");
+    }
+
+    public function testPerformance()
+    {
+        $this->assertExecutionTimeLessThan(10, function() {
+            // Code critique
+        });
+    }
+}
+```
+
+Lancer les tests :
+
+```bash
+php vendor/bin/unitester tests/
+```
+
+### 🛠 API Reference
+
+#### Assertions de Base
+Toutes les assertions acceptent un dernier paramètre `$message` optionnel.
+
+- `assertEquals($expected, $actual)` : Égalité souple (`==`).
+- `assertStrictEquals($expected, $actual)` : Égalité stricte (`===`).
+- `assertNotEquals($expected, $actual)`
+- `assertNotStrictEquals($expected, $actual)`
+- `assertTrue($condition)`
+- `assertFalse($condition)`
+- `assertNull($value)`
+- `assertNotNull($value)`
+- `assertEmpty($value)`
+- `assertNotEmpty($value)`
+- `assertInstanceOf($expectedClass, $object)`
+
+#### Assertions sur les Tableaux et Chaînes
+- `assertCount($expectedCount, $haystack)` : Vérifie la taille d'un tableau ou Countable.
+- `assertArrayHasKey($key, $array)` : Vérifie la présence d'une clé.
+- `assertStringContains($haystack, $needle)`
+- `assertStringStartsWith($haystack, $needle)`
+- `assertStringEndsWith($haystack, $needle)`
+- `assertStringLength($string, $length)`
+
+#### Assertions de Performance (Low Level)
+- `assertExecutionTimeLessThan(float $ms, callable $callback)` : Vérifie que le callback s'exécute en moins de X ms.
+- `assertMemoryUsageLessThan(int $bytes, callable $callback)` : Vérifie que le callback consomme moins de X octets.
+
+#### Assertions Système de Fichiers
+- `assertFileExists($path)`
+- `assertFileNotExists($path)`
+- `assertDirectoryExists($path)`
+- `assertIsReadable($path)`
+- `assertIsWritable($path)`
+
+#### Helpers de Debugging
+- `$this->log(string $message)` : Affiche un message dans la console pendant les tests.
+- `$this->dump($value)` : Affiche le contenu formaté d'une variable.
+- `fail(string $message)` : Fait échouer le test manuellement.

+ 362 - 0
bin/test

@@ -0,0 +1,362 @@
+#!/usr/bin/php
+<?php
+
+use Depo\UniTester\Console\Output;
+use Depo\UniTester\Exception\AssertionFailureException;
+use Depo\UniTester\TestCase;
+use Depo\UniTester\TestExecutor;
+use Depo\UniTester\TestFinder;
+use Depo\UniTester\TestRunnerCli;
+use Test\Depo\UniTester\AssertionTest;
+use Test\Depo\UniTester\ComparisonTest;
+use Test\Depo\UniTester\ExceptionHandlingTest;
+use Test\Depo\UniTester\PerformanceFileTest;
+use function Depo\UniTester\assert_empty;
+use function Depo\UniTester\assert_equals;
+use function Depo\UniTester\assert_not_strict_equals;
+use function Depo\UniTester\assert_strict_equals;
+use function Depo\UniTester\assert_false;
+use function Depo\UniTester\assert_instanceof;
+use function Depo\UniTester\assert_negative_int;
+use function Depo\UniTester\assert_not_empty;
+use function Depo\UniTester\assert_not_equals;
+use function Depo\UniTester\assert_not_null;
+use function Depo\UniTester\assert_null;
+use function Depo\UniTester\assert_positive_int;
+use function Depo\UniTester\assert_similar;
+use function Depo\UniTester\assert_string_contains;
+use function Depo\UniTester\assert_string_ends_with;
+use function Depo\UniTester\assert_string_length;
+use function Depo\UniTester\assert_string_starts_with;
+use function Depo\UniTester\assert_true;
+use function Depo\UniTester\assert_count;
+use function Depo\UniTester\assert_array_has_key;
+use function Depo\UniTester\assert_execution_time_less_than;
+use function Depo\UniTester\assert_memory_usage_less_than;
+use function Depo\UniTester\assert_file_exists;
+use function Depo\UniTester\assert_file_not_exists;
+use function Depo\UniTester\assert_directory_exists;
+use function Depo\UniTester\assert_is_readable;
+use function Depo\UniTester\assert_is_writable;
+use function Depo\UniTester\fail;
+
+if (file_exists(dirname(__DIR__) . '/vendor/autoload.php')) {
+    $composer = require dirname(__DIR__) . '/vendor/autoload.php';
+} else {
+    die(
+        'You need to set up the project dependencies using the following commands:' . PHP_EOL .
+        'curl -sS https://getcomposer.org/installer | php' . PHP_EOL .
+        'php composer.phar install' . PHP_EOL
+    );
+}
+
+final class SelfTest
+{
+    private static array $listOfTestClasses = [AssertionTest::class, ComparisonTest::class, ExceptionHandlingTest::class, PerformanceFileTest::class];
+
+    public static function run(): void
+    {
+        try {
+            self::log('');
+            self::log('Running self tests...');
+            self::log('========================');
+            self::log('');
+
+            self::log('--- Starting Assertions Tests ---');
+            self::testAsserts();
+            self::log('--- ✅ Assertions Tests Passed ---');
+            self::log('');
+
+            self::log('--- Starting TestFinder Tests ---');
+            self::testFinder();
+            self::log('--- ✅ TestFinder Tests Passed ---');
+            self::log('');
+
+            self::log('--- Starting TestCase Tests ---');
+            self::testTestCase();
+            self::log('--- ✅ TestCase Tests Passed ---');
+            self::log('');
+
+            self::log('--- Starting TestExecutor Tests ---');
+            self::testTestExecutor();
+            self::log('--- ✅ TestExecutor Tests Passed ---');
+            self::log('');
+
+            self::log('--- Starting TestRunner Tests ---');
+            self::testTestRunner();
+            self::log('--- ✅ TestRunner Tests Passed ---');
+            self::log('');
+
+            self::log('✅ All tests passed successfully!');
+            exit(0);
+        } catch (RuntimeException $e) {
+            self::log(sprintf('❌ Critical error in %s: %s', get_class($e), $e->getMessage()), true);
+            self::log('Stack trace:', true);
+            self::log($e->getTraceAsString(), true);
+            exit(1);
+        }
+    }
+
+    public static function testAsserts(): void
+    {
+        // Test assertions
+
+        $hello = 'Hello';
+        try {
+            assert_strict_equals(5, 5);
+            assert_equals(5, '5');
+            assert_not_strict_equals(true, 1);
+            assert_not_equals(5, 6);
+            assert_similar($hello, $hello);
+            assert_true(true);
+            assert_false(false);
+            assert_null(null);
+            assert_not_null(5);
+            assert_not_null('');
+            assert_empty([]);
+            assert_empty(null);
+            assert_empty('');
+            assert_not_empty([1, 2, 3]);
+            assert_instanceof(StdClass::class, new stdClass());
+            assert_string_length($hello, 5);
+            assert_string_contains($hello, 'll');
+            assert_string_starts_with($hello, 'He');
+            assert_string_ends_with($hello, 'lo');
+            assert_positive_int(5);
+            assert_negative_int(-5);
+            assert_count(2, [1, 2]);
+            assert_array_has_key('id', ['id' => 1]);
+            assert_execution_time_less_than(1000, function () {
+                usleep(1000);
+            });
+            assert_memory_usage_less_than(1000000, function () {
+                $a = 'a';
+            });
+            
+            $tempFile = sys_get_temp_dir() . '/unitester_test_file';
+            file_put_contents($tempFile, 'content');
+            try {
+                assert_file_exists($tempFile);
+                assert_is_readable($tempFile);
+                assert_is_writable($tempFile);
+            } finally {
+                @unlink($tempFile);
+            }
+            assert_file_not_exists($tempFile);
+            assert_directory_exists(__DIR__);
+        } catch (Throwable $e) {
+            throw new RuntimeException($e->getMessage());
+        }
+
+        // Test exceptions
+        $throwMessage = 'Expected exception not thrown. : ' . AssertionFailureException::class;
+        try {
+            assert_strict_equals(true, 1);
+            throw new RuntimeException(sprintf('Line %d: %s', __LINE__, $throwMessage));
+        } catch (AssertionFailureException $e) {
+        }
+
+        try {
+            assert_equals(5, 6);
+            throw new RuntimeException(sprintf('Line %d: %s', __LINE__, $throwMessage));
+        } catch (AssertionFailureException $e) {
+        }
+
+        try {
+            assert_not_strict_equals(false, false);
+            throw new RuntimeException(sprintf('Line %d: %s', __LINE__, $throwMessage));
+        } catch (AssertionFailureException $e) {
+        }
+
+        try {
+            assert_not_equals(5, 5);
+            throw new RuntimeException(sprintf('Line %d: %s', __LINE__, $throwMessage));
+        } catch (AssertionFailureException $e) {
+        }
+
+        try {
+            assert_similar($hello, 'hello');
+            throw new RuntimeException(sprintf('Line %d: %s', __LINE__, $throwMessage));
+        } catch (AssertionFailureException $e) {
+        }
+
+        try {
+            assert_true(false);
+            throw new RuntimeException(sprintf('Line %d: %s', __LINE__, $throwMessage));
+        } catch (AssertionFailureException $e) {
+        }
+
+        try {
+            assert_false(true);
+            throw new RuntimeException(sprintf('Line %d: %s', __LINE__, $throwMessage));
+        } catch (AssertionFailureException $e) {
+        }
+
+        try {
+            assert_null('');
+            throw new RuntimeException(sprintf('Line %d: %s', __LINE__, $throwMessage));
+        } catch (AssertionFailureException $e) {
+        }
+
+        try {
+            assert_not_null(null);
+            throw new RuntimeException(sprintf('Line %d: %s', __LINE__, $throwMessage));
+        } catch (AssertionFailureException $e) {
+        }
+
+        try {
+            assert_empty(' ');
+            throw new RuntimeException(sprintf('Line %d: %s', __LINE__, $throwMessage));
+        } catch (AssertionFailureException $e) {
+        }
+
+        try {
+            assert_not_empty([]);
+            throw new RuntimeException(sprintf('Line %d: %s', __LINE__, $throwMessage));
+        } catch (AssertionFailureException $e) {
+        }
+
+        try {
+            assert_instanceof(self::class, new stdClass());
+            throw new RuntimeException(sprintf('Line %d: %s', __LINE__, $throwMessage));
+        } catch (AssertionFailureException $e) {
+        }
+
+        try {
+            assert_string_length($hello, 6);
+            throw new RuntimeException(sprintf('Line %d: %s', __LINE__, $throwMessage));
+        } catch (AssertionFailureException $e) {
+        }
+    }
+
+    public static function testFinder(): void
+    {
+        try {
+            $testFinder = new TestFinder(dirname(__DIR__) . '/tests');
+            $foundTests = $testFinder->find();
+
+            self::expected(count($foundTests) === 4, 'Expected 4 tests, got ' . count($foundTests));
+
+            foreach ($testFinder->find() as $test) {
+                self::expected(in_array($test, self::$listOfTestClasses), 'Expected ' . implode(', ', self::$listOfTestClasses) . ', got ' . is_string($test) ? $test : gettype($test));
+            }
+        } catch (Throwable $e) {
+            throw new RuntimeException($e->getMessage());
+        }
+    }
+
+    public static function testTestCase(): void
+    {
+        /**
+         * @var TestCase $test
+         */
+        foreach (self::$listOfTestClasses as $testClass) {
+            $test = new $testClass();
+            $test->run();
+
+            switch ($testClass) {
+                case ComparisonTest::class:
+                case AssertionTest::class:
+                    self::expected($test->getAssertions() === 2, 'Expected 2 assertions, got ' . $test->getAssertions());
+                    break;
+                case ExceptionHandlingTest::class:
+                    self::expected($test->getAssertions() === 1, 'Expected 1 assertions, got ' . $test->getAssertions());
+                    break;
+                case PerformanceFileTest::class:           
+                    self::expected($test->getAssertions() === 7, 'Expected 7 assertions, got ' . $test->getAssertions());
+                    break;
+                default:
+                    throw new RuntimeException('Unexpected test class ' . $testClass);
+            }
+        }
+    }
+
+    private static function testTestExecutor(): void
+    {
+        $output = new Output(function (string $message) {
+        });
+        $testExecutor = new TestExecutor(self::$listOfTestClasses, $output);
+        $code = $testExecutor->run();
+        self::expected($code === 0, 'Expected code 0, got ' . $code);
+
+        $wrongTestClass = new class extends TestCase {
+            protected function setUp(): void
+            {
+            }
+
+            protected function tearDown(): void
+            {
+            }
+
+            protected function execute(): void
+            {
+                $this->assertNotNull(null);
+            }
+        };
+        $testExecutor = new TestExecutor([$wrongTestClass], $output);
+        $code = $testExecutor->run();
+        self::expected($code === 1, 'Expected code 1, got ' . $code);
+    }
+
+    private static function testTestRunner(): void
+    {
+        $line = 0;
+        $output = new Output(static function (string $message) use (&$line) {
+            ++$line;
+            if ($line === 1) {
+                self::expected($message === 'Usage PHP UniTester : [options] [folder]', 'Expected "Usage PHP UniTester : [options] [folder]", got "' . $message . '"');
+            }
+        });
+
+        $code = TestRunnerCli::run($output, ['', '--help']);
+        self::expected($code === 0, 'Expected code 0, got ' . $code);
+
+
+        $line = 0;
+        $waitingLineMessage = sprintf('PHP UniTester version %s', TestRunnerCli::VERSION);
+        $output = new Output(static function (string $message) use (&$line, $waitingLineMessage) {
+            ++$line;
+            if ($line === 1) {
+                self::expected($message === $waitingLineMessage, sprintf('Expected "%s", got "%s"', $waitingLineMessage, $message));
+            }
+        });
+        $code = TestRunnerCli::run($output, ['', '--version']);
+        self::expected($code === 0, 'Expected code 0, got ' . $code);
+
+        $output = new Output(function (string $message) {
+        });
+        $code = TestRunnerCli::run($output, ['', '--not-exists-option']);
+        self::expected($code === 1, 'Expected code 1, got ' . $code);
+
+        $output = new Output(function (string $message) {
+        });
+        $code = TestRunnerCli::run($output, ['', 'argument1', 'argument2']);
+        self::expected($code === 1, 'Expected code 1, got ' . $code);
+
+        $output = new Output(function (string $message) {
+        });
+        $code = TestRunnerCli::run($output, ['', 'folder/not/exists']);
+        self::expected($code === 1, 'Expected code 1, got ' . $code);
+
+        $output = new Output(function (string $message) {
+        });
+        $code = TestRunnerCli::run($output, ['', dirname(__DIR__) . '/tests']);
+        self::expected($code === 0, 'Expected code 0, got ' . $code);
+    }
+
+    private static function expected($condition, string $errorMessage = null): void
+    {
+        if (!$condition) {
+            throw new RuntimeException($errorMessage);
+        }
+    }
+
+    private static function log(string $message, bool $isError = false): void
+    {
+        fwrite($isError ? STDERR : STDOUT, $message . PHP_EOL);
+    }
+}
+
+SelfTest::run();
+
+

+ 21 - 0
bin/unitester

@@ -0,0 +1,21 @@
+#!/usr/bin/php
+<?php
+
+use Depo\UniTester\Console\Output;
+use Depo\UniTester\TestRunnerCli;
+
+if (file_exists(dirname(__DIR__) . '/../../autoload.php')) {
+    $composer = require dirname(__DIR__) . '/../../autoload.php';
+} elseif (file_exists(dirname(__DIR__) . '/vendor/autoload.php')) {
+    $composer = require dirname(__DIR__) . '/vendor/autoload.php';
+} else {
+    die(
+        'You need to set up the project dependencies using the following commands:' . PHP_EOL .
+        'curl -sS https://getcomposer.org/installer | php' . PHP_EOL .
+        'php composer.phar install' . PHP_EOL
+    );
+}
+
+$exitCode = TestRunnerCli::run(new Output());
+exit($exitCode);
+

+ 29 - 0
composer.json

@@ -0,0 +1,29 @@
+{
+    "name": "depo/unitester",
+    "description": "PHP UniTester is a unit testing library for PHP that provides a straightforward interface for writing and executing tests. It focuses on a minimalist approach, allowing developers to create assertions and organize tests at the lowest level of PHP, without reliance on complex external libraries.",
+    "type": "library",
+    "autoload": {
+        "psr-4": {
+            "Depo\\UniTester\\": "src",
+            "Test\\Depo\\UniTester\\": "tests"
+        },
+        "files": [
+            "src/assert.php"
+        ]
+    },
+    "require": {
+        "php": ">=7.4",
+        "ext-mbstring": "*",
+        "psr/container": "^2.0"
+    },
+    "license": "MIT",
+    "authors": [
+        {
+            "name": "F. Michel",
+            "homepage": "https://www.depohub.org"
+        }
+    ],
+    "bin": [
+        "bin/unitester"
+    ]
+}

+ 88 - 0
src/Console/ArgParser.php

@@ -0,0 +1,88 @@
+<?php
+
+namespace Depo\UniTester\Console;
+
+use InvalidArgumentException;
+use function strlen;
+use function strncmp;
+
+final class ArgParser
+{
+    private array $options = [];
+    private array $arguments = [];
+
+    public function __construct(array $argv)
+    {
+        array_shift($argv);
+        $ignoreKeys = [];
+        foreach ($argv as $key => $value) {
+            if (in_array($key, $ignoreKeys, true)) {
+                continue;
+            }
+
+            if (self::startsWith($value, '--')) {
+                $it = explode("=", ltrim($value, '-'), 2);
+                $optionName = $it[0];
+                $optionValue = $it[1] ?? true;
+                $this->options[$optionName] = $optionValue;
+            } elseif (self::startsWith($value, '-')) {
+                $optionName = ltrim($value, '-');
+                if (strlen($optionName) > 1) {
+                    $options = str_split($optionName);
+                    foreach ($options as $option) {
+                        $this->options[$option] = true;
+                    }
+                } else {
+                    $this->options[$optionName] = true;
+                    if (isset($argv[$key + 1]) && !self::startsWith($argv[$key + 1], '-')) {
+                        $ignoreKeys[] = $key + 1;
+                        $this->options[$optionName] = $argv[$key + 1];
+                    }
+                }
+            } else {
+                $this->arguments[] = $value;
+            }
+        }
+    }
+
+    public function getOptions(): array
+    {
+        return $this->options;
+    }
+
+    public function getArguments(): array
+    {
+        return $this->arguments;
+    }
+
+    public function hasOption(string $name): bool
+    {
+        return array_key_exists($name, $this->options);
+    }
+
+    public function getOptionValue(string $name)
+    {
+        if (!$this->hasOption($name)) {
+            throw new InvalidArgumentException(sprintf('Option "%s" is not defined.', $name));
+        }
+        return $this->options[$name];
+    }
+
+    public function getArgumentValue(int $key)
+    {
+        if (!$this->hasArgument($key)) {
+            throw new InvalidArgumentException(sprintf('Argument "%s" is not defined.', $key));
+        }
+        return $this->arguments[$key];
+    }
+
+    public function hasArgument(int $key): bool
+    {
+        return array_key_exists($key, $this->arguments);
+    }
+
+    private static function startsWith(string $haystack, string $needle): bool
+    {
+        return strncmp($haystack, $needle, strlen($needle)) === 0;
+    }
+}

+ 180 - 0
src/Console/Output.php

@@ -0,0 +1,180 @@
+<?php
+
+namespace Depo\UniTester\Console;
+
+use const PHP_EOL;
+
+final class Output
+{
+    /**
+     * @var callable
+     */
+    private $output;
+
+    const FOREGROUND_COLORS = [
+        'black' => '0;30',
+        'dark_gray' => '1;30',
+        'green' => '0;32',
+        'light_green' => '1;32',
+        'red' => '0;31',
+        'light_red' => '1;31',
+        'yellow' => '0;33',
+        'light_yellow' => '1;33',
+        'blue' => '0;34',
+        'dark_blue' => '0;34',
+        'light_blue' => '1;34',
+        'purple' => '0;35',
+        'light_purple' => '1;35',
+        'cyan' => '0;36',
+        'light_cyan' => '1;36',
+        'light_gray' => '0;37',
+        'white' => '1;37',
+    ];
+
+    const BG_COLORS = [
+        'black' => '40',
+        'red' => '41',
+        'green' => '42',
+        'yellow' => '43',
+        'blue' => '44',
+        'magenta' => '45',
+        'cyan' => '46',
+        'light_gray' => '47',
+    ];
+
+    public function __construct(callable $output = null)
+    {
+        if ($output === null) {
+            $output = function ($message) {
+                fwrite(STDOUT, $message);
+            };
+        }
+        $this->output = $output;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function write(string $message, ?string $color = null, ?string $background = null): void
+    {
+        $formattedMessage = '';
+
+        if ($color) {
+            $formattedMessage .= "\033[" . self::FOREGROUND_COLORS[$color] . 'm';
+        }
+        if ($background) {
+            $formattedMessage .= "\033[" . self::BG_COLORS[$background] . 'm';
+        }
+
+        if (!empty($formattedMessage)) {
+            $formattedMessage .= $message . "\033[0m";
+        } else {
+            $formattedMessage = $message;
+        }
+
+        $output = $this->output;
+        $output($formattedMessage);
+    }
+
+    public function writeln(string $message): void
+    {
+        $this->write($message);
+        $this->write(PHP_EOL);
+    }
+
+    public function list(array $items): void
+    {
+        foreach ($items as $item) {
+            $this->write('- ' . $item);
+            $this->write(PHP_EOL);
+        }
+        $this->write(PHP_EOL);
+    }
+
+    public function listKeyValues(array $items, bool $inlined = false): void
+    {
+        $maxKeyLength = 0;
+        if ($inlined) {
+            foreach ($items as $key => $value) {
+                $keyLength = \mb_strlen($key);
+                if ($keyLength > $maxKeyLength) {
+                    $maxKeyLength = $keyLength;
+                }
+            }
+        }
+
+        foreach ($items as $key => $value) {
+            $key = str_pad($key, $maxKeyLength, ' ', STR_PAD_RIGHT);
+            $this->write($key, 'green');
+            $this->write(' : ');
+            $this->write($value, 'white');
+            $this->write(PHP_EOL);
+        }
+        $this->write(PHP_EOL);
+    }
+
+    public function success(string $message): void
+    {
+        [$formattedMessage, $lineLength, $color] = $this->formatMessage('OK', $message, 'green');
+        $this->outputMessage($formattedMessage, $lineLength, $color);
+    }
+
+    public function error(string $message): void
+    {
+        [$formattedMessage, $lineLength, $color] = $this->formatMessage('ERROR', $message, 'red');
+        $this->outputMessage($formattedMessage, $lineLength, $color);
+    }
+
+    public function warning(string $message): void
+    {
+        [$formattedMessage, $lineLength, $color] = $this->formatMessage('WARNING', $message, 'yellow');
+        $this->outputMessage($formattedMessage, $lineLength, $color);
+    }
+
+    public function info(string $message): void
+    {
+        [$formattedMessage, $lineLength, $color] = $this->formatMessage('INFO', $message, 'blue');
+        $this->outputMessage($formattedMessage, $lineLength, $color);
+    }
+
+    private function outputMessage($formattedMessage, int $lineLength, string $color): void
+    {
+        $this->write(PHP_EOL);
+        $this->write(str_repeat(' ', $lineLength), 'white', $color);
+        $this->write(PHP_EOL);
+
+        if (is_string($formattedMessage)) {
+            $formattedMessage = [$formattedMessage];
+        }
+
+        foreach ($formattedMessage as $line) {
+            $this->write($line, 'white', $color);
+        }
+
+        $this->write(PHP_EOL);
+        $this->write(str_repeat(' ', $lineLength), 'white', $color);
+        $this->write(PHP_EOL);
+        $this->write(PHP_EOL);
+    }
+
+    private function formatMessage(string $prefix, string $message, string $color): array
+    {
+        $formattedMessage = sprintf('[%s] %s', $prefix, trim($message));
+        $lineLength = \mb_strlen($formattedMessage);
+        $consoleWidth = $this->geTerminalWidth();
+
+        if ($lineLength > $consoleWidth) {
+            $lineLength = $consoleWidth;
+            $lines = explode('|', wordwrap($formattedMessage, $lineLength, '|', true));
+            $formattedMessage = array_map(function ($line) use ($lineLength) {
+                return str_pad($line, $lineLength);
+            }, $lines);
+        }
+        return [$formattedMessage, $lineLength, $color];
+    }
+
+    private function geTerminalWidth(): int
+    {
+        return ((int)exec('tput cols') ?? 85 - 5);
+    }
+}

+ 7 - 0
src/Exception/AssertionFailureException.php

@@ -0,0 +1,7 @@
+<?php
+
+namespace Depo\UniTester\Exception;
+
+final class AssertionFailureException extends \ErrorException
+{
+}

+ 258 - 0
src/TestCase.php

@@ -0,0 +1,258 @@
+<?php
+
+namespace Depo\UniTester;
+
+use Exception;
+use LogicException;
+use Depo\UniTester\Exception\AssertionFailureException;
+use Depo\UniTester\Console\Output;
+use Psr\Container\ContainerInterface;
+use Throwable;
+
+abstract class TestCase
+{
+    private int $assertions = 0;
+    private ?ContainerInterface $container;
+    private ?Output $output = null;
+
+    final public function __construct(ContainerInterface $container = null)
+    {
+        $this->container = $container;
+    }
+
+    final public function setOutput(Output $output): void
+    {
+        $this->output = $output;
+    }
+
+    final protected function log(string $message): void
+    {
+        if ($this->output) {
+            $this->output->writeln($message);
+        }
+    }
+
+    final protected function dump($value): void
+    {
+        if ($this->output) {
+            $this->output->writeln(_dump_with_type($value));
+        }
+    }
+
+    abstract protected function setUp(): void;
+
+    abstract protected function tearDown(): void;
+
+    abstract protected function execute(): void;
+
+    final public function run(): void
+    {
+        $this->setUp();
+
+        try {
+            $this->execute();
+        } finally {
+            $this->tearDown();
+        }
+    }
+
+    final public function getContainer(): ContainerInterface
+    {
+        if ($this->container === null) {
+            throw new LogicException('Container not set in Depo\UniTester\TestExecutor.');
+        }
+
+        return $this->container;
+    }
+
+    final protected function assertStrictEquals($expected, $actual, string $message = ''): void
+    {
+        assert_strict_equals($expected, $actual, $message);
+        $this->assertions++;
+    }
+
+    final protected function assertEquals($expected, $actual, string $message = ''): void
+    {
+        assert_equals($expected, $actual, $message);
+        $this->assertions++;
+    }
+
+    final protected function assertNotStrictEquals($expected, $actual, string $message = ''): void
+    {
+        assert_not_strict_equals($expected, $actual, $message);
+        $this->assertions++;
+    }
+
+    final protected function assertNotEquals($expected, $actual, string $message = ''): void
+    {
+        assert_not_equals($expected, $actual, $message);
+        $this->assertions++;
+    }
+
+    final protected function assertSimilar($expected, $actual, string $message = ''): void
+    {
+        assert_similar($expected, $actual, $message);
+        $this->assertions++;
+    }
+
+    final protected function assertTrue($condition, string $message = ''): void
+    {
+        assert_true($condition, $message);
+        $this->assertions++;
+    }
+
+    final protected function assertFalse($condition, string $message = ''): void
+    {
+        assert_false($condition, $message);
+        $this->assertions++;
+    }
+
+    final protected function assertNull($value, string $message = ''): void
+    {
+        assert_null($value, $message);
+        $this->assertions++;
+    }
+
+    final protected function assertNotNull($value, string $message = ''): void
+    {
+        assert_not_null($value, $message);
+        $this->assertions++;
+    }
+
+    final protected function assertEmpty($value, string $message = ''): void
+    {
+        assert_empty($value, $message);
+        $this->assertions++;
+    }
+
+    final protected function assertNotEmpty($value, string $message = ''): void
+    {
+        assert_not_empty($value, $message);
+        $this->assertions++;
+    }
+
+    final protected function assertInstanceOf(string $expected, $actual, string $message = ''): void
+    {
+        assert_instanceof($expected, $actual, $message);
+        $this->assertions++;
+    }
+
+
+    final protected function assertStringLength($string, int $length, string $message = ''): void
+    {
+        assert_string_length($string, $length, $message);
+        $this->assertions++;
+    }
+
+    final protected function assertStringContains(string $haystack, $needle, string $message = ''): void
+    {
+        assert_string_contains($haystack, $needle, $message);
+        $this->assertions++;
+    }
+
+    final protected function assertStringStartsWith(string $haystack, $needle, string $message = ''): void
+    {
+        assert_string_starts_with($haystack, $needle, $message);
+        $this->assertions++;
+    }
+
+    final protected function assertStringEndsWith(string $haystack, $needle, string $message = ''): void
+    {
+        assert_string_ends_with($haystack, $needle, $message);
+        $this->assertions++;
+    }
+
+    final protected function assertPositiveInt($value, string $message = ''): void
+    {
+        assert_positive_int($value, $message);
+        $this->assertions++;
+    }
+
+    final protected function assertNegativeInt($value, string $message = ''): void
+    {
+        assert_negative_int($value, $message);
+        $this->assertions++;
+    }
+
+    final protected function expectException(string $exception, callable $callable, string $message = null): void
+    {
+        try {
+            $callable();
+            throw new AssertionFailureException('Expected exception not thrown. : ' . $exception);
+        } catch (Throwable $e) {
+            if ($e instanceof AssertionFailureException) {
+                throw $e;
+            }
+            assert_equals($exception, get_class($e));
+            if ($message !== null) {
+                assert_equals($message, $e->getMessage());
+            }
+            $this->assertions++;
+        }
+    }
+
+    final protected function assertCount(int $expectedCount, $haystack, string $message = ''): void
+    {
+        assert_count($expectedCount, $haystack, $message);
+        $this->assertions++;
+    }
+
+    final protected function assertArrayHasKey($key, $array, string $message = ''): void
+    {
+        assert_array_has_key($key, $array, $message);
+        $this->assertions++;
+    }
+
+    final protected function fail(string $message = ''): void
+    {
+        fail($message);
+    }
+
+
+    final protected function assertExecutionTimeLessThan(float $maxMs, callable $callback, string $message = ''): void
+    {
+        assert_execution_time_less_than($maxMs, $callback, $message);
+        $this->assertions++;
+    }
+
+    final protected function assertMemoryUsageLessThan(int $maxBytes, callable $callback, string $message = ''): void
+    {
+        assert_memory_usage_less_than($maxBytes, $callback, $message);
+        $this->assertions++;
+    }
+
+    final protected function assertFileExists(string $path, string $message = ''): void
+    {
+        assert_file_exists($path, $message);
+        $this->assertions++;
+    }
+
+    final protected function assertFileNotExists(string $path, string $message = ''): void
+    {
+        assert_file_not_exists($path, $message);
+        $this->assertions++;
+    }
+
+    final protected function assertDirectoryExists(string $path, string $message = ''): void
+    {
+        assert_directory_exists($path, $message);
+        $this->assertions++;
+    }
+
+    final protected function assertIsReadable(string $path, string $message = ''): void
+    {
+        assert_is_readable($path, $message);
+        $this->assertions++;
+    }
+
+    final protected function assertIsWritable(string $path, string $message = ''): void
+    {
+        assert_is_writable($path, $message);
+        $this->assertions++;
+    }
+
+    final public function getAssertions(): int
+    {
+        return $this->assertions;
+    }
+}

+ 121 - 0
src/TestExecutor.php

@@ -0,0 +1,121 @@
+<?php
+
+namespace Depo\UniTester;
+
+use LogicException;
+use Depo\Unitester\Console\Output;
+use Psr\Container\ContainerExceptionInterface;
+use Psr\Container\ContainerInterface;
+use Psr\Container\NotFoundExceptionInterface;
+use Throwable;
+
+final class TestExecutor
+{
+    const TEST_PASSED = 0;
+    const TEST_FAILED = 1;
+    private array $testClasses = [];
+    private Output $output;
+    private ?ContainerInterface $container;
+
+    public function __construct(array $testClasses, Output $output, ?ContainerInterface $container = null)
+    {
+        $this->testClasses = $testClasses;
+        $this->container = $container;
+        $this->output = $output;
+    }
+
+    public function run(): int
+    {
+        $this->output->writeln('Running ' . count($this->testClasses) . ' tests...');
+        $this->output->writeln('Press Ctrl+C to stop...');
+        $this->output->writeln('');
+
+        $passedTests = 0;
+        $failedTests = 0;
+        $assertions = 0;
+        $memory = memory_get_usage();
+        foreach ($this->testClasses as $class) {
+            try {
+                $testCase = $this->resolveClassName($class);
+                $testCase->setOutput($this->output);
+                $testCase->run();
+                $passedTests++;
+                $assertions += $testCase->getAssertions();
+                $this->logSuccess($testCase);
+            } catch (Throwable $e) {
+                $failedTests++;
+                $this->logFailure($class, $e);
+            }
+
+        }
+
+        $this->output->writeln('');
+        $this->printSummary($passedTests, $failedTests, $assertions);
+        if ($failedTests == 0) {
+            $this->output->success('All tests passed.');
+            $this->output->writeln('Memory usage: ' . self::memoryConvert(memory_get_peak_usage() - $memory));
+            return self::TEST_PASSED;
+        }
+        $this->output->error($failedTests . ' tests failed.');
+        return self::TEST_FAILED;
+    }
+
+    /**
+     * @param string|object $className
+     * @return TestCase
+     * @throws ContainerExceptionInterface
+     * @throws NotFoundExceptionInterface
+     */
+    private function resolveClassName($className): TestCase
+    {
+        if (!is_subclass_of($className, TestCase::class)) {
+            throw new LogicException("Test class $className is not a subclass of TestCase.");
+        }
+
+        if (is_object($className)) {
+            return $className;
+        }
+
+        if (!$this->container instanceof ContainerInterface) {
+            return new $className();
+        }
+
+        return $this->container->get($className);
+    }
+
+    private function logSuccess(object $testCase): void
+    {
+        $testName = get_class($testCase);
+        $this->output->write("✔ $testName PASSED", 'green');
+        $this->output->write(PHP_EOL);
+    }
+
+    private function logFailure($testCase, Throwable $e): void
+    {
+        $message = $e->getMessage();
+        $this->output->writeln(sprintf('❌ Critical error in %s: %s', get_class($e), $message));
+        $this->output->writeln('Stack trace:');
+        $this->output->writeln($e->getTraceAsString());
+
+        $testName = is_object($testCase) ? get_class($testCase) : $testCase;
+        $this->output->write("✘ $testName FAILED : $message", 'red');
+        $this->output->write(PHP_EOL);
+    }
+
+    private function printSummary(int $passedTests = 0, int $failedTests = 0, int $assertions = 0): void
+    {
+        $totalTests = $passedTests + $failedTests;
+        $this->output->listKeyValues([
+            'Total Tests' => $totalTests,
+            'Passed' => $passedTests,
+            'Failed' => $failedTests,
+            'Assertions' => $assertions,
+        ]);
+    }
+
+    private static function memoryConvert(int $size): string
+    {
+        $unit = array('B', 'KB', 'MB', 'GB', 'TB', 'PB');
+        return @round($size / pow(1024, ($i = floor(log($size, 1024)))), 2) . ' ' . $unit[$i];
+    }
+}

+ 78 - 0
src/TestFinder.php

@@ -0,0 +1,78 @@
+<?php
+
+namespace Depo\UniTester;
+
+final class TestFinder
+{
+    private string $directory;
+    public function __construct(string $directory)
+    {
+        if (!is_dir($directory)) {
+            throw new \InvalidArgumentException("Directory '{$directory}' does not exist.");
+        }
+
+        $this->directory = $directory;
+    }
+
+    public function find(): array
+    {
+        return $this->findTestClasses();
+    }
+
+    private function findTestClasses(): array
+    {
+        $testClasses = [];
+        $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($this->directory));
+        foreach ($iterator as $file) {
+            if ($file->isDir() || $file->getExtension() !== 'php') {
+                continue;
+            }
+
+            $class = self::extractNamespaceAndClass($file->getPathname());
+            if (class_exists($class) && is_subclass_of($class, TestCase::class)) {
+                $testClasses[] = $class;
+            }
+        }
+        return $testClasses;
+    }
+
+    private static function extractNamespaceAndClass(string $filePath): string
+    {
+        if (!file_exists($filePath)) {
+            throw new \InvalidArgumentException('File not found: ' . $filePath);
+        }
+
+        $contents = file_get_contents($filePath);
+        $namespace = '';
+        $class = '';
+        $isExtractingNamespace = false;
+        $isExtractingClass = false;
+
+        foreach (token_get_all($contents) as $token) {
+            if (is_array($token) && $token[0] == T_NAMESPACE) {
+                $isExtractingNamespace = true;
+            }
+
+            if (is_array($token) && $token[0] == T_CLASS) {
+                $isExtractingClass = true;
+            }
+
+            if ($isExtractingNamespace) {
+                if (is_array($token) && in_array($token[0], [T_STRING, T_NS_SEPARATOR,  265 /* T_NAME_QUALIFIED For PHP 8*/])) {
+                    $namespace .= $token[1];
+                } else if ($token === ';') {
+                    $isExtractingNamespace = false;
+                }
+            }
+
+            if ($isExtractingClass) {
+                if (is_array($token) && $token[0] == T_STRING) {
+                    $class = $token[1];
+                    break;
+                }
+            }
+        }
+        return $namespace ? $namespace . '\\' . $class : $class;
+    }
+
+}

+ 144 - 0
src/TestRunnerCli.php

@@ -0,0 +1,144 @@
+<?php
+
+namespace Depo\UniTester;
+
+use InvalidArgumentException;
+use LogicException;
+use Depo\UniTester\Console\ArgParser;
+use Depo\UniTester\Console\Output;
+use Throwable;
+
+final class TestRunnerCli
+{
+    const VERSION = '1.0.0';
+
+    const CLI_ERROR = 1;
+    const CLI_SUCCESS = 0;
+
+    public static function run(Output $output, array $args = null): int
+    {
+        if (php_sapi_name() !== 'cli') {
+            throw new LogicException('This script can only be run from the command line.');
+        }
+
+        set_time_limit(0);
+
+        try {
+            $args = $args ?? $_SERVER['argv'] ?? [];
+            $arg = new ArgParser($args);
+            foreach ($arg->getOptions() as $name => $value) {
+                if (!in_array($name, self::allowedOptions(), true)) {
+                    throw new InvalidArgumentException('Invalid option: ' . $name);
+                }
+            }
+
+            if ($arg->hasOption('help')) {
+                self::printUsage($output);
+                return self::CLI_SUCCESS;
+            }
+
+            if ($arg->hasOption('version')) {
+                $output->writeln(sprintf('PHP UniTester version %s', self::VERSION));
+                return self::CLI_SUCCESS;
+            }
+
+            if (count($arg->getArguments()) === 0) {
+                self::printUsage($output);
+                return self::CLI_SUCCESS;
+            }
+
+            if (count($arg->getArguments()) > 1) {
+                throw new InvalidArgumentException('Too many arguments, only one allowed.');
+            }
+
+            $dir = $arg->getArgumentValue(0);
+            if (!(strncmp($dir, DIRECTORY_SEPARATOR, strlen(DIRECTORY_SEPARATOR)) === 0)) {
+                $dir = getcwd() . DIRECTORY_SEPARATOR . $dir;
+            }
+
+
+            $testFinder = new TestFinder($dir);
+            $envFile = getcwd() . DIRECTORY_SEPARATOR. '.env.test';
+            self::loadEnv($envFile);
+            $testExecutor = new TestExecutor($testFinder->find(), $output);
+            return $testExecutor->run() === 0 ? self::CLI_SUCCESS : self::CLI_ERROR;
+
+        } catch (Throwable $e) {
+            $output->error($e->getMessage());
+            return self::CLI_ERROR;
+        }
+    }
+
+
+    private static function printUsage(Output $output): void
+    {
+        $output->writeln('Usage PHP UniTester : [options] [folder]');
+        $output->writeln('Options:');
+        $output->writeln('  --help       Show this help message');
+        $output->writeln('  --version    Show the version number');
+
+        $output->writeln('Examples:');
+        $output->writeln('  Run all tests in tests folder : bin/unitester tests/');
+        $output->writeln('  Display help                  : bin/unitester --help');
+        $output->writeln('  Show version                  : bin/unitester --version');
+    }
+
+    private static function allowedOptions(): array
+    {
+        return ['help', 'version'];
+    }
+
+    private static function loadEnv(string $path): void {
+        if (!file_exists($path)) {
+            return;
+        }
+
+        $lines = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
+        foreach ($lines as $line) {
+            $line = trim($line);
+            if (strpos(trim($line), '#') === 0) {
+                continue;
+            }
+            list($name, $value) = explode('=', $line, 2);
+            $name = trim($name);
+            $value = self::processEnvValue($value);
+            if (!array_key_exists($name, $_ENV)) {
+                putenv(sprintf('%s=%s', $name, $value));
+                $_ENV[$name] = $value;
+            }
+        }
+    }
+
+    private static function processEnvValue(string $value)
+    {
+        $value = trim($value);
+        if (is_numeric($value)) {
+            $int = filter_var($value, FILTER_VALIDATE_INT);
+            if ($int !== false) {
+                return $int;
+            }
+            return (float) $value;
+        }
+        if (empty($value)) {
+            return null;
+        }
+
+
+        if (strtolower($value) === 'true') {
+            return true;
+        }
+        if (strtolower($value) === 'false') {
+            return false;
+        }
+        if (in_array($value, ['null', 'NULL'], true)) {
+            return null;
+        }
+
+        $first = $value[0] ?? null;
+        $last = $value[strlen($value) - 1] ?? null;
+        if (($first === '"' && $last === '"') || ($first === "'" && $last === "'")) {
+            return trim(substr($value, 1, -1));
+        }
+        return $value;
+    }
+}

+ 325 - 0
src/assert.php

@@ -0,0 +1,325 @@
+<?php
+
+namespace Depo\UniTester;
+
+use Exception;
+use Depo\UniTester\Exception\AssertionFailureException;
+
+/**
+ * Formats a value for display in assertion messages.
+ *
+ * @param mixed $value
+ * @return string
+ */
+function _dump($value): string
+{
+    if (is_null($value)) {
+        return 'null';
+    }
+    if (is_bool($value)) {
+        return $value ? 'true' : 'false';
+    }
+    if (is_string($value)) {
+        return "'{$value}'";
+    }
+    if (is_array($value)) {
+        // Use json_encode for compact array representation
+        return json_encode($value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
+    }
+    if (is_object($value)) {
+        return get_class($value) . ' ' . json_encode($value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
+    }
+    if (is_resource($value)) {
+        return get_resource_type($value);
+    }
+    return (string)$value;
+}
+
+/**
+ * Formats a value with its type for display in assertion messages.
+ *
+ * @param mixed $value
+ * @return string
+ */
+function _dump_with_type($value): string
+{
+    $type = gettype($value);
+    $dump = _dump($value);
+    return "{$dump} ({$type})";
+}
+
+function assert_strict_equals($expected, $actual, string $message = ''): void
+{
+    if ($expected !== $actual) {
+        $expectedStr = _dump_with_type($expected);
+        $actualStr = _dump_with_type($actual);
+        throw new AssertionFailureException($message ?: "Expected strictly {$expectedStr}, got {$actualStr}");
+    }
+}
+
+function assert_equals($expected, $actual, string $message = ''): void
+{
+    if ($expected != $actual) {
+        $expectedStr = _dump($expected);
+        $actualStr = _dump($actual);
+        throw new AssertionFailureException($message ?: "Expected {$expectedStr}, got {$actualStr}");
+    }
+}
+
+function assert_not_strict_equals($expected, $actual, string $message = ''): void
+{
+    if ($expected === $actual) {
+        $expectedStr = _dump_with_type($expected);
+        throw new AssertionFailureException($message ?: "Expected value to not be strictly equal to {$expectedStr}");
+    }
+}
+
+function assert_not_equals($expected, $actual, string $message = ''): void
+{
+    if ($expected == $actual) {
+        $expectedStr = _dump($expected);
+        throw new AssertionFailureException($message ?: "Expected value to not be equal to {$expectedStr}");
+    }
+}
+
+function assert_similar($expected, $actual, string $message = ''): void
+{
+    if ($expected != $actual) {
+        $expectedStr = _dump($expected);
+        $actualStr = _dump($actual);
+        throw new AssertionFailureException($message ?: "Expected similar to {$expectedStr}, got {$actualStr}");
+    }
+}
+
+function assert_true($condition, string $message = ''): void
+{
+    if ($condition !== true) {
+        $actualStr = _dump_with_type($condition);
+        throw new AssertionFailureException($message ?: "Expected true, got {$actualStr}");
+    }
+}
+
+function assert_false($condition, string $message = ''): void
+{
+    if ($condition !== false) {
+        $actualStr = _dump_with_type($condition);
+        throw new AssertionFailureException($message ?: "Expected false, got {$actualStr}");
+    }
+}
+
+function assert_null($value, string $message = ''): void
+{
+    if (!is_null($value)) {
+        $actualStr = _dump_with_type($value);
+        throw new AssertionFailureException($message ?: "Expected null, got {$actualStr}");
+    }
+}
+
+function assert_not_null($value, string $message = ''): void
+{
+    if (is_null($value)) {
+        throw new AssertionFailureException($message ?: "Expected not null, got null");
+    }
+}
+
+function assert_empty($value, string $message = ''): void
+{
+    if (!empty($value)) {
+        $actualStr = _dump_with_type($value);
+        throw new AssertionFailureException($message ?: "Expected empty, got {$actualStr}");
+    }
+}
+
+function assert_not_empty($value, string $message = ''): void
+{
+    if (empty($value)) {
+        $actualStr = _dump_with_type($value);
+        throw new AssertionFailureException($message ?: "Expected not empty, got {$actualStr}");
+    }
+}
+
+function assert_instanceof(string $expected, $actual, string $message = ''): void
+{
+    if (!is_object($actual)) {
+        $actualStr = _dump_with_type($actual);
+        throw new AssertionFailureException($message ?: "Expected instance of {$expected}, got {$actualStr}");
+    }
+
+    if (!is_a($actual, $expected)) {
+        $actualClass = get_class($actual);
+        throw new AssertionFailureException($message ?: "Expected instance of {$expected}, got {$actualClass}");
+    }
+}
+
+function assert_string_length($string, int $length, string $message = ''): void
+{
+    if (!is_string($string)) {
+        $actualStr = _dump_with_type($string);
+        throw new AssertionFailureException($message ?: "Expected string, got {$actualStr}");
+    }
+
+    if (strlen($string) !== $length) {
+        $actualLength = strlen($string);
+        throw new AssertionFailureException($message ?: "Expected string length of {$length}, got {$actualLength}");
+    }
+}
+
+function assert_string_contains($haystack, $needle, string $message = ''): void
+{
+    if (!is_string($haystack)) {
+        $actualStr = _dump_with_type($haystack);
+        throw new AssertionFailureException($message ?: "Expected haystack to be string, got {$actualStr}");
+    }
+    if (!is_string($needle)) {
+        $needleStr = _dump_with_type($needle);
+        throw new AssertionFailureException($message ?: "Expected needle to be string, got {$needleStr}");
+    }
+
+    if (strpos($haystack, $needle) === false) {
+        throw new AssertionFailureException($message ?: "Expected '{$haystack}' to contain '{$needle}'");
+    }
+}
+
+function assert_string_starts_with($haystack, $needle, string $message = ''): void
+{
+    if (!is_string($haystack)) {
+        $actualStr = _dump_with_type($haystack);
+        throw new AssertionFailureException($message ?: "Expected haystack to be string, got {$actualStr}");
+    }
+    if (!is_string($needle)) {
+        $needleStr = _dump_with_type($needle);
+        throw new AssertionFailureException($message ?: "Expected needle to be string, got {$needleStr}");
+    }
+
+    if (strpos($haystack, $needle) !== 0) {
+        throw new AssertionFailureException($message ?: "Expected '{$haystack}' to start with '{$needle}'");
+    }
+}
+
+function assert_string_ends_with($haystack, $needle, string $message = ''): void
+{
+    if (!is_string($haystack)) {
+        $actualStr = _dump_with_type($haystack);
+        throw new AssertionFailureException($message ?: "Expected haystack to be string, got {$actualStr}");
+    }
+    if (!is_string($needle)) {
+        $needleStr = _dump_with_type($needle);
+        throw new AssertionFailureException($message ?: "Expected needle to be string, got {$needleStr}");
+    }
+    if (substr($haystack, -strlen($needle)) !== $needle) {
+        throw new AssertionFailureException($message ?: "Expected '{$haystack}' to end with '{$needle}'");
+    }
+}
+
+function assert_positive_int($value, string $message = ''): void
+{
+    if (!is_int($value) || $value <= 0) {
+        $actualStr = _dump_with_type($value);
+        throw new AssertionFailureException($message ?: "Expected positive integer, got {$actualStr}");
+    }
+}
+
+function assert_negative_int($value, string $message = ''): void
+{
+    if (!is_int($value) || $value >= 0) {
+        $actualStr = _dump_with_type($value);
+        throw new AssertionFailureException($message ?: "Expected negative integer, got {$actualStr}");
+    }
+}
+
+function assert_count(int $expectedCount, $haystack, string $message = ''): void
+{
+    if (!is_array($haystack) && !$haystack instanceof \Countable) {
+        $actualStr = _dump_with_type($haystack);
+        throw new AssertionFailureException($message ?: "Expected array or Countable, got {$actualStr}");
+    }
+
+    $actualCount = count($haystack);
+    if ($actualCount !== $expectedCount) {
+        throw new AssertionFailureException($message ?: "Expected count {$expectedCount}, got {$actualCount}");
+    }
+}
+
+function assert_array_has_key($key, $array, string $message = ''): void
+{
+    if (!is_array($array) && !($array instanceof \ArrayAccess)) {
+        $actualStr = _dump_with_type($array);
+        throw new AssertionFailureException($message ?: "Expected array or ArrayAccess, got {$actualStr}");
+    }
+
+    if (is_array($array)) {
+        if (!array_key_exists($key, $array)) {
+            $keyStr = _dump($key);
+            throw new AssertionFailureException($message ?: "Expected array to have key {$keyStr}");
+        }
+    } else {
+        if (!$array->offsetExists($key)) {
+            $keyStr = _dump($key);
+            throw new AssertionFailureException($message ?: "Expected ArrayAccess to have key {$keyStr}");
+        }
+    }
+}
+
+function fail(string $message = ''): void
+{
+    throw new AssertionFailureException($message ?: "Test failed");
+}
+
+function assert_execution_time_less_than(float $maxMs, callable $callback, string $message = ''): void
+{
+    $start = microtime(true);
+    $callback();
+    $end = microtime(true);
+    $executionTimeMs = ($end - $start) * 1000;
+
+    if ($executionTimeMs > $maxMs) {
+        throw new AssertionFailureException($message ?: "Expected execution time to be less than {$maxMs}ms, but it took {$executionTimeMs}ms");
+    }
+}
+
+function assert_memory_usage_less_than(int $maxBytes, callable $callback, string $message = ''): void
+{
+    $startMemory = memory_get_usage();
+    $callback();
+    $endMemory = memory_get_usage();
+    $memoryUsage = $endMemory - $startMemory;
+
+    if ($memoryUsage > $maxBytes) {
+        throw new AssertionFailureException($message ?: "Expected memory usage to be less than {$maxBytes} bytes, but it used {$memoryUsage} bytes");
+    }
+}
+
+function assert_file_exists(string $path, string $message = ''): void
+{
+    if (!file_exists($path)) {
+        throw new AssertionFailureException($message ?: "Expected file '{$path}' to exist");
+    }
+}
+
+function assert_file_not_exists(string $path, string $message = ''): void
+{
+    if (file_exists($path)) {
+        throw new AssertionFailureException($message ?: "Expected file '{$path}' to not exist");
+    }
+}
+
+function assert_directory_exists(string $path, string $message = ''): void
+{
+    if (!is_dir($path)) {
+        throw new AssertionFailureException($message ?: "Expected directory '{$path}' to exist");
+    }
+}
+
+function assert_is_readable(string $path, string $message = ''): void
+{
+    if (!is_readable($path)) {
+        throw new AssertionFailureException($message ?: "Expected path '{$path}' to be readable");
+    }
+}
+
+function assert_is_writable(string $path, string $message = ''): void
+{
+    if (!is_writable($path)) {
+        throw new AssertionFailureException($message ?: "Expected path '{$path}' to be writable");
+    }
+}

+ 32 - 0
tests/AssertionTest.php

@@ -0,0 +1,32 @@
+<?php
+
+namespace Test\Depo\UniTester;
+
+use Depo\UniTester\TestCase;
+
+class AssertionTest extends TestCase
+{
+    protected function setUp(): void
+    {
+    }
+
+    protected function tearDown(): void
+    {
+    }
+
+    protected function execute(): void
+    {
+        $this->testAssertTrue();
+        $this->testAssertEquals();
+    }
+
+    public function testAssertTrue()
+    {
+        $this->assertTrue(true);
+    }
+
+    public function testAssertEquals()
+    {
+        $this->assertEquals(10, 5 + 5);
+    }
+}

+ 33 - 0
tests/ComparisonTest.php

@@ -0,0 +1,33 @@
+<?php
+
+namespace Test\Depo\UniTester;
+
+use Depo\UniTester\TestCase;
+
+class ComparisonTest extends TestCase
+{
+    protected function setUp(): void
+    {
+    }
+
+    protected function tearDown(): void
+    {
+    }
+
+    protected function execute(): void
+    {
+        $this->testAssertNotEquals();
+        $this->testAssertNull();
+    }
+
+    public function testAssertNotEquals()
+    {
+        $this->assertNotEquals(10, 20);
+    }
+
+    public function testAssertNull()
+    {
+        $this->assertNull(null); // Test que la valeur est null
+    }
+
+}

+ 28 - 0
tests/ExceptionHandlingTest.php

@@ -0,0 +1,28 @@
+<?php
+
+namespace Test\Depo\UniTester;
+
+use Depo\UniTester\TestCase;
+
+class ExceptionHandlingTest extends TestCase
+{
+    protected function setUp(): void
+    {
+    }
+
+    protected function tearDown(): void
+    {
+    }
+
+    protected function execute(): void
+    {
+        $this->testExceptionThrown();
+    }
+
+    public function testExceptionThrown()
+    {
+        $this->expectException(\LogicException::class, function() {
+            throw new \LogicException("Test exception");
+        });
+    }
+}

+ 7 - 0
tests/FakeTest.php

@@ -0,0 +1,7 @@
+<?php
+
+namespace Test\Depo\UniTester;
+
+final class FakeTest extends \stdClass
+{
+}

+ 49 - 0
tests/PerformanceFileTest.php

@@ -0,0 +1,49 @@
+<?php
+
+namespace Test\Depo\UniTester;
+
+use Depo\UniTester\TestCase;
+
+class PerformanceFileTest extends TestCase
+{
+    protected function setUp(): void
+    {
+        // Create a temporary file for testing
+        file_put_contents(__DIR__ . '/temp_test_file.txt', 'content');
+    }
+
+    protected function tearDown(): void
+    {
+        // Clean up
+        if (file_exists(__DIR__ . '/temp_test_file.txt')) {
+            unlink(__DIR__ . '/temp_test_file.txt');
+        }
+    }
+
+    protected function execute(): void
+    {
+        $this->log("Testing Performance Assertions...");
+        
+        // Test Execution Time
+        $this->assertExecutionTimeLessThan(100, function() {
+            usleep(10000); // 10ms
+        }, "Execution time should be less than 100ms");
+
+        // Test Memory Usage
+        $this->assertMemoryUsageLessThan(1024 * 1024, function() {
+            $a = str_repeat('a', 1024); // 1KB
+        }, "Memory usage should be less than 1MB");
+
+        $this->log("Testing File Assertions...");
+
+        $tempFile = __DIR__ . '/temp_test_file.txt';
+        
+        $this->assertFileExists($tempFile);
+        $this->assertFileNotExists(__DIR__ . '/non_existent_file.txt');
+        $this->assertDirectoryExists(__DIR__);
+        $this->assertIsReadable($tempFile);
+        $this->assertIsWritable($tempFile);
+        
+        $this->log("All new assertions passed!");
+    }
+}