Unit Tests

Table of contents

  1. Overview
  2. Creating Tests
  3. Running Tests
  4. Testing Configuration
  5. Simulating Controller Output
  6. ApplicationTestCase Assertions
  7. PHPUnit Assertions

1. Overview Table of Contents

The Chappy.php framework has support for unit testing.


2. Creating Tests Table of Contents

You can make your own PHPUnit test class by running the following command:

php console make:test ${testName}

By default the make:test places the file under the unit test suite. To create a feature test run:

php console make:test ${testName} --feature

This version of the PHPUnit test class adds support for migrations and database seeding.


3. Running Tests Table of Contents

Run all available tests.

php console test

Run Tests By File Run all tests in a file that exists within the feature, unit, or both test suites.

php console test ${fileName}

Run A Particular Test Run a specific test in a file.

php console test ${fileName}::${functionName}

If you have the same function in a class with the same name inside both test suites only the one found within the unit test suite will be executed.

Run A Test Suite Run all test within a particular test suite by adding the --unit and/or --feature flags.

Run Specific Test File Within A Suite You can run all test within a specific test file for an individual suite by specifying the file name and adding the --unit and/or --feature flags.


4. Testing Configuration Table of Contents

The Chappy.php framework allows you to run your unit and feature tests against SQLite (in-memory) or MySQL, depending on your project’s requirements.

This gives you flexibility for:

  • ✅ Fast, isolated testing with SQLite
  • ✅ Full-database compatibility testing with MySQL

🧪 SQLite (In-Memory) for Fast Testing

For quick and isolated test runs, SQLite is ideal. It requires no setup and runs entirely in memory.

Configure PHPUnit to use SQLite

Update your phpunit.xml and make sure .env.testing does not exist in your project root:

<env name="APP_ENV" value="testing"/>
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>
...

To toggle on or off refresh, migrations, and seeding uncomment out the following:

