Tests


1Presentation

To avoid regressions during development, it is advisable to write automated tests. There are several test frameworks in PHP, the most commonly used being PHPUnit.

You can use PHPUnit directly to unit-test your objects. You may then need to create a special environment in which to run these tests.

Temma makes it easy to write integration tests. An integration test checks that the output of an action is correct, according to the parameters supplied as input.


2Installing PHPUnit

There are several ways to install PHPUnit (see documentation). Here, we'll download the PHAR archive, copy it into the project's bin/ directory and make it executable:

$ wget https://phar.phpunit.de/phpunit-10.phar -O bin/phpunit
$ chmod +x bin/phpunit

You can check that everything has gone smoothly by typing the following command:

$ bin/phpunit --version
PHPUnit 10.5.2 by Sebastian Bergmann and contributors.

3How it works

The \Temma\Web\Test object can be used to launch requests that will directly execute the actions of the requested controllers, without passing through an HTTP server. Depending on the type of request, several types of return are possible:

  • The output stream generated by the view. By default, this will be the HTML feed generated by the Smarty view, but it can also be a JSON stream, CSV file, RSS feed…
  • Template variables set by the controller and plugins.
  • An object of type \Temma\Web\Response, which retrieves precise information on execution (redirection, HTTP error code, view, template).
  • The dependency injection component (the "loader"), which contains all objects instantiated by Temma and application code.

Most of the time, template variables are retrieved to check that the application code has worked as intended.

Using the output stream can be useful for verifying non-regression on key elements of an HTML page.

The response object may be necessary for advanced verification.

Finally, the dependency injection component offers complete control over the result of query execution.


4Example controller

Let's imagine a controller used to display a list of articles and their contents (file controllers/Article.php):

/** Articles controller. */
class Articles extends \Temma\Web\Controller {
    /** Displays the list of articles. */
    public function list() {
        $this['articles'] = $this->_loader->ArticleDao->getList();
    }
    /** Shows the content of an article. */
    public function show(int $articleId) {
        $this['article'] = $this->_loader->ArticleDao->get($articleId);
        if (!$this['articles'])
            $this->_redirect('/article/list');
    }
}

For the list action, we have the following template (templates/articles/list.tpl file):

<html>
<body>
    <h1>Articles</h1>
    <ul>
        {foreach $articles as $article}
            <li>{$article.title|escape}</li>
        {/foreach}
    </ul>
</body>

For the show action, we have the following template (templates/articles/show.tpl file):

<html>
<body>
    <h1>{$article.title|escape}</h1>
    {$article.html}
</body>

5Creating a test

We're going to create an object containing two integration tests (one test per controller action) in the tests/ArticlesTest.php file:

<?php

class ArticlesTest extends \PHPUnit\Framework\TestCase {
    /** Temma test management object. */
    private \Temma\Web\Test $_test;

    /** Initialization. */
    public function setUp() : void {
        $this->_test = new \Temma\Web\Test();
    }
    /** Test of the 'list' action. */
    public function testList() {
        $data = $this->_test->execData('/articles/list');
        $this->assertIsArray($data['articles'] ?? null);
        $this->assertNotEmpty($data['articles']);
    }
    /** Test of the 'show' action. */
    public function testShow() {
        $data = $this->_test->execData('/articles/show/1');
        $this->assertEquals(1, ($data['article']['id'] ?? null));
    }
}
  • Line 3: The object name must have the suffix Test, and it must inherit from the \PHPUnit\Framework\TestCase object.
  • Lines 8 to 10: The setUp() method is called at object initialization, before tests are run. Here, we instantiate the test object supplied by Temma, which will be used later.
  • Lines 12 to 16: Test the list action.
    • Line 13: We launch a query on the /articles/list URL, which should normally display the list of articles, and retrieve an array containing all template variables.
    • Line 14: Check that the articles template variable exists and that it's an array.
    • Line 15: Check that this variable is not empty.
  • Lines 18 to 21: Test the show action.
    • Line 19: Launch a query on the /articles/show/1 URL, which should normally display the contents of an article, and retrieve an array containing all template variables.
    • Line 20: Check that the article template variable exists, that it contains an id key, and that the value associated with this key is 1.

6Command-line test execution

