TYPO3 Extension testing
TDD and automated software tests is always a topic for developers.
How to definitely start testing your own TYPO3 extensions is a bit challenging to figure out. There is an official documentation, some old articles, some slides, some videos here and here (old) and a best-practice example extension but no complete (if only basic) detailed step-by-step guide. This article is intended to help you get started. 👍
Official TYPO3 Documentation
In this TYPO3 documentation article the why and most of the how is explained. Start by reading this article and then come back here to link the loose ends.
Getting started
I assume you work on a Linux server with composer.
So, first of all you need to add the typo3/testing-framework
(an alternative can be found at the end of the article) as dependency to your project.
composer require --dev typo3/testing-framework
This should add the following to the file:
"require-dev": {
"typo3/testing-framework": "*"
}
Create directory structure
As described in the documentation you create a directory Test
in your extension directory and add directories for the required type of test, e.g. Unit
or Functional
and below that even more directories reflecting the structure of your classes.
Should finally look something like this:
├── Classes
└── Test
├── Functional
│ ├── Fixtures
│ │ ├── DatabaseAssertions
│ │ │ └── importSomething.csv
│ │ └── defaultPages.xml
│ └── Service
│ └── ConcreteSomethingImportServiceTest.php
└── Unit
└── Service
└── AbstractSomethingServiceTest.php
So we test a class named AbstractSomethingService
with a Unit Test.
Unit Testing
The Unit Test is straight forward and needs not much setup.
Extend your test class (whose name is the class to test with a Test
postfix) from TYPO3\TestingFramework\Core\Unit\UnitTestCase
:
Let’s take a closer look at some of these lines.
protected $resetSingletonInstances = true;
If you use any code in your extension or from the TYPO3 core that uses GeneralUtility::makeInstance()
to create a Singleton you have to set this property to true
in order to reset (aka remove) all these instances before each test is run.
If you’re curious, this calls:
\TYPO3\CMS\Core\Utility\GeneralUtility::resetSingletonInstances
Next you add your test methods. You can either name them testThisAndThat
(with a test
prefix) or add the annotation @test
in PHPDoc.
Run the Unit tests
You can run the tests with:
./vendor/bin/phpunit path/to/extension/Test/Unit
and see it pass or fail
. 1 / 1 (100%)Time: 30 ms, Memory: 6.00 MBOK (1 test, 3 assertions)
Functional testing
Functional testing is a bit more complicated in setup as your code is likely to require a database.
You should definitely consider sqlite as test database. TYPO3 9+ comes with support for this database. Note that this creates publicly accessible new files like /typo3temp/var/test/functional-1c51a2b/test.sqlite
in your installation! So you shouldn’t run this on production 😉
First of all you’ll likely need some pages or other records in your database to work with.
You can add them with fixtures either in XML or CSV format. I used XML for example to insert a simple page of type sysfolder ( doctype 255
) into the test database before the test is run.
<?xml version="1.0" encoding="utf-8"?>
<dataset>
<pages>
<uid>1</uid>
<pid>0</pid>
<doktype>254</doktype>
<title>Import</title>
<deleted>0</deleted>
<perms_everybody>15</perms_everybody>
</pages>
</dataset>
Save this as Test/Functional/Fixtures/defaultPages.xml
.
Next we add our test class ConcreteSomethingImportServiceTest
that tests methods of the class ConcreteSomethingImportService
. This time we extend the class from TYPO3\TestingFramework\Core\Functional\FunctionalTestCase
:
Let’s take a closer look at some of these lines again.
protected $testExtensionsToLoad = [
'typo3conf/ext/my_extension'
];
This tells the system to load your extension while running the tests. Makes sense huh? 👏
protected function setUp(): void
{
parent::setUp();
// Core DataHandler requires a backend user
$this->setUpBackendUserFromFixture(1);
$this->importDataSet(
'EXT:my_extension/Test/Functional/Fixtures/defaultPages.xml'
);
}
This method is run before every test and it
- creates a backend user object in
$GLOBALS['BE_USER']
that’s required by e.g. theTYPO3\CMS\Core\DataHandling\DataHandler
class of the TYPO3 core - and imports the fixtures we talked about earlier into the test database
So now we have a page in the database and can import new records in our ConcreteSomethingImportService
class method we test here.
Run the Functional tests
This is a bit different than running the Unit tests. We have to specify information for the test database. As we use sqlite here (see above) it’s sufficient to set the environment variable for the database driver typo3DatabaseDriver
to pdo_sqlite
(default is mysqli
). If you want to use another kind of database here’s a complete list of environment variables you can set:
typo3DatabaseName
typo3DatabaseHost
typo3DatabaseUsername
typo3DatabasePassword
typo3DatabasePort
typo3DatabaseSocket
typo3DatabaseDriver
typo3DatabaseCharset
The second thing is you have to specify a bootstrap class for the functional tests. The TYPO3 testing framework already provides this.
The complete working command line looks like this:
$ typo3DatabaseDriver=pdo_sqlite ./vendor/bin/phpunit --bootstrap vendor/typo3/testing-framework/Resources/Core/Build/FunctionalTestsBootstrap.php path/to/extension/Test/Functional
This kind of tests takes a bit more time to run, but you should see it pass or fail again:
. 1 / 1 (100%)Time: 2.36 seconds, Memory: 28.00 MBOK (1 test, 1 assertion)
If the test fails you’ll get a nice table with expected and actual values compared to the fixtures:
There was 1 failure:1) Vendor\MyExtension\Test\Functional\Service\ConcreteSomethingImportServiceTest::importDoesItRightAssertion in data-set failed for "tx_myextension_domain_model_something:1":Fields |Assertion |Recordname |Example name |Xample name/app/vendor/typo3/testing-framework/Classes/Core/Functional/FunctionalTestCase.php:525/app/local/my_extension/Test/Functional/Service/ConcreteSomethingImportServiceTest.php:12FAILURES!Tests: 1, Assertions: 1, Failures: 1.
Alternative testing framework
There’s another testing framework for TYPO3 nimut/testing-framework
as a drop-in replacement for typo3/testing-framework
. Check out their github page for details.
I hope you learned something new from the article and you enjoy testing your TYPO3 extensions! 💪