```xml
...
<!-- Feature test configuration -->
<!-- <env name="DB_REFRESH" value="true"/> -->
<!-- <env name="DB_MIGRATE" value="true"/> -->
<!-- <env name="DB_SEED" value="true"/> -->

Benefits

  • No external service needed
  • Fastest test execution
  • Great for CI pipelines

🐬 MySQL for Real-World Compatibility To test real-world behavior like foreign key constraints or strict SQL modes (ONLY_FULL_GROUP_BY), use MySQL.

Configure PHPUnit to use MySQL by entering the required information in the MySQL/MariaDB section. Leave the SQLite information commented out.

<php>
    <env name="APP_ENV" value="testing"/>
    <!-- In memory SQLite config -->
    <!-- <env name="DB_CONNECTION" value="sqlite"/>
    <env name="DB_DATABASE" value=":memory:"/> -->

    <!-- MySQL/MariaDB test DB config -->
    <env name="DB_CONNECTION" value="mysql_testing"/>
    <env name="DB_HOST" value="127.0.0.1"/>
    <env name="DB_PORT" value="3306"/>
    <env name="DB_DATABASE" value=""/>
    <env name="DB_USERNAME" value=""/>
    <env name="DB_PASSWORD" value=""/>

    <!-- Feature test configuration -->
    <!-- <env name="DB_REFRESH" value="true"/> -->
    <!-- <env name="DB_MIGRATE" value="true"/> -->
    <!-- <env name="DB_SEED" value="true"/> -->
</php>

You can keep everything commented out except for the APP_ENV line and make a copy of the .env.testing.sample file and rename it to .env.testing and configure that file.

DB_CONNECTION=mysql_testing
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=
DB_USERNAME=
DB_PASSWORD=
DB_REFRESH=true
DB_MIGRATE=true
DB_SEED=true

✅ Requirements

  • MySQL test database must exist (e.g., chappy_test)
  • Test user must have privileges to create/drop tables


5. Simulating Controller Output Table of Contents

The Chappy.php framework provides a convenient test helper method named controllerOutput() that allows you to simulate controller behavior inside unit or feature tests, similar to how routes behave when accessed via a browser.

This method is available within ApplicationTestCase and is ideal for verifying the output of controller actions.

🔧 Syntax

$this->controllerOutput(string $controller, string $action, array $params = []): string

Description of arguments:

  • controller — The lowercase controller slug (e.g. 'home', 'user')
  • action — The lowercase action name without the Action suffix (e.g. 'index', 'details')
  • params — Optional array of route parameters (like path segments)

🧪 Example Usage ✅ Simulate a basic route like /home/index

$html = $this->controllerOutput('home', 'index');
$this->assertStringContainsString('Welcome to Chappy.php', $html);

✅ Simulate a route like /admindashboard/details/1

public function test_feature_example_1() {
    $user = Users::findById(1);

    $username = $user->username;
    $output = $this->controllerOutput('admindashboard', 'details', ['1']);

    $this->assertStringContainsString('Details for '.$username, $output);
}

✅ Behavior Details

  • The controller is instantiated using the same logic as the router (App\Controllers\{Name}Controller)
  • The action is called with any extra parameters (mimicking a parsed URL like /user/edit/3)
  • Output from the controller is captured using ob_start() and returned as a string
  • This method is ideal for asserting against full HTML responses or checking content rendered by views

⚠️ Note on Test Data Since this method operates inside your test environment, ensure the required database records (e.g. users, posts) exist either by:

  • Calling a seeder (e.g. DatabaseSeeder)
  • Manually inserting records
  • Otherwise, calls like Users::findById($id) may return null, causing your view or controller to throw exceptions during the test.


6. ApplicationTestCase Assertions Table of Contents

This framework’s test infrastructure provides convenient assertion helpers to validate the state of the database during tests. These assertions are especially useful in integration and feature tests that interact with the database.

The following support assertions are available in the ApplicationTestCase base class:

🔍 assertDatabaseHas()

$this->assertDatabaseHas(string $table, array $data, string $message = '');

Description: Asserts that a given row exists in the specified database table. This is useful for verifying that a model or query correctly created or updated a record.

Parameters:

Name Type Description
$table string The name of the database table to search.
$data array Key-value pairs representing column and expected value.
$message string (Optional) Custom error message if the assertion fails.

Example:

$this->assertDatabaseHas('users', [
    'email' => 'jane@example.com',
    'lname' => 'Doe',
]);

If the record is not found, the test will fail and display an informative error message.


🚫 assertDatabaseMissing()

$this->assertDatabaseMissing(string $table, array $data, string $message = '');

Description: Asserts that no row exists in the given table with the provided data. Useful for checking deletions, failed inserts, or rollback behavior.

Parameters:

Name Type Description
$table string The name of the database table to search.
$data array Key-value pairs representing column and value to search for.
$message string (Optional) Custom error message if the assertion fails.

Example:

$this->assertDatabaseMissing('orders', [
    'user_id' => 1,
    'status' => 'canceled',
]);

This will fail if a record with the specified conditions exists in the table.


🧪 Testing View Variables with controllerOutput() and assertViewContains() This section describes how to test whether a controller assigns the expected properties to the View object using controllerOutput() and the assertViewContains() assertion method.

✅ Overview In this framework, controller actions assign data to views via dynamic properties:

$this->view->user = $user;
$this->view->render('admindashboard/details');

To test whether specific view variables are set correctly during a controller action, you can use:

  • controllerOutput() – simulates dispatching a controller and stores output
  • logViewForTesting() – captures the view during rendering
  • assertViewContains() – checks whether a specific view property exists (and optionally, its value)

🧩 Prerequisites Make sure the controller action includes:

$this->logViewForTesting($this->view);
$this->view->render('admindashboard/details');

📥 Example Controller Action

public function detailsAction($id): void {
    $user = Users::findById((int)$id);
    $profileImage = ProfileImages::findCurrentProfileImage($user->id);

    $this->view->profileImage = $profileImage;
    $this->view->user = $user;

    $this->logViewForTesting($this->view); // Required for testing
    $this->view->render('admindashboard/details');
}

🧪 Writing the Test

public function test_user_details_view_contains_user()
{
    static::controllerOutput('admindashboard', 'details', [1]);

    $this->assertViewContains('user');
    $this->assertViewContains('profileImage');
}

You can also verify the value if needed:

$this->assertViewContains('user', Users::findById(1));

🧠 How It Works

  • controllerOutput() simulates the route to AdmDashboardController@details
  • The controller calls logViewForTesting(), which stores $this->view in a static test property
  • assertViewContains() retrieves the view object and verifies its dynamic properties


7. PHPUnit Assertions Table of Contents

PHPUnit provides a rich set of built-in assertions you can use in your tests. These are all supported out of the box in your test classes (like ApplicationTestCase) because they extend PHPUnit\Framework\TestCase.

Here’s a categorized list of commonly used PHPUnit assertions (as of PHPUnit 11.x):

✅ Equality & Identity

Assertion Description
assertEquals($expected, $actual) Checks if two values are equal (==)
assertSame($expected, $actual) Checks if two values are identical (===)
assertNotEquals($expected, $actual) Asserts that two values are not equal
assertNotSame($expected, $actual) Asserts that two values are not identical


🚫 Null / Empty / Boolean

Assertion Description
assertNull($actual) Checks if a value is null
assertNotNull($actual) Checks if a value is not null
assertTrue($condition) Checks if condition is true
assertFalse($condition) Checks if condition is false
assertEmpty($actual) Checks if a variable is empty (e.g., [], "", null)
assertNotEmpty($actual) Checks if a variable is not empty


🧵 Type Assertions

Assertion Description
assertInstanceOf($expectedClass, $object) Asserts object is an instance of a class
assertIsArray($actual) Asserts variable is an array
assertIsString($actual) Asserts variable is a string
assertIsInt($actual) Asserts variable is an integer
assertIsBool($actual) Asserts variable is a boolean
assertIsFloat($actual) Asserts variable is a float
assertIsCallable($actual) Asserts variable is callable
assertIsObject($actual) Asserts variable is an object
assertIsScalar($actual) Asserts variable is a scalar (int, float, string, or bool)


🧮 Array / Count / Contains

Assertion Description
assertCount($expectedCount, $array) Asserts array has expected number of elements
assertContains($needle, $haystack) Asserts that a value exists in array or string
assertArrayHasKey($key, $array) Asserts key exists in an array
assertArrayNotHasKey($key, $array) Asserts key does not exist in array
assertContainsOnly($type, $array) Asserts array contains only values of a certain type


⚠️ Exception / Error / Output

Assertion Description
expectException(Exception::class) Expects an exception to be thrown
expectExceptionMessage('message') Expects exception message to match
expectExceptionCode(123) Expects exception code to match
expectOutputString('expected output') Asserts output matches string
assertStringContainsString($needle, $haystack) Asserts that a string contains another string


⏱️ Performance / Custom

Assertion Description
assertLessThan($expected, $actual) Asserts that actual is less than expected
assertGreaterThan($expected, $actual) Asserts that actual is greater than expected
assertMatchesRegularExpression($pattern, $string) Asserts that a string matches regex
assertThat($value, $constraint) Use custom constraints (advanced)