To run all the tests that have been written to the tests/ directory, simply execute the following command:

$ bin/phpunit --bootstrap tests/autoload.php tests

In return, you should get a display like this:

PHPUnit 10.5.2 by Sebastian Bergmann and contributors.

Runtime:       PHP 8.1.2-1ubuntu2.14

..                                                                  2 / 2 (100%)

Time: 00:00.003, Memory: 22.57 MB

OK (2 tests, 5 assertions)

It is also possible to run a single specific test:

$ bin/phpunit --bootstrap tests/autoload.php tests/ArticlesTest.php

7Calling up URLs

Four methods are available to initiate test execution:

  • execOutput() returns the view's output stream.
  • execData() returns template variables or the redirection string.
  • execResponse() returns a \Temma\Web\Response object.
  • execLoader() returns the dependency injection component created to execute the request.

These methods can all take four parameters:

  1. string $url: (mandatory) URL to call, starting with a slash character (/).
  2. string $httpMethod: (optional) The HTTP method of the request. By default, it's the GET method.
  3. ?array $data: (optional) Associative array containing the GET or POST parameters to be transmitted. The type (GET or POST) is determined by the $httpMethod parameter.
  4. ?array $cookies: (optional) Associative array containing cookies to be transmitted.

7.1Fetch the output stream

The execOutput() method is used to retrieve the output stream generated by the view. This method can return an empty string.

Example:

$html = $this->_test->execOutput('/articles/show/1');

// simple check
$this->assertStringContainsString('<h1>', $html);

// check with a regular expression
$this->assertMatchesRegularExpression('/<h1>.+<\/h1>/', $html);

7.2Fetch data

We saw above the execData() method, which returns template variables defined by the controller and plugins.

In fact, this method can return three different types of data:

  • An associative array, containing the template variables.
  • A character string, if a redirection has been defined. In this case, the redirection URL is returned.
  • The null value, if execution has been interrupted.

Example:

$data = $this->_test->execData('/articles/show/1');

// shouldn't be null
$this->assertNotNull($data, "Stopped processing.");

// if it's a redirect, the article doesn't exist
$this->assertIsNotString($data, "Unknown article.");

// check article identifier
$this->assertEquals(1, ($data['article']['id'] ?? null));

7.3Fetch the response

The execResponse() method retrieves the \Temma\Web\Response object created during query execution. This object offers the following getters:

  • getRedirection(): Returns the redirection string, or null if no redirection has been requested.
  • getRedirectionCode(): Returns the redirection code (301 or 302).
  • getHttpError(): Returns HTTP error code, or null.
  • getHttpCode(): Returns the HTTP code of the response (200 by default).
  • getView(): Returns the view name, or null if not defined.
  • getTemplatePrefix(): Returns the prefix that is appended to the beginning of template paths, or null.
  • getTemplate(): Returns the template path used, or null.
  • getData(): Returns an associative array containing template variables defined by the controller and plugins.

It is also possible to access a template variable directly using array-like syntax.

Example:

$response = $this->_test->execResponse('/articles/show/1');

// if it's a redirect, the article doesn't exist
$redir = $response->getRedirection();
$this->assertNull($redir, "Unknown article.");

// if the HTTP code is not 200, there's an error
$code = $response->getHttpCode();
$this->assertEquals(200, $code, "Processing error.");

// the view should be the Smarty view
$view = $response->getView();
$this->assertEquals('\Temma\Views\Smarty', $view, "Incorrect view.");

// we should retrieve the template variable containing the article
$this->assertEquals(1, ($response['article']['id'] ?? null));

7.4Using the dependency injection component

