How to get started with writing tests for contrib
Brent Gees
Slides + example module
Who am I?
Who am I?
- Brent
- Developer / Architect
- @brentgees
Who are you?
What is testing
“Software testing is a process used to identify the correctness, completeness, and quality of a developed computer software.
It includes a set of activities conducted with the intent of finding errors in software so that it could be corrected before the product is released to the end user”
“In simple words, software testing is an activity to check that the software is defect free”
Why this session
Why this session?
- The first sprint for me was testing
- You'll learn a lot of new things
- Really helpful and enough issues
- You don't have to know the complete module
Own modules vs other modules
Own modules
- Easy tests are still available
- You'll have to set up everything yourself
Other modules
- Will have a lot of functions already created
- Easy parts will be gone most likely
- Best if you can find a maintainer
- 2000 tagged with 'needs tests' in core
- 4000 tagged with 'needs tests' overall
Why you should write tests
The China airline Airbus A300 crashed due to a software bug in 1994 killing 264 people.
Software bug caused the failure of a military satellite launch, causing a loss of $1.2 billion
responsibility
When your module / patch is used by multiple people, you should make sure that it will work with every update, otherwise you'll break other peoples websites.
- Manual testing
- Automated testing
- Unit testing
- Kernel testing
- Functional testing
Types of testing
Manual testing
Manual testing
- Done by developer, tester, client and/or project manager
- Most primitive of all testing types
- Mostly used for short-term projects
- Easy to forget use cases
- Doesn't require a lot of time
- Becomes very boring if you have to execute the same test multiple times.
Manual testing
- In the browser
- With xdebug, var_dump, dsm, dpm, kint
- Reading code
Manual testing
https://www.drupal.org/project/issues?projects=&status=8
Unit testing
Unit testing
- Tests on functions, methods, classes
- Extends on the class UnitTestCase
Advantages of unit testing
- Verify individual parts
- Quickly find problems in code
- Fast execution
- No system setup for the test run
Disadvantages of unit testing
- Refactoring your code might require tests to be rewritten
- Complicated mocking
- No guarantee that the whole system actually works
Folder structure
../modules/custom
--lissabon
----lissabon.info.yml
----lissabon.module
----src
------Lissabon.php
----tests
------src
--------Unit
----------LissabonSumTest.php
Lissabon.php
<?php
namespace Drupal\lissabon;
/**
* Defines a Lissabon class.
*/
class Lissabon {
private $total = 0;
/**
* Returns the total amount.
*
* @return int
* Returns the total
*/
public function getTotal() {
return $this->total;
}
/**
* Sets the total.
*
* @param int $amount
* Amount to be set.
*/
public function setTotal($amount) {
$this->total = $amount;
}
/**
* Adds an amount to the total.
*
* @param int $amount
* Amount to be added.
*/
public function addToTotal($amount) {
$this->total = $this->getTotal() + $amount;
}
}
LissabonSumTest.php
<?php
namespace Drupal\lissabon;
use Drupal\Tests\UnitTestCase;
/**
* Defines a Unit class.
*
* @group lissabon
*/
class LissabonSumTest extends UnitTestCase {
protected $lissabon;
/**
* Before a test method is run, setUp() is invoked.
*
* We create a new object of the class Lissabon.
*/
public function setUp() {
$this->lissabon = new Lissabon();
}
}
LissabonSumTest.php
<?php
namespace Drupal\lissabon;
use Drupal\Tests\UnitTestCase;
/**
* Defines a Unit class.
*
* @group lissabon
*/
class LissabonSumTest extends UnitTestCase {
/**
* We unset the lissabon object.
*
* Once test method has finished running, whether it succeeded or
* failed, tearDown() will be invoked.
*/
public function tearDown() {
unset($this->lissabon);
}
}
LissabonSumTest.php
<?php
namespace Drupal\lissabon;
use Drupal\Tests\UnitTestCase;
/**
* Defines a Unit class.
*
* @group lissabon
*/
class LissabonSumTest extends UnitTestCase {
/**
* Covers setTotal.
*/
public function testSetTotal() {
$this->assertEquals('0', $this->lissabon->getTotal());
$this->lissabon->setTotal(366);
$this->assertEquals(366, $this->lissabon->getTotal());
}
/**
* Covers getTotal.
*/
public function testGetTotal() {
$this->lissabon->setTotal(366);
$this->assertNotEquals(200, $this->lissabon->getTotal());
}
/**
* Covers addToTotal.
*/
public function testAddToTotal() {
$this->lissabon->setTotal(200);
$this->lissabon->addToTotal(166);
$this->assertEquals(366, $this->lissabon->getTotal());
}
}
Setting it up for the first time
# Download a drupal installation file (you can also use git clone here)
composer create-project drupal-composer/drupal-project:8.x-dev lissabon-testing
--stability dev --no-interaction --prefer-source
# Install the module you're working on (unless you're working on core,
# here you can alsou use git clone if you want)
composer require drupal/example --prefer-source
# go to the core folder in the web directory
cd lissabon-testing/web/core
# Copy the phpunit.xml.dist file to phpunit.xml
cp phpunit.xml.dist phpunit.xml
Now to the testing part
# Go to the root of your website (web folder)
cd ..
# start the test (change example for the module you're using or leave it empty to test core)
../vendor/bin/phpunit -c core modules/example
# If you want to test 1 specific test, you can add the following option
--filter testName
In action
../vendor/bin/phpunit modules/custom/lissabon/tests/src/Unit/
Looks like it's not working yet
In action
../vendor/bin/phpunit -c core modules/custom/lissabon/tests/src/Unit/
In action
#or, go into the core folder
cd core
../../vendor/bin/phpunit ../modules/custom/lissabon/tests/src/Unit/
Kernel testing
Kernel testing
Kernel tests are integration tests that test on components.
You can install modules
Minimal Drupal, full
Extends from KernelTestBase or EntityKernelTestBase
Advantages of Kernel testing
- Verify that components actually work together
- Somewhat easy to locate bugs
Disadvantages of Kernel testing
- Slower execution
- System setup required
- No guarantee that end user features actually work
Folder structure
../modules/custom
--lissabon
----lissabon.info.yml
----lissabon.module
----config
------install
--------lissabon.settings.yml
------schema
--------lissabon.schema.yml
----src
------Form
--------LissabonConfigForm.php
------Lissabon.php
----tests
------src
--------Kernel
----------LissabonConfigTest.php
LissabonConfigForm.php
<?php
namespace Drupal\lissabon\Form;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Defines a Configuration form.
*/
class LissabonConfigForm extends ConfigFormBase {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'lissabon_config_form';
}
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames() {
return [
'lissabon.settings',
];
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$config = $this->config('lissabon.settings');
$lissabon_name = $config->get('lissabon_name');
$form['lissabon_name'] = [
'#type' => 'textfield',
'#default_value' => isset($lissabon_name) ? $lissabon_name : '',
'#title' => $this->t('Fill in a name'),
];
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
// Save the form values in config.
$lissabon_name = $form_state->getValue('lissabon_name');
\Drupal::configFactory()
->getEditable('lissabon.settings')
->set('lissabon_name', $lissabon_name)
->save();
parent::submitForm($form, $form_state);
}
}
lissabon.settings.yml
lissabon_name: 'Dev days is awesome!'
lissabon.schema.yml
lissabon.settings:
type: config_object
label: 'Lissabon testing'
mapping:
lissabon_name:
type: string
label: 'Lissabon name'
LissabonConfigTest.php
<?php
namespace Drupal\lissabon;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests the config for Lissabon.
*
* @package Drupal\lissabon
*/
class LissabonConfigTest extends KernelTestBase {
/**
* User for testing.
*
* @var \Drupal\user\UserInterface
*/
protected $testUser;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = [
'system',
'user',
'lissabon',
];
/**
* Sets up the test.
*/
protected function setUp() {
/** @var \Drupal\user\RoleInterface $role */
parent::setUp();
// We install the config of this module, otherwise the default value won't
// be set.
$this->installConfig(['lissabon']);
}
}
LissabonConfigTest.php
<?php
namespace Drupal\lissabon;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests the config for Lissabon.
*
* @package Drupal\lissabon
*/
class LissabonConfigTest extends KernelTestBase {
/**
* Tests the default config.
*/
public function testDefaultLissabonConfig() {
$config = $this->config('lissabon.settings');
$lissabon_name = $config->get('lissabon_name');
$this->assertEquals('Dev days is awesome!', $lissabon_name);
}
/**
* Tests changing the config.
*/
public function testChangeLissabonConfig() {
// First we check if the config is the default one.
$config = $this->config('lissabon.settings');
$lissabon_name = $config->get('lissabon_name');
$this->assertEquals('Dev days is awesome!', $lissabon_name);
// We change the config.
\Drupal::configFactory()
->getEditable('lissabon.settings')
->set('lissabon_name', 'Lissabon is awesome!')
->save();
// We check if the config has changed.
$lissabon_name = $config->get('lissabon_name');
$this->assertEquals('Lissabon is awesome!', $lissabon_name);
}
}
Setting it up for the first time
# Download a drupal installation file
composer create-project drupal-composer/drupal-project:8.x-dev lissabon-testing
--stability dev --no-interaction --prefer-source
# Install the module you're working on (unless you're working on core)
composer require drupal/example --prefer-source
# go to the core folder in the web directory
cd lissabon-testing/web/core
# Copy the phpunit.xml.dist file to phpunit.xml
cp phpunit.xml.dist phpunit.xml
# Create a database for your website (e.g. lissabon_testing).
# in the phpunit.xml file, update the following lines:
<env name="SIMPLETEST_DB" value=""/>
# to
<env name="SIMPLETEST_DB" value="mysql://root:root@localhost/lissabon_testing"/>
# where you change the value with your values (mysql://username:password@localhost/database_name)
Now to the testing part
# Go to the root of your website (web folder)
cd ..
# start the test (change example for the module you're using or leave it empty to test core)
../vendor/bin/phpunit -c core modules/example
# If you want to test 1 specific test, you can add the following option
--filter testName
In action
../vendor/bin/phpunit -c core modules/custom/lissabon/tests/src/Kernel/
Functional testing
Functional testing
2 types:
- BrowserTestBase
- JavascriptTestbase
Advantages of functional testing
- Verify that the system works as experienced by the user
- Verify that the system works when code is refactored
Disadvantages of functional testing
- Very slow execution
- Heavy system setup
- Hard to locate origins of bugs
- Prone to random test fails
- Hard to change
Folder structure
../modules/custom
--lissabon
----lissabon.info.yml
----lissabon.module
----lissabon.routing.yml
----config
------install
--------lissabon.settings.yml
------schema
--------lissabon.schema.yml
----src
------Form
--------LissabonConfigForm.php
------Lissabon.php
----tests
------src
--------Functional
----------LissabonConfigFormTest.php
----------LoadTest.php
lissabon.routing.yml
# Lissabon routing definition
entity.lissabon_routing.collection:
path: '/admin/lissabon'
defaults:
_form: '\Drupal\lissabon\Form\LissabonConfigForm'
_title: 'Lissabon configuration'
requirements:
_permission: 'administer site configuration'
options:
_admin_route: TRUE
LissabonConfigForm.php
<?php
namespace Drupal\lissabon\Form;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Defines a Configuration form.
*/
class LissabonConfigForm extends ConfigFormBase {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'lissabon_config_form';
}
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames() {
return [
'lissabon.settings',
];
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$config = $this->config('lissabon.settings');
$lissabon_name = $config->get('lissabon_name');
$form['lissabon_name'] = [
'#type' => 'textfield',
'#default_value' => isset($lissabon_name) ? $lissabon_name : '',
'#title' => $this->t('Fill in a name'),
];
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
// Save the form values in config.
$lissabon_name = $form_state->getValue('lissabon_name');
\Drupal::configFactory()
->getEditable('lissabon.settings')
->set('lissabon_name', $lissabon_name)
->save();
parent::submitForm($form, $form_state);
}
}
LissabonConfigFormTest.php
<?php
namespace Drupal\lissabon;
use Drupal\Tests\BrowserTestBase;
/**
* Tests the config form.
*
* @package Drupal\lissabon
*/
class LissabonConfigFormTest extends BrowserTestBase {
protected $user;
protected $editForm;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['lissabon'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->user = $this->drupalCreateUser(['administer site configuration']);
$this->drupalLogin($this->user);
}
}
LissabonConfigFormTest.php
<?php
namespace Drupal\lissabon;
use Drupal\Tests\BrowserTestBase;
/**
* Tests the config form.
*
* @package Drupal\lissabon
*/
class LissabonConfigFormTest extends BrowserTestBase {
/**
* Tests the configuration form.
*/
public function testConfigForm() {
// We try to change the form on /admin/lissabon.
$this->editForm = 'admin/lissabon';
$form = [
'edit-lissabon-name' => 'Lissabon is awesome',
];
$this->drupalPostForm($this->editForm, $form, 'Save');
// We check if our change went through.
$this->drupalGet('admin/lissabon');
$this->assertResponse(200);
$config = $this->config('lissabon.settings');
$this->assertFieldByName('lissabon_name', $config->get('lissabon_name'));
}
}
Setting it up for the first time
# Download a drupal installation file
composer create-project drupal-composer/drupal-project:8.x-dev lissabon-testing
--stability dev --no-interaction --prefer-source
# Install the module you're working on (unless you're working on core)
composer require drupal/example --prefer-source
# go to the core folder in the web directory
cd lissabon-testing/web/core
# Copy the phpunit.xml.dist file to phpunit.xml
cp phpunit.xml.dist phpunit.xml
# Create a database for your website (e.g. lissabon_testing).
# in the phpunit.xml file, update the following lines:
<env name="SIMPLETEST_DB" value=""/>
# to
<env name="SIMPLETEST_DB" value="mysql://root:root@localhost/lissabon_testing"/>
# where you change the value with your values (mysql://username:password@localhost/database_name)
# Now you'll have to set up a base url as well, so change the following line
<env name="SIMPLETEST_BASE_URL" value=""/>
# to
<env name="SIMPLETEST_BASE_URL" value="http://lissabontesting.local"/>
# where you change the value with your values (http://lissabontesting.local)
Now to the testing part
# Go to the root of your website (web folder)
cd ..
# start the test (change example for the module you're using or leave it empty to test core)
../vendor/bin/phpunit -c core modules/example
# If you want to test 1 specific test, you can add the following option
--filter testName
In action
../vendor/bin/phpunit -c core modules/custom/lissabon/tests/src/Unit/
Debugging functional tests
# In the phpunit.xml file, change
<env name="BROWSERTEST_OUTPUT_DIRECTORY" value=""/>
# to
<env name="BROWSERTEST_OUTPUT_DIRECTORY" value="/Applications/MAMP/htdocs/customprojects/lissabon-testing/web/sites/default/files"/>
# where The value is a writable folder on your site
# Add this to your command.
--printer="\Drupal\Tests\Listeners\HtmlOutputPrinter"
In action
# Execute test while printing html
../vendor/bin/phpunit -c core modules/custom/lissabon/tests/src/Functional/
--printer="\Drupal\Tests\Listeners\HtmlOutputPrinter"
To summarise
- If you want to test functions, Unit tests
- If you want to test module APIs without HTTP with a database, use Kernel tests
- If you want to test web interfaces, Use Functional tests
Tips and tricks
Start small
By starting with the easy task, you'll get more and more familiar with the code for testing.
Learn from others
A lot of tests are similar, use that to your advantage and read other tests of similar modules.
Find a maintainer
Try to find a maintainer/co-maintainer of a module you would like to help on.
They will be glad to help you and guide you through it.
Create a roadmap (own modules)
On your project page, create a roadmap with future steps of your module.
Add a segment tests that you'll update along the way
Use Drupal console
When creating a module with Drupal console, it asks if you want to create a test.
This is a very simple functional test to check if the front page is still working
Use Drupal console
<?php
namespace Drupal\Tests\facets_autocomplete\Functional;
use Drupal\Core\Url;
use Drupal\Tests\BrowserTestBase;
/**
* Simple test to ensure that main page loads with module enabled.
*
* @group facets_autocomplete
*/
class LoadTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['facets_autocomplete'];
/**
* A user with permission to administer site configuration.
*
* @var \Drupal\user\UserInterface
*/
protected $user;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->user = $this->drupalCreateUser(['administer site configuration']);
$this->drupalLogin($this->user);
}
/**
* Tests that the home page loads with a 200 response.
*/
public function testLoad() {
$this->drupalGet(Url::fromRoute('<front>'));
$this->assertSession()->statusCodeEquals(200);
}
}
Questions?
@brentgees
Lissabon - testing in D8
By Brent Gees
Lissabon - testing in D8
How to get started with writing tests for contrib
- 1,377