Test Suites

Organize TestCases and expose functionality for all of the associated TestCases in one place. A fundamental concept for integration testing capabilities provided by AsyncUnit!

A test suite is a way to organize TestCase implementations together. A client interacting with the framework could then allow you to filter which tests get run by only running a specific test suite. However, a TestSuite can be much more than that as it is a first-class citizen and a fundamental aspect of how the framework operates.

In our example we're going to assume the code under test has a heavy initialization cost or is not something that works well with the existing Amp PHPUnit wrapper. We're going to implement a TestSuite that will instantiate just one of these Heavy objects for all of the tests associated to it. Additionally, the TestSuite will perform some operation before each test to ensure that the object is properly setup.

Defining a TestSuite

The first thing is define the TestSuite.

<?php

use Cspray\Labrador\AsyncUnit\TestSuite;
use Cspray\Labrador\AsyncUnit\Attribute\BeforeAll;
use Cspray\Labrador\AsyncUnit\Attribute\BeforeEachTest;
use Generator;

class HeavyTestSuite extends TestSuite {

   #[BeforeAll]
   public function createHeavy() : Generator {
      $heavy = yield HeavyFactory::create();
      $this->set('heavy', $heavy);
   }   
   
   #[BeforeEachTest]
   public function runHeavy() : Generator {
      yield $this-get('heavy')->doSomethingHeavy();
   }
   
   #[AfterEachTest]
   public function freeHeavy() : Generator {
      yield $this->get('heavy')->free();
      $this->set('heavy', null);
   }

}

For those familiar with unit testing it should be clear what's happening in the code above. Let's take a step-by-step look at what's happening.

The BeforeAll Hook

#[BeforeAl]
public function createHeavy() : void {
    $this->set('heavy', new Heavy());
}

The "TestCase Hooks" article introduces the #[BeforeAll] Attribute. In that scenario the Attribute was used on a TestCase where this time it is declared on a TestSuite. There's a couple things to note about this hook though; it isn't a static method and it is run before all associated TestCase implementations are executed. A TestSuite lives higher up the test hierarchy and therefore so does its hooks!

This hook also set some arbitrary data; in this case an instance of Heavy, that we make use of later on. This use case, instantiating objects with a heavy initialization cost for later use, is the primary reason for the existence of the test suite functionality! A single TestSuite object is created for all associated tests so that this type of state can be shared.

The BeforeEachTest Hook

#[BeforeEachTest]
public function runHeavy() : Generator {
   yield $this-get('heavy')->run();
}

This hook is not one covered in "TestCase Hooks"; because it is only available to the TestSuite type. Since a TestSuite lives higher up if we were to run BeforeEach it would only execute one time for each TestCase but this should run for each test. This hook allows doing just that. We simply get our previously created object and perform some operation on it.

The AfterEachTest Hook

#[AfterEachTest]
   public function freeHeavy() : Generator {
      yield $this->get('heavy')->free();
      $this->set('heavy', null);
   }

This hook is the parallel to BeforeEachTest and is run after each test is finished processing. This implementation simply frees up whatever resources were being used by Heavy and ensures that at some point the object gets garbage collected.

Declaring a TestSuite

Now that a TestSuite has been implemented the appropriate TestCase need to be associated to it. There's currently 2 ways in which to do this. Making a TestSuite the default or explicitly declaring on each TestCase what suite it belongs to.

Making a TestSuite the default

This will cause every single TestCase that does not explicitly declare a TestSuite to be associated to the class being annotated. There can only be 1 default suite at a time. Simply use the DefaultTestSuite Attribute.

use Cspray\Labrador\AsyncUnit\Attribute\DefaultTestSuite;

#[DefaultTestSuite]
class HeavyTestSuite extends TestSuite {

Declare on each TestCase

It is also possible to explicitly state which TestSuite a TestCase belongs to. In this case put the annotation on the TestCase and use the AttachToTestSuite Attribute. For example...

use Cspray\Labrador\AsyncUnit\Attribute\AttachToTestSuite;
use Cspray\Labrador\AsyncUnit\TestCase;

#[AttachToTestSuite(HeavyTestSuite::class)]
class SomeHeavyTestCase extends TestCase {}

There's currently a known limitation on the AttachToTestSuite Attribute that requires you to define the TestSuite using class constant notation. Defining your TestSuite with a literal string is not currently supported. Future improvements to the static analyzer will remediate this problem.

There's always a TestSuite

It is important to realize that even if there is no explicit TestSuite there is still an instance created and all of the TestCase and tests are associated to it. If the parser can't find an explicit TestSuite AsyncUnit will create an instance of ImplicitTestSuite to use instead. This implementation performs no operations and has no hooks defined.

Understand the Hook lifecycle

Before you implement your own TestSuite with custom hooks ensure the ramifications are well understood. With great power comes great responsiblity and implementing an explicit TestSuite, while sometimes necessary, also brings an additional level of complexity. Make sure you've read over the Hooks reference!

Last updated