Skip to content

How to write unit tests

John R. D'Orazio edited this page Dec 28, 2024 · 6 revisions

N.B. This page has been superseded in the latest development version, there is now a new approach to creating unit tests, using JSON definitions which can be created from a user interface.

PHPUnit conventions

Following the PHPUnit convention, unit tests must end in *Test.php. So in order to write a new unit test, you must make a pull request for a new file in the tests folder, in the LitCal API repo, following this naming convention. You can see the examples already present for an idea.

The backend orchestrator for unit tests is https://github.com/Liturgical-Calendar/LiturgicalCalendarAPI/blob/development/LitCalHealth.php . This class will automatically pick up on any unit tests defined or created in the tests folder, and make sure they are available for any requests coming from the frontend (by including them within itself). This class will also make sure that each unit test class will have access to the necessary Calendar resource that it is testing against.

How to write a Unit Test

Each Unit Test should be a class that extends PHPUnit\Framework\TestCase. The name of the class will be the identifier for the unit test, and will show in the frontend as the title of an accordion item within the section EXECUTE UNIT TESTS FOR SPECIFIC OCCURRENCES.

Each Unit Test should contain the following class constants:

  • DESCRIPTION : A string that describes the context or the purpose of the unit test. This string will be displayed in the frontend as the tooltip for the info icon to the right of the title of the unit test.
  • TEST_TYPE : should have one of three string values:
    • exactCorrespondence: this means that the test or tests performed in the test method will only be performed against the years defined in the ExpectedValues class constant, and the only assertions for this test will be found in the Assertion class constant with a one to one correspondence to the years defined in the ExpectedValues class constant. The frontend will only show tests for the years defined in the ExpectedValues class constant.
    • exactCorrespondenceSince: this means that the test or tests performed in the test method will not only test against the years defined in the ExpectedValues class constant, but will also handle any year prior to the first year defined in the ExpectedValues class constant. The years prior to the first year defined in the ExpectedValues class constant will have a single assertion in the Assertions class constant whose key is "before". This kind of unit test will affirm assertions for years defined in the ExpectedValues class constant, and negate the existence of the festivity in question for any years prior. This kind of test is useful for festivities that are created based on Decrees of the Congregation for Divine Worship, starting from a given year; such festivities should not exist in Calendar years prior to the Decree of the Congregation for Divine Worship. The frontend will show tests not only for the years defined in the ExpectedValues class constant, but also for at least 3 years prior to the first year defined in the ExpectedValues class constant.
    • variableCorrespondence: this means that different tests with different assertions will be performed depending on the year that the test is being performed against. Definite expected date values for when the festivity should exist should be defined in the ExpectedValues class constant; any other years that might be tested with assertions of their own (usually for non existence of the festivity in a given year) can be defined in the Assertions class constant. The frontend will show tests for years both in the ExpectedValues class constant and in the Assertions class constant.
  • ExpectedValues: an associative array, where the keys are years to be tested, and the values are generally Unix timestamps that are expected for the festivity being tested in the year being tested.
  • Assertions: an associative array, where the keys are years that are either defined in the ExpectedValues class constant, or extra years that are to be tested (generally for non existence of the festivity in the given year in the given calendar), and the values are strings that represent the assertion being made for test performed on the corresponding year for the given calendar. Other than years as keys, one more key is permitted: "before", which can be used in the case of a TEST_TYPE class constant with a value of exactCorrespondenceSince; in this case, the value corresponding to the "before" key should be a string with the assertion being made for any years prior to the first year defined in the ExpectedValues class constant. The assertion string values will show in the frontend as a tooltip for the info icon to the right of the single test valid test being performed for a given year.

All Unit test classes should also define a public static object $testObject;. This will be populated with the Calendar resource being tested against. In the LitCal project, each Calendar, whether the Universal Calendar, or a national calendar, or a diocesan calendar, together with the year for the calendar, is a resource of its own (whether a JSON resource, or on ICS resource). So the Universal Calendar for 1999 is one resource, and the Universal Calendar for 2000 is another resource. The national calendar for the United States for 1999 is yet another resource, and the national calendar for the United States for the year 2000 is yet another resouce. Since each resource must be tested separately, the resource being tested must be made available to the Unit Test class: this is taken care of by the LitCalHealth.php orchestrator, and the resource is made available in the public static object $testObject. The frontend takes care of making Calendars to choose from available for testing (these are picked up by the frontend at the LitCalMetadata.php endpoint).

All Unit test classes should implement a single public method test() with a return value of type bool|object:

public function test() : bool|object {}

Even though the PHP Unit conventions allow for multiple methods as long as they begin with test*, in the specific case of the LitCal project's unit testing, we will be implementing only a single method named test() (at least for the time being). The backend orchestrator LitCalHealth.php will instantiate the Unit Test class and run the single test() method, and report results back to the frontend.

How the test() method should be written

The public test() method within the Unit Test class will be constructed in a slightly different manner, depending on the TEST_TYPE. You can refer to the examples already created to get an idea:

