{"id":13700086,"url":"https://github.com/deminy/counit","last_synced_at":"2025-04-12T23:21:07.334Z","repository":{"id":56965276,"uuid":"402650695","full_name":"deminy/counit","owner":"deminy","description":"To run time/IO related unit tests (e.g., sleep function calls, database queries, API calls, etc) faster using Swoole.","archived":false,"fork":false,"pushed_at":"2024-02-01T07:25:05.000Z","size":87,"stargazers_count":12,"open_issues_count":0,"forks_count":2,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-03-26T17:21:19.140Z","etag":null,"topics":["phpunit","swoole","unit-testing"],"latest_commit_sha":null,"homepage":"","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/deminy.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null}},"created_at":"2021-09-03T04:54:42.000Z","updated_at":"2024-03-22T01:59:45.000Z","dependencies_parsed_at":"2024-01-12T05:13:41.556Z","dependency_job_id":"72b928f8-ecb3-4eb9-8dbe-860dc2066bfe","html_url":"https://github.com/deminy/counit","commit_stats":{"total_commits":42,"total_committers":1,"mean_commits":42.0,"dds":0.0,"last_synced_commit":"d8423b8fe29b3f2c87d5586d2345fefd71d3c360"},"previous_names":[],"tags_count":4,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/deminy%2Fcounit","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/deminy%2Fcounit/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/deminy%2Fcounit/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/deminy%2Fcounit/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/deminy","download_url":"https://codeload.github.com/deminy/counit/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248404339,"owners_count":21097713,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["phpunit","swoole","unit-testing"],"created_at":"2024-08-02T20:00:48.377Z","updated_at":"2025-04-12T23:21:07.310Z","avatar_url":"https://github.com/deminy.png","language":"PHP","funding_links":[],"categories":["Testing"],"sub_categories":[],"readme":"# counit: to run time/IO related unit tests faster using Swoole\n[![Library Status](https://github.com/deminy/counit/workflows/Unit%20Tests/badge.svg)](https://github.com/deminy/counit/actions)\n[![Latest Stable Version](https://poser.pugx.org/deminy/counit/v/stable.svg)](https://packagist.org/packages/deminy/counit)\n[![Latest Unstable Version](https://poser.pugx.org/deminy/counit/v/unstable.svg)](https://packagist.org/packages/deminy/counit)\n[![License](https://poser.pugx.org/deminy/counit/license.svg)](https://packagist.org/packages/deminy/counit)\n\nThis package helps to run time/IO related unit tests (e.g., sleep function calls, database queries, API calls, etc)\nfaster using [Swoole](https://github.com/swoole).\n\nTable of Contents\n=================\n\n* [How Does It Work](#how-does-it-work)\n* [Installation](#installation)\n* [Use \"counit\" in Your Project](#use-counit-in-your-project)\n* [Examples](#examples)\n   * [Setup Test Environment](#setup-test-environment)\n   * [The \"global\" Style](#the-global-style-recommended)\n   * [The \"case by case\" Style](#the-case-by-case-style)\n   * [Comparisons](#comparisons)\n* [Additional Notes](#additional-notes)\n* [Local Development](#local-development)\n* [Alternatives](#alternatives)\n* [TODOs](#todos)\n* [License](#license)\n\n# How Does It Work\n\nPackage _counit_ allows running multiple time/IO related tests concurrently within a single PHP process using Swoole.\n_Counit_ is compatible with _PHPUnit_, which means:\n\n1. Test cases can be written in the same way as those for _PHPUnit_.\n2. Test cases can run directly under _PHPUnit_.\n\nA typical test case of _counit_ looks like this:\n\n```php\nuse Deminy\\Counit\\TestCase; // Here is the only change made for counit, comparing to test cases for PHPUnit.\n\nclass SleepTest extends TestCase\n{\n  public function testSleep(): void\n  {\n    $startTime = time();\n    sleep(3);\n    $endTime = time();\n\n    self::assertEqualsWithDelta(3, ($endTime - $startTime), 1, 'The sleep() function call takes about 3 seconds to finish.');\n  }\n}\n```\n\nComparing to _PHPUnit_, _counit_ could make your test cases faster. Here is a comparison when running the same test suite\nusing _PHPUnit_ and _counit_ for a real project. In the test suite, many tests make calls to method\n_\\Deminy\\Counit\\Counit::sleep()_ to wait something to happen (e.g., wait data to expire).\n\n\u003ctable\u003e\n  \u003ctr\u003e\n    \u003cth\u003e\u0026nbsp;\u003c/th\u003e\n    \u003cth\u003e# of Tests\u003c/th\u003e\n    \u003cth\u003e# of Assertions\u003c/th\u003e\n    \u003cth\u003eTime to Finish\u003c/th\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd\u003e\u003cstrong\u003ecounit (without Swoole), or PHPUnit\u003c/strong\u003e\u003c/td\u003e\n    \u003ctd rowspan=\"2\"\u003e44\u003c/td\u003e\n    \u003ctd rowspan=\"2\"\u003e1148\u003c/td\u003e\n    \u003ctd\u003e9 minutes and 18 seconds\u003c/td\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd\u003e\u003cstrong\u003ecounit (with Swoole enabled)\u003c/strong\u003e\u003c/td\u003e\n    \u003ctd\u003e19 seconds\u003c/td\u003e\n  \u003c/tr\u003e\n\u003c/table\u003e\n\n# Installation\n\nThe package can be installed using _Composer_:\n\n```bash\ncomposer require deminy/counit --dev\n```\n\nOr, in your _composer.json_ file, make sure to have package _deminy/counit_ included:\n\n```json\n{\n  \"require-dev\": {\n    \"deminy/counit\": \"~0.2.0\"\n  }\n}\n```\n\n# Use \"counit\" in Your Project\n\n* Write unit tests in the same way as those for _PHPUnit_. However, to make those tests faster, please write those time/IO related tests in one of the following two styles (details will be discussed in the next sections):\n  * **The global style (recommended)**: Use class [_Deminy\\Counit\\TestCase_](https://github.com/deminy/counit/blob/master/src/TestCase.php) instead of _PHPUnit\\Framework\\TestCase_ as the base class.\n  * **The case-by-case style**: Wrap each test case inside the callback function for method [_Deminy\\Counit\\Counit::create()_](https://github.com/deminy/counit/blob/master/src/Counit.php), and use method [_Deminy\\Counit\\Counit::sleep()_](https://github.com/deminy/counit/blob/master/src/Counit.php) instead of the PHP function _sleep()_.\n* Use the binary executable _./vendor/bin/counit_ instead of _./vendor/bin/phpunit_ when running unit tests.\n* Have the Swoole extension installed. If not installed, _counit_ will work exactly same as _PHPUnit_ (in blocking mode).\n* Optional steps:\n  * use PHPUnit extension [_Deminy\\Counit\\CounitExtension_](https://github.com/deminy/counit/blob/master/src/CounitExtension.php) as shown in file [phpunit.xml.dist](https://github.com/deminy/counit/blob/master/phpunit.xml.dist). This is to wait the whole test suite to finish before printing out the summary information at the end.\n\n# Examples\n\nFolder [./tests/unit/global](https://github.com/deminy/counit/tree/master/tests/unit/global) and [./tests/unit/case-by-case](https://github.com/deminy/counit/tree/master/tests/unit/case-by-case) contain some sample tests, where we\nhave following time-related tests included:\n\n* Test slow HTTP requests.\n* Test long-running MySQL queries.\n* Test data expiration in Redis.\n* Test _sleep()_ function calls in PHP.\n\n## Setup Test Environment\n\nTo run the sample tests, please start the Docker containers and install Composer packages first:\n\n```bash\ndocker-compose up -d\ndocker compose exec -ti swoole composer install -n\n```\n\nThere are five containers started: a PHP container, a Swoole container, a Redis container, a MySQL container, and a web\nserver. The PHP container doesn't have the Swoole extension installed, while the Swoole container has it installed and enabled.\n\nAs said previously, test cases can be written in the same way as those for _PHPUnit_. However, to run time/IO related\ntests faster with _counit_, we need to make some adjustments when writing those test cases; these adjustments can be\nmade in two different styles.\n\n## The \"global\" Style (recommended)\n\nIn this style, each test case runs in a separate coroutine automatically.\n\nFor test cases written in this style, the only change to make on your existing test cases is to use class\n_Deminy\\Counit\\TestCase_ instead of _PHPUnit\\Framework\\TestCase_ as the base class.\n\nA typical test case of the global style looks like this:\n\n```php\nuse Deminy\\Counit\\TestCase; // Here is the only change made for counit, comparing to test cases for PHPUnit.\n\nclass SleepTest extends TestCase\n{\n  public function testSleep(): void\n  {\n    $startTime = time();\n    sleep(3);\n    $endTime = time();\n\n    self::assertEqualsWithDelta(3, ($endTime - $startTime), 1, 'The sleep() function call takes about 3 seconds to finish.');\n  }\n}\n```\n\nWhen customized method _setUpBeforeClass()_ and _tearDownAfterClass()_ are defined in the test cases, please make sure\nto call their parent methods accordingly in these customized methods.\n\nThis style assumes there is no immediate assertions in test cases, nor assertions before a sleep() function call or a\ncoroutine-friendly IO operation. Test cases like following still work, but they will trigger some warning messages when\ntested:\n\n```php\nclass GlobalTest extends Deminy\\Counit\\TestCase\n{\n  public function testAssertionSuppression(): void\n  {\n    self::assertTrue(true, 'Trigger an immediate assertion.');\n    // ......\n  }\n}\n```\n\nWe can rewrite this test class using the \"case by case\" style (discussed in the next section) to eliminate the warning messages.\n\nTo find more tests written in this style, please check tests under folder [./tests/unit/global](https://github.com/deminy/counit/tree/master/tests/unit/global) (test suite \"global\").\n\n## The \"case by case\" Style\n\nIn this style, you make changes directly on a test case to make it work asynchronously. \n\nFor test cases written in this style, we need to use class _Deminy\\Counit\\Counit_ accordingly in the test cases where\nwe need to wait for PHP execution or to perform IO operations. Typically, following method calls will be used:\n\n* Use method _Deminy\\Counit\\Counit::create()_ to wrap the test case.\n* Use method _Deminy\\Counit\\Counit::sleep()_ instead of the PHP function _sleep()_ to wait for PHP execution. You will\n  need some knowledge on Swoole if you want to make other IO related tests run asynchronously.\n\nA typical test case of the case-by-case style looks like this:\n\n```php\nuse Deminy\\Counit\\Counit;\nuse PHPUnit\\Framework\\TestCase;\n\nclass SleepTest extends TestCase\n{\n  public function testSleep(): void\n  {\n    Counit::create(function () { // To create a new coroutine manually to run the test case.\n      $startTime = time();\n      Counit::sleep(3); // Call this method instead of PHP function sleep().\n      $endTime = time();\n\n      self::assertEqualsWithDelta(3, ($endTime - $startTime), 1, 'The sleep() function call takes about 3 seconds to finish.');\n    });\n  }\n}\n```\n\nIn case you need to suppress warning message \"This test did not perform any assertions\" or to make the number of\nassertions match, you can include a 2nd parameter when creating the new coroutine:\n\n```php\nuse Deminy\\Counit\\Counit;\nuse PHPUnit\\Framework\\TestCase;\n\nclass SleepTest extends TestCase\n{\n  public function testSleep(): void\n  {\n    Counit::create( // To create a new coroutine manually to run the test case.\n      function () {\n        $startTime = time();\n        Counit::sleep(3); // Call this method instead of PHP function sleep().\n        $endTime = time();\n\n        self::assertEqualsWithDelta(3, ($endTime - $startTime), 1, 'The sleep() function call takes about 3 seconds to finish.');\n      },\n      1 // Optional. To suppress warning message \"This test did not perform any assertions\", and to make the counters match.\n    );\n  }\n}\n```\n\nTo find more tests written in this style, please check tests under folder [./tests/unit/case-by-case](https://github.com/deminy/counit/tree/master/tests/unit/case-by-case) (test suite \"case-by-case\").\n\n## Comparisons\n\nHere we will run the tests under different environments, with or without Swoole.\n\n`#1` Run the test suites using _PHPUnit_:\n\n```bash\n# To run test suite \"global\":\ndocker compose exec -ti php    ./vendor/bin/phpunit --testsuite global\n# or,\ndocker compose exec -ti swoole ./vendor/bin/phpunit --testsuite global\n\n# To run test suite \"case-by-case\":\ndocker compose exec -ti php    ./vendor/bin/phpunit --testsuite case-by-case\n# or,\ndocker compose exec -ti swoole ./vendor/bin/phpunit --testsuite case-by-case\n```\n\n`#2` Run the test suites using _counit_ (without Swoole):\n\n```bash\n# To run test suite \"global\":\ndocker compose exec -ti php    ./counit --testsuite global\n\n# To run test suite \"case-by-case\":\ndocker compose exec -ti php    ./counit --testsuite case-by-case\n```\n\n`#3` Run the test suites using _counit_  (with extension Swoole enabled):\n\n```bash\n# To run test suite \"global\":\ndocker compose exec -ti swoole ./counit --testsuite global\n\n# To run test suite \"case-by-case\":\ndocker compose exec -ti swoole ./counit --testsuite case-by-case\n```\n\nThe first two sets of commands take about same amount of time to finish. The last set of commands uses _counit_ and runs\nin the Swoole container (where the Swoole extension is enabled); thus it's faster than the others:\n\n\u003ctable\u003e\n  \u003ctr\u003e\n    \u003cth\u003e\u0026nbsp;\u003c/th\u003e\n    \u003cth\u003eStyle\u003c/th\u003e\n    \u003cth\u003e# of Tests\u003c/th\u003e\n    \u003cth\u003e# of Assertions\u003c/th\u003e\n    \u003cth\u003eTime to Finish\u003c/th\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd rowspan=\"2\"\u003e\u003cstrong\u003ecounit (without Swoole), or PHPUnit\u003c/strong\u003e\u003c/td\u003e\n    \u003ctd\u003eglobal\u003c/td\u003e\n    \u003ctd rowspan=\"4\"\u003e16\u003c/td\u003e\n    \u003ctd rowspan=\"4\"\u003e24\u003c/td\u003e\n    \u003ctd\u003e48 seconds\u003c/td\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd\u003ecase by case\u003c/td\u003e\n    \u003ctd\u003e48 seconds\u003c/td\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd rowspan=\"2\"\u003e\u003cstrong\u003ecounit (with Swoole enabled)\u003c/strong\u003e\u003c/td\u003e\n    \u003ctd\u003eglobal\u003c/td\u003e\n    \u003ctd\u003e7 seconds\u003c/td\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd\u003ecase by case\u003c/td\u003e\n    \u003ctd\u003e7 seconds\u003c/td\u003e\n  \u003c/tr\u003e\n\u003c/table\u003e\n\n# Additional Notes\n\nSince this package allows running multiple tests simultaneously, we should not use same resources in different tests;\notherwise, racing conditions could happen. For example, if multiple tests use the same Redis key, some of them could\nfail occasionally. In this case, we should use different Redis keys in different test cases. Method\n_\\Deminy\\Counit\\Helper::getNewKey()_ and _\\Deminy\\Counit\\Helper::getNewKeys()_ can be used to generate random and unique\ntest keys.\n\nThe package works best for tests that have function call _sleep()_ in use; It can also help to run some IO related tests\nfaster, with limitations apply. Here is a list of limitations of this package:\n\n* The package makes tests running faster by performing time/IO operations simultaneously. For functions/extensions that\n  work in blocking mode only, this package can't make their function calls faster. Here are some extensions that work in\n  blocking mode only: _MongoDB_, _Couchbase_, and some ODBC drivers.\n* The package doesn't work exactly the same as when running under _PHPUnit_:\n  * Tests may not have yet finished even it's marked as finished (by _PHPUnit_). Because of that, a test marked as \"passed\" (by PHPUnit) could still fail at a later time under _counit_. Because of this, the most reliable way to check if all test cases have passed or not is to check the exit code of _counit_.\n  * The # of assertions reported could be different from _PHPUnit_.\n  * Some exceptions/errors are not handled/reported the same.\n\n# Local Development\n\nThere are pre-built images [deminy/counit](https://hub.docker.com/r/deminy/counit) for running the sample tests. Here are\nthe commands to build the images:\n\n```bash\ndocker build -t deminy/counit:php-only       -f ./dockerfiles/php/Dockerfile    .\ndocker build -t deminy/counit:swoole-enabled -f ./dockerfiles/swoole/Dockerfile .\n```\n\n# Alternatives\n\nThis package allows to use Swoole to run multiple time/IO related tests without multiprocessing, which means all tests\ncan run within a single PHP process. To understand how exactly it works, I'd recommend checking this free online talk:\n[CSP Programming in PHP](https://nomadphp.com/video/306/csp-programming-in-php) (and here are the [slides](http://talks.deminy.in/csp.html)).\n\nIn the PHP ecosystem, there are other options to run unit tests in parallel, most end up using multiprocessing:\n\n* Process isolation in PHPUnit. This allows to run tests in separate PHP processes.\n* Package [brianium/paratest](https://github.com/paratestphp/paratest)\n* Package [pestphp/pest](https://pestphp.com)\n\n# TODOs\n\n* Better integration with _PHPUnit_.\n  * Deal with annotation _@doesNotPerformAssertions_ in the global style.\n  * Make # of assertions consistent with the one reported from _PHPUnit_.\n* Better error/exception handling.\n\n# License\n\nMIT license.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdeminy%2Fcounit","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdeminy%2Fcounit","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdeminy%2Fcounit/lists"}