lucinda / unit-testing
Lightweight PHP8.1 library for unit testing with zero dependencies
Installs: 19 047
Dependents: 21
Suggesters: 0
Security: 0
Stars: 0
Watchers: 2
Forks: 0
Open Issues: 0
Requires
- php: ^8.1
- ext-curl: *
- ext-pdo: *
- ext-simplexml: *
- ext-tokenizer: *
- lucinda/console: ~2.0
This package is auto-updated.
Last update: 2024-11-05 11:49:34 UTC
README
Table of contents:
About
This library was in part created out of frustration while working with PHPUnit, the standard solution used by over 99% of PHP applications that feature unit testing. This current software aims at building something that PHPUnit is not: a cleanly coded, zero dependencies API!
It only requires developer to follow these steps:
- configuration: setting up an XML file where unit testing is configured
- initialization: automated creation of unit testing architecture (classes and methods) for target API under testing
- development: user development of one or more unit tests for each class method created above
- execution: automated execution of unit tests on above foundations and display of unit test results on console or as JSON
API is fully PSR-4 compliant, only requiring PHP8.1+ interpreter, SimpleXML + cURL + PDO extensions (latter for URI and SQL testing) and Console Table API (for displaying unit test results). To quickly see how it works, check:
- installation: describes how to install API on your computer, in light of steps above
- assertions: describes how to make assertions using this API
- examples: shows real life unit tests for OAuth2 Client API
Why Not PHPUnit
Everything about that PHPUnit reminds of bygone ages when developers built huge classes that do "everything" and knew nothing about encapsulation except keyword "extends" (doubters should check https://github.com/sebastianbergmann/phpunit/blob/master/src/Framework/TestCase.php all PHPUnit tests must extend!).
Can something better be done? Should unit testing APIs abide to good principles of object oriented programming or only the code that is being tested? IMHO, as long as a developer feels confortable working with a mess, it will become a bad precedent to build something similar later on. Something much better MUST be done!
Configuration
Similar to PHPUnit, configuration of unit tests is done via an XML file with following syntax:
<?xml version="1.0" encoding="utf-8"?> <!DOCTYPE xml> <xml> <unit_tests> <unit_test> <sources path="PATH" namespace="NAMESPACE"/> <tests path="PATH" namespace="NAMESPACE"/> </unit_test> ... </unit_tests> <servers> <sql> <ENVIRONMENT> <server driver="DRIVER" host="HOSTNAME" port="PORT" username="USERNAME" password="PASSWORD" schema="SCHEMA" charset="CHARSET"/> </ENVIRONMENT> ... </sql> </servers> </xml>
Mandatory tag unit_tests stores the suite of unit tests to be executed. Each API under testing is identified by a unit_test tag which has following subtags:
- sources: configures base path of sources path (eg: src) and base API namespace (eg: Lucinda\Logging) via namesake attributes
- tests: configures base path of tests path (eg: tests) and base API namespace (eg: Test\Lucinda\Logging) via namesake attributes
These settings will be used to autoload classes test/sources classes whenever used: like in composer's case, you are required to fully qualify namespaces and end them with a backslash.
Optional tag servers stores connection settings for SQL servers that are going to be used in the unit tests, broken by ENVIRONMENT (value of php getenv("ENVIRONMENT")
). Each server must reflect into a server subtag where connection is configured by following attributes:
- DRIVER: (mandatory) name of SQL vendor, as recognized by PDO. Example: mysql
- HOSTNAME: (mandatory) current database server host name. Example: 127.0.0.1
- PORT: (optional) current database server port number. Example: 3306
- USERNAME: (mandatory) database server user name. Example: root
- PASSWORD: (mandatory) database server use password. Example: my-password
- SCHEMA: (optional) name of schema unit tests will run on. Example: test_schema
- CHARSET: (optional) default character set to use in connection. Example: utf8
Example: unit tests @ OAuth2 Client API
Implementation
Initialization
By simply running a Lucinda\UnitTest\Controller implementation (see installation section), classes in sources folder are mirrored into tests folder according to following rules:
- original folder structure is preserved, only that classes are renamed (see below)
- original class and file name is preserved, only it has "Test" appended. So MyClass and MyClass.php is mirrored to MyClassTest and MyClassTest.php
- original namespace is preserved, only it has "Test" namespace prepended. So Foo\Bar is mirrored to Test\Foo\Bar
- only public methods of source classes are mirrored
- arguments and return type of source methods are ignored, so original asd(string fgh): int will be mirrored to php asd()
- all created methods will have empty bodies
This insures 100% coverage is maintained on every execution, leaving programmers to develop missing unit tests themselves
Development
In order to be covered, each tests class public method MUST return either a single Lucinda\UnitTest\Result instance or a list of former, depending on whether or not you desire one or more tests. Each test has a status (passed or not) and an optional message (containing details that identify test against siblings).
Example:
namespace Test\Foo; // mirrors source namespace: Foo class BarTest { // mirrors class: Bar public function asd(): Lucinda\UnitTest\Result // mirrors method: asd @ Bar { $object = new \Foo\Bar(...); $data = $object->asd(...); // makes a single numeric assertion return (new Lucinda\UnitTest\Validator\Integers($data))->assertEquals(12); } public function fgh(): array // mirrors method: fgh @ Bar { $results = []; $object = new \Foo\Bar(...); $data = $object->fgh(...); // makes multiple assertions on same value $test = new Lucinda\UnitTest\Validator\Arrays($data); $results[] = $test->assertNotEmpty("is it empty"); $results[] = $test->assertContainsValue("qwerty"); return $results; } }
Execution
By simply running a Lucinda\UnitTest\Controller (see installation section) classes in tests folder are instanced, their public methods executed in the order they are set and Lucinda\UnitTest\Result instances are collected. The logic is as following:
- if a class @ src has no mirror class @ tests, unit test is marked as failed for respective class!
- if a class @ src has public methods not present in mirror class @ tests, unit test is marked as failed for respective method!
- if any of methods of mirror class do not return a Lucinda\UnitTest\Result or a list of latter, unit test is marked as failed for respective method with message that method is not covered
- results of unit tests are collected into a list of Lucinda\UnitTest\Result
This abstract class comes with following methods of interest:
API comes already with two with two Lucinda\UnitTest\Controller implementations:
- Lucinda\UnitTest\ConsoleController: displays unit test results in a table on console
- Lucinda\UnitTest\JsonController: displays unit test results as a json
Developers can build their own extensions that also save results somewhere...
Installation
In folder where your API under testing resides, run this command in console:
composer require lucinda/unit-testing
Then create a unit-tests.xml file holding configuration settings (see configuration above) and a test.php file with following code:
require(__DIR__."/vendor/autoload.php"); try { new Lucinda\UnitTest\ConsoleController("unit-tests.xml", "local"); } catch (Exception $e) { // handle exceptions }
To see a live example of usage, check unit tests for OAuth2 Client API!
Assertions
Assertions on Primitive Values
API allows you to make assertions on all PHP primitive data types:
- integer: via Lucinda\UnitTest\Validator\Integers
- float: via Lucinda\UnitTest\Validator\Floats
- string: via Lucinda\UnitTest\Validator\Strings
- boolean: via Lucinda\UnitTest\Validator\Booleans
- array: via Lucinda\UnitTest\Validator\Arrays
- object: via Lucinda\UnitTest\Validator\Objects
Each of these classes has a constructor in which a value of respective type is injected then a number of methods that make assertions on that value. In real life, you will only use those classes to make single assertions.
Assertion example:
$test = new Lucinda\UnitTest\Validator\Arrays($data); return $test->assertNotEmpty("is it empty");
Assertions on SQL Queries Results
Sometimes it is necessary to test information in database as well. For this you can use Lucinda\UnitTest\Validator\SQL class provided by API, which has four public methods:
Assertion example:
$test = new Lucinda\UnitTest\Validator\SQL($dataSource); $test->assertStatement("SELECT COUNT(id) AS nr FROM users", new class extends Lucinda\UnitTest\Validator\SQL\ResultValidator() { public function validate(\PDOStatement $statementResults): Result { $test = new Lucinda\UnitTest\Validator\Integer((integer) $statementResults->fetchColumn()); return $test->assertEquals(8); } });
Above mechanism allows you to develop MULTIPLE assertions on a single Lucinda\UnitTest\Validator\SQL instance, which in turn corresponds to a single SQL connection.
Assertions on URL Execution Results
Sometimes it is necessary to test results of URL execution. For this you can use Lucinda\UnitTest\Validator\URL class provided by API, which has two public methods:
Assertion example:
$test = new Lucinda\UnitTest\Validator\URL(new Lucinda\UnitTest\Validator\URL\DataSource("https://www.google.com")); $test->assert(new class extends Lucinda\UnitTest\Validator\URL\ResultValidator() { public function validate(Lucinda\UnitTest\Validator\URL\Response $response): Result { $test = new Lucinda\UnitTest\Validator\Strings($response->getBody()); return $test->assertContains("google"); } });
Above mechanism allows you to develop MULTIPLE assertions on same URL execution result via a single Lucinda\UnitTest\Validator\URL instance.
Assertions on Files
One can perform assertions on files by using Lucinda\UnitTest\Validator\Files class, which comes with following public methods:
Assertion example:
$test = new Lucinda\UnitTest\Validator\Files("foo/bar.php"); return $test->assertExists();
Examples
This OAuth2 Client API is among others that use this API for unit testing, so check:
- unit-tests.xml: for an example of configuration
- test.php: for an example of test suite executor
- tests: for examples of unit tests of classes from src
- tests_drivers: for examples of unit tests of classes from drivers