Code Monkey home page Code Monkey logo

asynit's Introduction

asynit

Asynchronous (if library use fiber) testing library runner for HTTP / API and more...

Install

composer require --dev jolicode/asynit

Usage

Writing a test

Asynit will read PHP's classes to find available Test using the Asynit\Attribute\TestCase attribute. You need to create a test class in some directory, which will have the TestCase attribute of Asynit:

use Asynit\Attribute\TestCase;

#[TestCase]
class ApiTest
{
}

Then you can add some tests that will use the API of the TestCase class:

use Asynit\Attribute\Test;
use Asynit\Attribute\TestCase;

#[TestCase]
class ApiTest
{
    #[Test]
    public function my_test()
    {
        // do some test
    }
}

Note: All test methods should be prefixed by the test keyword or use the Asynit\Attribute\Test anotation. All others methods will not be executed automatically.

A test fail when an exception occurs during the test

Using assertion

Asynit provide trait to ease the writing of test. You can use the Asynit\AssertCaseTrait trait to use the assertion.

use Asynit\Attribute\Test;
use Asynit\Attribute\TestCase;

#[TestCase]
class ApiTest
{
    use Asynit\AssertCaseTrait;

    #[Test]
    public function my_test()
    {
        $this->assertSame('foo', 'foo');
    }
}

All assertions supported by PHPUnit are also supported by Asynit thanks to the bovigo-assert library. But you can use your own as long as it's throw an exception on failure.

Running the test

For running this test you will only need to use the PHP file provided by this project:

$ php vendor/bin/asynit path/to/the/file.php

If you have many test files, you can run Asynit with a directory

$ php vendor/bin/asynit path/to/the/directory

Using HTTP Client

Asynit provide an optional Asynit\HttpClient\HttpClientWebCaseTrait trait that you can use to make HTTP request. You will need to install amphp/http-client and nyholm/psr7 to use it.

use Asynit\Attribute\TestCase;
use Asynit\HttpClient\HttpClientWebCaseTrait;

#[TestCase]
class FunctionalHttpTests
{
    use HttpClientWebCaseTrait;

    public function testGet()
    {
        $response = $this->get('https//example.com');

        $this->assertStatusCode(200, $response);
    }
}

You can also use a more oriented API trait Asynit\HttpClient\HttpClientApiCaseTrait that will allow you to write test like this:

use Asynit\Attribute\TestCase;
use Asynit\HttpClient\HttpClientApiCaseTrait;

#[TestCase]
class FunctionalHttpTests
{
    use HttpClientApiCaseTrait;

    public function testGet()
    {
        $response = $this->get('https//example.com');

        $this->assertStatusCode(200, $response);
        $this->assertSame('bar', $response['foo']);
    }
}

Dependency between tests

Sometime a test may need a value from the result of another test, like an authentication token that need to be available for some requests (or a cookie defining the session).

Asynit provides a Depend attribute which allows you to specify that a test is dependent from another one.

So if you have 3 tests, A, B and C and you say that C depend on A; Test A and B will be run in parallel and once A is completed and successful, C will be run with the result from A.

Let's see an example:

use Asynit\Attribute\Depend;
use Asynit\Attribute\TestCase;
use Asynit\HttpClient\HttpClientApiCaseTrait;

#[TestCase]
class SecurityTest extends TestCase
{
    use HttpClientApiCaseTrait;

    public function testLogin()
    {
        $response = $this->post('/', ['username' => user, 'password' => 'test']);

        $this->assertStatusCode(200, $response);

        return $response->getBody()->getContents();
    }

    #[Depend("testLogin")]
    public function testAuthenticatedRequest(string $token)
    {
        $response = $this->get('/api', headers: ['X-Auth-Token' => $token]);

        $this->assertStatusCode(200, $response);
    }
}

Here testAuthenticatedRequest will only be run after testLogin has been completed. You can also use dependency between different test case. The previous test case is under the Application\ApiTest namespace and thus we can write another test case like this:

use Asynit\Attribute\Depend;
use Asynit\Attribute\TestCase;
use Asynit\HttpClient\HttpClientApiCaseTrait;

#[TestCase]
class PostTest
{
    #[Depend("Application\ApiTest\SecurityTest::testLogin")]
    public function testGet($token)
    {
        $response = $this->get('/posts', headers: ['X-Auth-Token' => $token]);

        $this->assertStatusCode(200, $response);
    }
}

Test Organization

It's really common to reuse this token in a lot of test, and maybe you don't need test when fetching the token. Asynit allow you to depend on any method of any class.

So you could write a TokenFetcherClass that will fetch the token and then use it in your test.

