PHPackages                             deminy/counit - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Testing &amp; Quality](/categories/testing)
4. /
5. deminy/counit

ActiveLibrary[Testing &amp; Quality](/categories/testing)

deminy/counit
=============

To run time/IO related unit tests (e.g., sleep function calls, database queries, API calls, etc) faster using Swoole.

1.0.2(3w ago)1215.2k—7.1%21MITPHPCI passing

Since Sep 22Pushed 3w ago2 watchersCompare

[ Source](https://github.com/deminy/counit)[ Packagist](https://packagist.org/packages/deminy/counit)[ RSS](/packages/deminy-counit/feed)WikiDiscussions master Synced 2w ago

READMEChangelog (9)Dependencies (4)Versions (12)Used By (1)

counit: to run time/IO related unit tests faster using Swoole
=============================================================

[](#counit-to-run-timeio-related-unit-tests-faster-using-swoole)

[![Library Status](https://github.com/deminy/counit/workflows/Unit%20Tests/badge.svg)](https://github.com/deminy/counit/actions)[![Latest Stable Version](https://camo.githubusercontent.com/1aad74558cf8fcfebd3ff609bf3a78bbe10244efd2cf90328b6acdf8640be951/68747470733a2f2f706f7365722e707567782e6f72672f64656d696e792f636f756e69742f762f737461626c652e737667)](https://packagist.org/packages/deminy/counit)[![Latest Unstable Version](https://camo.githubusercontent.com/230e5a82a6ec73088c4f784972b3da68dda1772003d3bb1c4e1d65a65bf717da/68747470733a2f2f706f7365722e707567782e6f72672f64656d696e792f636f756e69742f762f756e737461626c652e737667)](https://packagist.org/packages/deminy/counit)[![License](https://camo.githubusercontent.com/f52fc3996cb0f568302f467e2191dc1000e1cdfd0674cb5b62cf2e75da9404a3/68747470733a2f2f706f7365722e707567782e6f72672f64656d696e792f636f756e69742f6c6963656e73652e737667)](https://packagist.org/packages/deminy/counit)

This package helps to run time/IO related unit tests (e.g., sleep function calls, database queries, API calls, etc) faster using [Swoole](https://github.com/swoole).

Table of Contents
=================

[](#table-of-contents)

- [How Does It Work](#how-does-it-work)
- [Installation](#installation)
- [Use "counit" in Your Project](#use-counit-in-your-project)
- [Examples](#examples)
    - [Setup Test Environment](#setup-test-environment)
    - [The "global" Style](#the-global-style-recommended)
    - [The "case by case" Style](#the-case-by-case-style)
    - [Comparisons](#comparisons)
- [Additional Notes](#additional-notes)
- [Local Development](#local-development)
- [Alternatives](#alternatives)
- [TODOs](#todos)
- [License](#license)

How Does It Work
================

[](#how-does-it-work)

Package *counit* allows running multiple time/IO related tests concurrently within a single PHP process using Swoole. *Counit* is compatible with *PHPUnit*, which means:

1. Test cases can be written in the same way as those for *PHPUnit*.
2. Test cases can run directly under *PHPUnit*.

A typical test case of *counit* looks like this:

```
use Deminy\Counit\TestCase; // Here is the only change made for counit, comparing to test cases for PHPUnit.

class SleepTest extends TestCase
{
  public function testSleep(): void
  {
    $startTime = time();
    sleep(3);
    $endTime = time();

    self::assertEqualsWithDelta(3, ($endTime - $startTime), 1, 'The sleep() function call takes about 3 seconds to finish.');
  }
}
```

Comparing to *PHPUnit*, *counit* could make your test cases faster. Here is a comparison when running the same test suite using *PHPUnit* and *counit* for a real project. In the test suite, many tests make calls to method *\\Deminy\\Counit\\Counit::sleep()* to wait something to happen (e.g., wait data to expire).

    \# of Tests \# of Assertions Time to Finish   **counit (without Swoole), or PHPUnit** 44 1148 9 minutes and 18 seconds   **counit (with Swoole enabled)** 19 seconds Installation
============

[](#installation)

The package can be installed using *Composer*:

```
composer require deminy/counit --dev
```

Or, in your *composer.json* file, make sure to have package *deminy/counit* included:

```
{
  "require-dev": {
    "deminy/counit": "~1.0.0"
  }
}
```

Please pick the *counit* version matching the version of *PHPUnit* used in your project:

counitPHPUnitPHP~1.0.0~13.0&gt;= 8.4.1~1.0.0~12.5.24&gt;= 8.3~0.2.0~8.0, ~9.0&gt;= 7.2Use "counit" in Your Project
============================

[](#use-counit-in-your-project)

- 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):
    - **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.
    - **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()*.
- Use the binary executable *./vendor/bin/counit* instead of *./vendor/bin/phpunit* when running unit tests.
- Have the Swoole extension installed. If not installed, *counit* will work exactly same as *PHPUnit* (in blocking mode).
- Optional steps:
    - 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.

Examples
========

[](#examples)

Folder [./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 have following time-related tests included:

- Test slow HTTP requests.
- Test long-running MySQL queries.
- Test data expiration in Redis.
- Test *sleep()* function calls in PHP.

Setup Test Environment
----------------------

[](#setup-test-environment)

To run the sample tests, please start the Docker containers and install Composer packages first:

```
docker compose up -d
docker compose exec -ti swoole composer install -n
```

There are five containers started: a PHP container, a Swoole container, a Redis container, a MySQL container, and a web server. The PHP container doesn't have the Swoole extension installed, while the Swoole container has it installed and enabled.

As said previously, test cases can be written in the same way as those for *PHPUnit*. However, to run time/IO related tests faster with *counit*, we need to make some adjustments when writing those test cases; these adjustments can be made in two different styles.

The "global" Style (recommended)
--------------------------------

[](#the-global-style-recommended)

In this style, each test case runs in a separate coroutine automatically.

For test cases written in this style, the only change to make on your existing test cases is to use class *Deminy\\Counit\\TestCase* instead of *PHPUnit\\Framework\\TestCase* as the base class.

A typical test case of the global style looks like this:

```
use Deminy\Counit\TestCase; // Here is the only change made for counit, comparing to test cases for PHPUnit.

class SleepTest extends TestCase
{
  public function testSleep(): void
  {
    $startTime = time();
    sleep(3);
    $endTime = time();

    self::assertEqualsWithDelta(3, ($endTime - $startTime), 1, 'The sleep() function call takes about 3 seconds to finish.');
  }
}
```

When customized method *setUpBeforeClass()* and *tearDownAfterClass()* are defined in the test cases, please make sure to call their parent methods accordingly in these customized methods.

The total # of assertions reported at the end of a run matches *PHPUnit* exactly. What can still differ is which test an individual assertion is attributed to: an assertion performed after a sleep()/IO yield may be counted against whichever test happened to be running at that moment, so the per-test counts shown in verbose/TestDox-style output are not always exact. See [Additional Notes](#additional-notes).

To 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").

The "case by case" Style
------------------------

[](#the-case-by-case-style)

In this style, you make changes directly on a test case to make it work asynchronously.

For test cases written in this style, we need to use class *Deminy\\Counit\\Counit* accordingly in the test cases where we need to wait for PHP execution or to perform IO operations. Typically, following method calls will be used:

- Use method *Deminy\\Counit\\Counit::create()* to wrap the test case.
- Use method *Deminy\\Counit\\Counit::sleep()* instead of the PHP function *sleep()* to wait for PHP execution. You will need some knowledge on Swoole if you want to make other IO related tests run asynchronously.

A typical test case of the case-by-case style looks like this:

```
use Deminy\Counit\Counit;
use PHPUnit\Framework\TestCase;

class SleepTest extends TestCase
{
  public function testSleep(): void
  {
    Counit::create(function () { // To create a new coroutine manually to run the test case.
      $startTime = time();
      Counit::sleep(3); // Call this method instead of PHP function sleep().
      $endTime = time();

      self::assertEqualsWithDelta(3, ($endTime - $startTime), 1, 'The sleep() function call takes about 3 seconds to finish.');
    });
  }
}
```

In case you need to suppress warning message "This test did not perform any assertions" or to make the number of assertions match, you can include a 2nd parameter when creating the new coroutine:

```
use Deminy\Counit\Counit;
use PHPUnit\Framework\TestCase;

class SleepTest extends TestCase
{
  public function testSleep(): void
  {
    Counit::create( // To create a new coroutine manually to run the test case.
      function () {
        $startTime = time();
        Counit::sleep(3); // Call this method instead of PHP function sleep().
        $endTime = time();

        self::assertEqualsWithDelta(3, ($endTime - $startTime), 1, 'The sleep() function call takes about 3 seconds to finish.');
      },
      1 // Optional. To suppress warning message "This test did not perform any assertions", and to make the counters match.
    );
  }
}
```

To 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").

Comparisons
-----------

[](#comparisons)

Here we will run the tests under different environments, with or without Swoole.

`#1` Run the test suites using *PHPUnit*:

```
# To run test suite "global":
docker compose exec -ti php    ./vendor/bin/phpunit --testsuite global
# or,
docker compose exec -ti swoole ./vendor/bin/phpunit --testsuite global

# To run test suite "case-by-case":
docker compose exec -ti php    ./vendor/bin/phpunit --testsuite case-by-case
# or,
docker compose exec -ti swoole ./vendor/bin/phpunit --testsuite case-by-case
```

`#2` Run the test suites using *counit* (without Swoole):

```
# To run test suite "global":
docker compose exec -ti php    ./counit --testsuite global

# To run test suite "case-by-case":
docker compose exec -ti php    ./counit --testsuite case-by-case
```

`#3` Run the test suites using *counit* (with extension Swoole enabled):

```
# To run test suite "global":
docker compose exec -ti swoole ./counit --testsuite global

# To run test suite "case-by-case":
docker compose exec -ti swoole ./counit --testsuite case-by-case
```

The first two sets of commands take about same amount of time to finish. The last set of commands uses *counit* and runs in the Swoole container (where the Swoole extension is enabled); thus it's faster than the others:

    Style \# of Tests \# of Assertions Time to Finish   **counit (without Swoole), or PHPUnit** global 16 24 48 seconds   case by case 48 seconds   **counit (with Swoole enabled)** global 7 seconds   case by case 7 seconds Additional Notes
================

[](#additional-notes)

Since this package allows running multiple tests simultaneously, we should not use same resources in different tests; otherwise, racing conditions could happen. For example, if multiple tests use the same Redis key, some of them could fail occasionally. In this case, we should use different Redis keys in different test cases. Method *\\Deminy\\Counit\\Helper::getNewKey()* and *\\Deminy\\Counit\\Helper::getNewKeys()* can be used to generate random and unique test keys.

The package works best for tests that have function call *sleep()* in use; It can also help to run some IO related tests faster, with limitations apply. Here is a list of limitations of this package:

- The package makes tests running faster by performing time/IO operations simultaneously. For functions/extensions that work in blocking mode only, this package can't make their function calls faster. Here are some extensions that work in blocking mode only: *MongoDB*, *Couchbase*, and some ODBC drivers.
- The package doesn't work exactly the same as when running under *PHPUnit*:
    - 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*. When that happens, *counit* reports the failure at the end of the run and exits with a non-zero code, so the most reliable way to check if all test cases have passed or not is to check the exit code of *counit*.
    - The total # of assertions reported at the end of a run matches *PHPUnit*, but per-test assertion counts (as shown in verbose/TestDox-style output) may be attributed to a different test than the one that performed them.
    - Some exceptions/errors are not handled/reported the same.
- Tests using PHPUnit's process isolation (attributes *\#\[RunInSeparateProcess\]* and *\#\[RunTestsInSeparateProcesses\]*, or option `--process-isolation`) behave and are counted exactly as under *PHPUnit*, but they gain nothing from this package: *PHPUnit* runs each of them in a separate child process, which is plain non-coroutine PHP, so the test body there runs in blocking mode. Worse, such a test blocks the entire run for as long as its child process takes, since no other coroutine can make progress meanwhile. Mixing a few isolated tests into a suite is fine; running a whole suite under `--process-isolation` defeats the purpose of *counit*.

Local Development
=================

[](#local-development)

Docker images used to run the sample tests are built locally via `docker-compose.yml`, which builds the `php` and `swoole` services from the Dockerfiles under `./dockerfiles`. See [Setup Test Environment](#setup-test-environment).

Alternatives
============

[](#alternatives)

This package allows to use Swoole to run multiple time/IO related tests without multiprocessing, which means all tests can run within a single PHP process. To understand how exactly it works, I'd recommend checking this free online talk: [CSP Programming in PHP](https://nomadphp.com/video/306/csp-programming-in-php) (and here are the [slides](http://talks.deminy.in/csp.html)).

In the PHP ecosystem, there are other options to run unit tests in parallel, most end up using multiprocessing:

- Process isolation in PHPUnit. This allows to run tests in separate PHP processes.
- Package [brianium/paratest](https://github.com/paratestphp/paratest)
- Package [pestphp/pest](https://pestphp.com)

TODOs
=====

[](#todos)

- Better integration with *PHPUnit*.
    - Deal with attribute *\#\[DoesNotPerformAssertions\]* in the global style.
    - Make per-test # of assertions (not just the run total) consistent with the one reported from *PHPUnit*.
- Better error/exception handling.

License
=======

[](#license)

MIT license.

###  Health Score

53

—

FairBetter than 96% of packages

Maintenance95

Actively maintained with recent releases

Popularity33

Limited adoption so far

Community14

Small or concentrated contributor base

Maturity58

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 98.6% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~177 days

Recently: every ~1 days

Total

11

Last Release

22d ago

Major Versions

0.3.x-dev → 1.0.02026-07-22

0.2.2 → 1.0.12026-07-22

PHP version history (2 changes)0.1.0PHP &gt;=7.2

1.0.0PHP &gt;=8.4.1

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/865547?v=4)[Demin Yin](/maintainers/deminy)[@deminy](https://github.com/deminy)

---

Top Contributors

[![deminy](https://avatars.githubusercontent.com/u/865547?v=4)](https://github.com/deminy "deminy (69 commits)")[![claude](https://avatars.githubusercontent.com/u/81847?v=4)](https://github.com/claude "claude (1 commits)")

---

Tags

phpunitswooleunit-testingphpunitswoole

### Embed Badge

![Health badge](/badges/deminy-counit/health.svg)

```
[![Health](https://phpackages.com/badges/deminy-counit/health.svg)](https://phpackages.com/packages/deminy-counit)
```

###  Alternatives

[brianium/paratest

Parallel testing for PHP

2.5k142.8M1.1k](/packages/brianium-paratest)[drupal/core-dev

require-dev dependencies from drupal/drupal; use in addition to drupal/core-recommended to run tests from drupal/core.

2023.2M367](/packages/drupal-core-dev)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