Basically, if the test is performed correctly and passes, it should return bool true. If the test fails, it should return an object. If it returns a bool false, this means that the test was not performed correctly, and neither passed nor failed (currently, the LitCalHealth.php orchestrator doesn't handle this, and will simply ignore a bool false, which will result in the frontend never receiving information about the test passing or failing. Simply, if we want a result in the frontend, we need to make sure the test is performed correctly and either returns bool true or an object).

In case of a test failure, the object returned should have the following properties:

  • type: can have a values of "success" or "error", which will indicate whether the test passed or failed. In this case, it will have a value of "error"
  • text: should be a string that explains why the test may have passed or failed. In this case, we are explaining why the test failed

In case the test passes (i.e. we return bool true in the Unit Test class test() method), the LitCalHealth.php orchestrator will take care of returning an object to the frontend with the following properties:

$message = new stdClass();
$message->type = "success";
$message->text = "$Test passed for the Calendar $Calendar for the year $Year";
$message->classes = ".$Test.year-{$Year}.test-valid";
$message->test = $Test;

In case the test failed, the LitCalHealth.php orchestrator will take care of adding the properties classes and test to the object that is returned to the frontend, the single Unit Test should never have to worry about these two properties whether the test passes or fails.

The classes property will let the frontend know which UI elements to update based on the test results received.

exactCorrespondence type

We should only have to test against years defined in the ExpectedValues class constant. Therefore, we check if the resource year is within the years defined in the ExpectedValues class constant, and if not we return an out of bounds error.

We can also define the $expectedValue based on the year the test is being performed against (which can be found within the $testObject resource), and consequently the $actualValue and the corresponding $assertion.

if( array_key_exists( self::$testObject->Settings->Year, self::ExpectedValues ) ) {
    $expectedValue = self::ExpectedValues[ self::$testObject->Settings->Year ];
    $actualValue =  self::$testObject->LitCal->NativityJohnBaptist->date;
    $assertion = self::Assertions[ self::$testObject->Settings->Year ];
    /** perform tests */
}
else {
    $message = new stdClass();
    $message->type = "error";
    $message->text = get_class($this) . " out of bounds: this test only supports calendar years [ " . implode(', ', array_keys(self::ExpectedValues) ) . " ]";
    return $message;
}

exactCorrespondenceSince type

We should first determine the first year from which the expectedValue tests are to be performed.

Then we should check if the year being tested against (as found within the $testObject resource) is lower than this boundary: in this case we will probably be testing for non-existence of the festivity in the Calendar resource. And our assertion will be that of the self::Assertions["before"] property.

If instead the year being tested is within the range of years defined in the ExpectedValues class constant, we can define the $expectedValue based on the year the test is being performed against, and consequently the $actualValue and the corresponding $assertion.

If the year being tested against falls in neither of the above two categories, we return an out of bounds error.

$sinceYear = array_keys( self::ExpectedValues )[0];
if( self::$testObject->Settings->Year < $sinceYear ) {
    $assertion = self::Assertions["before"];
    /** perform test of non existence of festivity here */
}
else if( array_key_exists( self::$testObject->Settings->Year, self::ExpectedValues ) ) {
    $expectedValue = self::ExpectedValues[ self::$testObject->Settings->Year ];
    $actualValue = self::$testObject->LitCal->MaryMotherChurch->date;
    $assertion = self::Assertions[ self::$testObject->Settings->Year ];
    /** perform tests */
}
else {
    $message = new stdClass();
    $message->type = "error";
    $message->text = get_class($this) . " out of bounds: this test only supports calendar years [ " . implode(', ', array_keys(self::ExpectedValues) ) . " ]";
    return $message;
}

variableCorrespondence type

First we check if the year being tested against (as found in the $testObject resource) is within the range of years defined in the ExpectedValues class constant: if so we can define the $expectedValue based on the year the test is being performed against, and consequently the $actualValue and the corresponding $assertion.

If instead the year being tested against falls within the years defined directly in the Assertions class constant, we perform alternate tests with the corresponding assertion.

If the year being tested against falls in neither of the above two categories, we return an out of bounds error.

if( array_key_exists( self::$testObject->Settings->Year, self::ExpectedValues ) ) {
    $expectedValue = self::ExpectedValues[ self::$testObject->Settings->Year ];
    $actualValue = self::$testObject->LitCal->StJaneFrancesDeChantal->date;
    $assertion = self::Assertions[ self::$testObject->Settings->Year ];
    /** perform tests */
} else if( array_key_exists( self::$testObject->Settings->Year, self::Assertions ) ) {
    $assertion = self::Assertions[ self::$testObject->Settings->Year ];
    /** perform alternate tests (probably non-existence of the festivity in given years) */
}
else {
    $message = new stdClass();
    $message->type = "error";
    $message->text = get_class($this) . " out of bounds: this test only supports calendar years [ " . implode(', ', array_keys(self::Assertions) ) . " ]";
    return $message;
}