namespace App\Tests;

use Asynit\HttpClient\HttpClientApiCaseTrait;

class TokenFetcher
{
    use HttpClientApiCaseTrait;

    protected function fetchToken(string $email, string $password = 'password')
    {
        $payload = [
            'email' => $email,
            'password' => $password,
        ];

        $response = $this->post('/users/token', ['username' => 'user', 'password' => 'test']);

        return $response['token'];
    }
    
    protected function fetchUserToken()
    {
        return $this->fetchToken('[email protected]', 'password');
    }
}

Then in your test class you will be able to call this method:

namespace App\Tests;

use Asynit\Attribute\Depend;
use Asynit\Attribute\TestCase;
use Asynit\HttpClient\HttpClientApiCaseTrait;

#[TestCase]
class OrganizationTest
{
    use HttpClientApiCaseTrait;

    #[Depend("App\Tests\TokenFetcher::fetchUserToken")]
    public function test_api_method_with_token(string $token)
    {
        $response = $this->get('/api', headers: ['X-Auth-Token' => $token]);

        // ...
    }
}

As you may notice, the fetchUserToken method does not start with test. Thus by default this method will not be included in the test suite. But as it is a dependency of a test, it will be included as a regular test in the global test suite and will leverage the cache system.

asynit's People

Contributors

damienalexandre avatar joelwurtz avatar korbeil avatar lyrixx avatar mykiwi avatar pborreli avatar peter279k avatar pyrech avatar samnela avatar tlenclos avatar welcomattic avatar xavierlacot avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

asynit's Issues

TODO

Accept all method as a dependency even if it's not a test

My test suite has became big (23 files, 3500 LOC) and I would like to refactor it a bit.
All my tests extends a WebTestCase class.
I would like to put some shortcut / helper in it, but ATM it's not possible because the callback in @Depend should be a test. (For recall, a test is a callback that start with the test keywork)

IMHO, It could be any valid callback.

You could argue I can easily transform my callback to a test, but in this case asynit will run the same test each time a class extends WebTestCase. It's a waste of resource.

Do you think we could allow any callback in @Depend?

Add a way to run only one test file

Right now:

php bin/asynit src/OrganizationRoleAdminTest.php 

                                                                     
  [InvalidArgumentException]                                         
  The "src/OrganizationRoleAdminTest.php" directory does not exist.  
                                                                     

Fatal error if amp could not connect to target

If you write:

public function testFoobar()
{
    $response = yield $this->get('http://something-is-not-reachable');
}

you get this:

โœ• Argument 1 passed to Asynit\HttpClient\PromiseAdapter::reject() must be an instance of Http\Client\Exception, instance of Amp\Socket\ConnectException given, called in /PROJECT/tests/vendor/jolicode/asynit/src/HttpClient/PromiseAdapter.php on line 35
PHP Fatal error:  Uncaught TypeError: Argument 1 passed to Asynit\HttpClient\PromiseAdapter::reject() must be an instance of Http\Client\Exception, instance of Amp\Socket\ConnectException given, called in /PROJECT/tests/vendor/jolicode/asynit/src/HttpClient/PromiseAdapter.php on line 35 and defined in /PROJECT/tests/vendor/jolicode/asynit/src/HttpClient/PromiseAdapter.php:92
Stack trace:
#0 /PROJECT/tests/vendor/jolicode/asynit/src/HttpClient/PromiseAdapter.php(35): Asynit\HttpClient\PromiseAdapter->reject(Object(Amp\Socket\ConnectException))
#1 /PROJECT/tests/vendor/amphp/amp/lib/Internal/ResolutionQueue.php(51): Asynit\HttpClient\PromiseAdapter->Asynit\HttpClient\{closure}(Object(Amp\Socket\ConnectException), NULL)
#2 /PROJECT/tests/vendor/amphp/amp/lib/Failure.php(26): Amp\Internal\ResolutionQueue->__invoke(Object(Amp\Socket\ConnectException), NULL)
#3 /PROJECT/tests/vendor/amphp/amp/lib/Internal/Placeholder.php(118): Amp\Failure->onResolve(Object(Amp\Internal\ResolutionQueue))
#4 /PROJECT in /PROJECT/tests/vendor/jolicode/asynit/src/HttpClient/PromiseAdapter.php on line 92

[RFC] Switch to Amp/Artax Client

Goal of this change would be to allow a better API for writing test, instead of writing

$this->get('http://test.com')->shouldResolve(function ($response) {
     // Write test here
});

It would be written as

$response = yield $this->get('http://test.com');
// Write test here

WDYT ?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.