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 api, without http


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,239