Each time a request is executed, a dependency injection component is created, containing the objects instantiated by Temma, as well as those used by your application code (if you've used the component).

The execLoader() method returns the \Temma\Base\Loader object created during query execution. This component contains at least the following objects:

  • config: The configuration object (type \Temma\Web\Config) generated from the etc/temma.php configuration file.
  • request: The request object (type \Temma\Web\Request) generated from the requested URL.
  • response: The response object (type \Temma\Web\Response), described in the documentation for the execResponse() method above.
  • session: The session management object (type \Temma\Base\Session), which in particular retrieves the session identifier, which is stored as a cookie.

7.5GET and POST parameters

To send GET or POST parameters, specify the method to be used as the second parameter, and pass an associative array containing the parameters as the third parameter.

Example:

$html = $this->_test->execOutput('/articles/create', 'POST', [
    'title' => "New article",
    'html'  => "<p>blah blah blah</p>",
]);

8Configuration

8.1Defining the configuration file

By default, Temma finds the path to the etc/temma.php configuration file. This allows you to use the same configuration as for your developments (same database, same plugins, etc.).

But sometimes it may be desirable to use a different configuration file, for example to connect to a specific test database, or to activate/deactivate certain plugins.
In this case, two parameters must be supplied when the \Temma\Web\Test object is created:

  1. ?string $appPath: Path to the project's root.
  2. ?string $configPath: Path to the configuration file.

Example:

$appPath = '/opt/my_projet';
$configPath = "$appPath/etc/temma-test.php";
$this->_test = new \Temma\Web\Test($appPath, $configPath);

8.2Defining the dependency injection component

When testing, you may want to "mock" some objects, i.e. force the use of a substitution object when the application code wants to call a given object. This can be useful for preventing certain processes from being carried out, such as connecting to an external service.

In this case, you need to prepare a dependency injection component to which you will have supplied the substitution object, giving it the name under which the initial object is called. Next, the component must be supplied as a parameter to the constructor of the \Temma\Web\Test object (third parameter, or loader named parameter).

Example:

// fake "ArticleDao" object
class MockArticleDao {
    /** Returns a fake list of articles. */
    public function getList() {
        return [
            ['id' => 1, 'title' => 'Title 1'],
            ['id' => 2, 'title' => 'Title 2'],
            ['id' => 3, 'title' => 'Title 3'],
        ];
    }
    /** Returns a fake article. */
    public function get(int $id) {
        return [
            'id'    => $id,
            'title' => "Title $id",
            'html'  => "<p>blah blah blah</p>",
        ];
    }
}

// creation of the loader object
$loader = new \Temma\Base\Loader([
    'ArticleDao' => new MockArticleDao(),
]);

// do a test using this loader
$this->_test = new \Temma\Web\Test(loader: $loader);

Please note that it is possible to override the dependency injection component in the etc/temma.php file, using the loader directive (see configuration documentation).

In this case, remember to use the object defined in the configuration, or another overloaded object that will behave similarly when used by the application code.


9Users management

9.1Sessions

User sessions are created automatically by Temma, through the registration of a session cookie on the browser. For automated tests, if you wish to test a sequence of pages requiring a session, you will need to retrieve the session identifier generated when the first page is called up, and then pass it on to subsequent pages.

$test = new \Temma\Web\Test();

// call the first page
$loader = $test->execLoader('/page1');

// get the session ID
$sessionId = $loader->session->getSessionId();
// create the cookies array
$cookies = ['TemmaSession' => $sessionId];

// call the next pages
$data = $test->execData('/page2', 'GET', null, $cookies);
...

The name of the session cookie ("TemmaSession" in the example above) must be the same as that specified in the sessionName variable in the etc/temma.php configuration file (see configuration documentation).


9.2Test user authentication

Your website may have some pages that are only accessible to logged-in users. To do this, you can use the Auth plugin/controller and the Auth attribute provided by Temma.

In this case, first authenticate a user, then pass the session cookie from page to page.

$appPath = '/opt/my_projet';
$configPath = "$appPath/etc/temma-test.php";
$test = new \Temma\Web\Test($appPath, $configPath);

// authentication
$loader = $test->execLoader('/auth/authentication', 'POST', [
    'email' => $email,
]);
$token = $loader->response['token'];
$sessionId = $loader->session->getSessionId();

// token validation
$cookies = ['TemmaSession' => $sessionId];
$loader = $test->execLoader("/auth/check/$token", 'GET', null, $cookies);

// test a page accessible only to authenticated users
$html = $test->execOutput("/account", 'GET', null, $cookies);
$this->assertStringContainsString('My account', $html);

For this authentication to work correctly, the configuration file must contain the robotCheckDisabled directive. It is also advisable to disable the sending of connection messages:

[
    'x-security' => [
        'auth' => [
            'robotCheckDisabled' => true
        ]
    ],
    'x-email' => [
        'disabled' => true
    ]
]