Tutorial: Testing¶
Introduction¶
This tutorial explains:
Warning
It is required to run the tests from a phpBB clone of the git repository, because the test framework is not included in the download packages.
Unit Tests¶
Since unit tests are explained in more detail in the Unit Testing section of the general testing documentation and require no changes for extensions, we only add a quick example here.
Note
If you have not yet run the tests of phpBB, it is recommended to make a break here and try to run them first. This helps to make sure, you have set up everything correctly, before writing the tests for the extension.
More details are explained in the Unit Testing section.
<?php
// ext/acme/demo/tests/controller/main_test.php
/**
*
* This file is part of the phpBB Forum Software package.
*
* @copyright (c) phpBB Limited <https://www.phpbb.com>
* @license GNU General Public License, version 2 (GPL-2.0)
*
* For full copyright and license information, please see
* the docs/CREDITS.txt file.
*
*/
namespace acme\demo\tests\controller;
class main_test extends \phpbb_test_case
{
/** @var \acme\demo\controller\main */
protected $controller;
protected $config;
protected $helper;
protected $language;
protected $template;
public function setUp()
{
parent::setUp();
$this->config = $this->getMockBuilder('\phpbb\config\config')
->disableOriginalConstructor()
->getMock();
$this->helper = $this->getMockBuilder('\phpbb\controller\helper')
->disableOriginalConstructor()
->getMock();
$this->template = $this->getMockBuilder('\phpbb\template\template')
->disableOriginalConstructor()
->getMock();
$this->language = $this->getMockBuilder('\phpbb\language\language')
->disableOriginalConstructor()
->getMock();
$this->controller = new \acme\demo\controller\main(
$this->config,
$this->helper,
$this->template,
$this->language
);
}
public function test_handle_bertie()
{
$this->helper->expects($this->once())
->method('message')
->with('NO_AUTH_SPEAKING', array('bertie'), 'NO_AUTH_OPERATION', 403)
->willReturn($this->getMock('Symfony\Component\HttpFoundation\Response'));
$this->helper->expects($this->never())
->method('render');
$response = $this->controller->handle('bertie');
$this->assertInstanceOf('\Symfony\Component\HttpFoundation\Response', $response);
}
public function handle_data()
{
return array(
array('foo', true, 'DEMO_GOODBYE'),
array('bar', false, 'DEMO_HELLO'),
);
}
/**
* @dataProvider handle_data
*/
public function test_handle($name, $config, $expected_language)
{
$language_return = $expected_language . '#' . $name;
$this->config->expects($this->once())
->method('offsetExists')
->with('acme_demo_goodbye')
->willReturn(true);
$this->config->expects($this->once())
->method('offsetGet')
->with('acme_demo_goodbye')
->willReturn($config);
$this->language->expects($this->once())
->method('lang')
->with($expected_language, $name)
->willReturn($language_return);
$this->template->expects($this->once())
->method('assign_var')
->with('DEMO_MESSAGE', $language_return);
$this->helper->expects($this->once())
->method('render')
->with('demo_body.html',test_handle_bertie() $name, 200, false)
->willReturn($this->getMock('Symfony\Component\HttpFoundation\Response'));
$response = $this->controller->handle($name);
$this->assertInstanceOf('\Symfony\Component\HttpFoundation\Response', $response);
}
}
Using mocks¶
In the setUp()
method we create our controller object. However, we do not
use the actual phpBB classes which are used by the controller when opening
the page. Instead
phpunit mocks are
injected.
These mocks allow us to check how often a method is called, what the arguments are and let us specify the return value. This helps us to verify that our controller code behaves as expected. These mocks also help prevent getting false errors in cases where using the actual phpBB classes may behave unexpectedly.
Testing a method¶
In our controller we added special handling when the provided name is
bertie
. So our first test is whether the
phpbb\controller\helper::message()
method is called once with the
NO_AUTH_SPEAKING
error message. We also want to make sure that the
phpbb\controller\helper::render()
method is not called in this case.
Then we call the controller’s handle()
method. If the methods are invoked
correctly the test will pass.
public function test_handle_bertie()
{
$this->helper->expects($this->once())
->method('message')
->with('NO_AUTH_SPEAKING', array('bertie'), 'NO_AUTH_OPERATION', 403)
->willReturn($this->getMock('Symfony\Component\HttpFoundation\Response'));
$this->helper->expects($this->never())
->method('render');
$response = $this->controller->handle('bertie');
$this->assertInstanceOf('\Symfony\Component\HttpFoundation\Response', $response);
}
Note
Make sure that the name of your testing method starts with test
.
Otherwise the test will not be executed by phpunit.
Data providers¶
In most cases we will want to provide some test data to our unit test methods.
A data provider is a method that returns an array of arrays containing input
variables and expected output variables. The inner array contains the arguments
for the test
method.
public function handle_data()
{
return array(
array('foo', true, 'DEMO_GOODBYE'),
array('bar', false, 'DEMO_HELLO'),
);
}
This data provider contains two arrays of test data, so our test will be called twice, once with the arguments:
‘foo’
true
‘DEMO_GOODBYE’
and a second time with:
‘bar’
false
‘DEMO_HELLO’
In the test we then tell the phpbb\config\config
mock to expect to be
called only once, and to return the specified value (which is passed in as an
argument) for the acme_demo_goodbye
config variable.
/**
* @dataProvider handle_data
*/
public function test_handle($name, $config, $expected_language)
{
// ...
$this->config->expects($this->once())
->method('offsetGet')
->with('acme_demo_goodbye')
->willReturn($config);
If we have a short look at our controller again, we see that the value of the
config influences the \phpbb\language\language::lang()
call:
$l_message = empty($this->config['acme_demo_goodbye']) ? 'DEMO_HELLO' : 'DEMO_GOODBYE';
$this->template->assign_var('DEMO_MESSAGE', $this->language->lang($l_message, $name));
This is what we check with the third argument $expected_language
of our test
method:
$this->language->expects($this->once())
->method('lang')
->with($expected_language, $name)
->willReturn($language_return);
For more information about Mocks and phpunit, please have a look at the phpunit Documentation.
phpunit configuration file¶
Before we can run the tests we need to create the configuration file for
phpunit. Don’t be scared if you do not understand it, you should not need to
edit anything in that file.
The file should be stored as ext/acme/demo/phpunit.xml.dist
:
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="true"
backupStaticAttributes="true"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
syntaxCheck="false"
verbose="true"
bootstrap="../../../../tests/bootstrap.php"
>
<testsuites>
<testsuite name="Extension Test Suite">
<directory suffix="_test.php">./tests</directory>
<exclude>./tests/functional</exclude>
</testsuite>
<testsuite name="Extension Functional Tests">
<directory suffix="_test.php" phpVersion="5.3.19" phpVersionOperator=">=">./tests/functional/</directory>
</testsuite>
</testsuites>
<filter>
<blacklist>
<directory>./tests/</directory>
</blacklist>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">./</directory>
<exclude>
<directory suffix=".php">./language/</directory>
<directory suffix=".php">./migrations/</directory>
<directory suffix=".php">./tests/</directory>
</exclude>
</whitelist>
</filter>
</phpunit>
Now we can finally run the test suite by executing the following command:
$ ./phpBB/vendor/bin/phpunit -c phpBB/ext/acme/demo/phpunit.xml.dist
PHPUnit 4.1.0 by Sebastian Bergmann.
Configuration read from /home/nickv/phpBB/Ascraeus/phpBB/ext/acme/demo/phpunit.xml.dist
...
Time: 101 ms, Memory: 9.00Mb
OK (3 tests, 12 assertions)
To run only the tests from one file just append the relative path to the call:
$ ./phpBB/vendor/bin/phpunit -c phpBB/ext/acme/demo/phpunit.xml.dist phpBB/ext/acme/demo/tests/controller/main_test.php
PHPUnit 4.1.0 by Sebastian Bergmann.
Configuration read from /home/nickv/phpBB/Ascraeus/phpBB/ext/acme/demo/phpunit.xml.dist
...
Time: 92 ms, Memory: 9.00Mb
OK (3 tests, 12 assertions)
Unit tests with database interaction¶
When testing your own tables and columns, you can not use the normal database that phpBB’s unit tests set up by default.
Migration with database changes¶
Our extension has a migration file
ext/acme/demo/migrations/add_database_changes.php
, which contains some
database changes, so we can test them:
<?php
// ext/acme/demo/migrations/add_database_changes.php
/**
*
* This file is part of the phpBB Forum Software package.
*
* @copyright (c) phpBB Limited <https://www.phpbb.com>
* @license GNU General Public License, version 2 (GPL-2.0)
*
* For full copyright and license information, please see
* the docs/CREDITS.txt file.
*
*/
namespace acme\demo\migrations;
use phpbb\db\migration\migration;
class add_database_changes extends migration
{
public function effectively_installed()
{
return $this->db_tools->sql_column_exists($this->table_prefix . 'users', 'user_acme');
}
static public function depends_on()
{
return array('\acme\demo\migrations\add_module');
}
public function update_schema()
{
return array(
'add_tables' => array(
$this->table_prefix . 'acme_demo' => array(
'COLUMNS' => array(
'acme_id' => array('UINT', null, 'auto_increment'),
'acme_name' => array('VCHAR:255', ''),
),
'PRIMARY_KEY' => 'acme_id',
),
),
'add_columns' => array(
$this->table_prefix . 'users' => array(
'user_acme' => array('UINT', 0),
),
),
);
}
public function revert_schema()
{
return array(
'drop_columns' => array(
$this->table_prefix . 'users' => array(
'user_acme',
),
),
'drop_tables' => array(
$this->table_prefix . 'acme_demo',
),
);
}
}
Testing database changes¶
If we add a new test that checks for the existence of the table, we will see that the test fails:
<?php
// ext/acme/demo/tests/migrations/database/add_database_changes.php
/**
*
* This file is part of the phpBB Forum Software package.
*
* @copyright (c) phpBB Limited <https://www.phpbb.com>
* @license GNU General Public License, version 2 (GPL-2.0)
*
* For full copyright and license information, please see
* the docs/CREDITS.txt file.
*
*/
namespace acme\demo\tests\migrations;
class add_database_changes_test extends \phpbb_database_test_case
{
/** @var \phpbb\db\tools */
protected $db_tools;
/** @var string */
protected $table_prefix;
public function getDataSet()
{
return $this->createXMLDataSet(dirname(__FILE__) . '/fixtures/add_database_changes.xml');
}
public function setUp()
{
parent::setUp();
global $table_prefix;
$this->table_prefix = $table_prefix;
$db = $this->new_dbal();
$this->db_tools = new \phpbb\db\tools($db);
}
public function test_user_acme_column()
{
$this->assertTrue($this->db_tools->sql_column_exists(USERS_TABLE, 'user_acme'), 'Asserting that column "user_acme" exists');
}
public function test_acme_demo_table()
{
$this->assertTrue($this->db_tools->sql_table_exists($this->table_prefix . 'acme_demo'), 'Asserting that column "' . $this->table_prefix . 'acme_demo" does not exist');
}
}
Before we can run the database test, we need to create the fixture file we
specified in the getDataSet()
method. This file can be used to create
database entries before each test run is executed. We will make use of this
later. For now we just want to check if our tables have been created, so we
specify an empty config table
ext/acme/demo/tests/migrations/database/fixtures/add_database_changes.xml
:
<?xml version="1.0" encoding="UTF-8" ?>
<dataset>
<table name="phpbb_config">
<column>config_name</column>
<column>config_value</column>
<column>is_dynamic</column>
</table>
</dataset>
Execution¶
When we now execute the tests again they will fail:
$ ./phpBB/vendor/bin/phpunit -c phpBB/ext/acme/demo/phpunit.xml.dist
PHPUnit 4.1.0 by Sebastian Bergmann.
Configuration read from /home/nickv/phpBB/Ascraeus/phpBB/ext/acme/demo/phpunit.xml.dist
...FF
Time: 5.27 seconds, Memory: 10.75Mb
There were 2 failures:
1) acme\demo\tests\migrations\add_database_changes_test::test_user_acme_column
Asserting that column "user_acme" exists
Failed asserting that false is true.
/home/nickv/phpBB/Ascraeus/phpBB/ext/acme/demo/tests/migrations/add_database_changes_test.php:42
2) acme\demo\tests\migrations\add_database_changes_test::test_acme_demo_table
Asserting that column "phpbb_acme_demo" does not exist
Failed asserting that false is true.
/home/nickv/phpBB/Ascraeus/phpBB/ext/acme/demo/tests/migrations/add_database_changes_test.php:47
FAILURES!
Tests: 5, Assertions: 14, Failures: 2.
In order to get our database changes executed in unit tests, we need to tell
phpBB that this test needs the extension to be set up. We can do this by
overwriting the \phpbb_database_test_case::setup_extensions()
method of the
test and returning an array with the extension name:
class add_database_changes_test extends \phpbb_database_test_case
{
static protected function setup_extensions()
{
return array('acme/demo');
}
...
and now the test passes successfully:
$ ./phpBB/vendor/bin/phpunit -c phpBB/ext/acme/demo/phpunit.xml.dist
PHPUnit 4.1.0 by Sebastian Bergmann.
Configuration read from /home/nickv/phpBB/Ascraeus/phpBB/ext/acme/demo/phpunit.xml.dist
.....
Time: 5.45 seconds, Memory: 13.75Mb
OK (5 tests, 14 assertions)
Note
As you can see, the time for the test execution went up from a few hundred milliseconds to a few seconds. This is because database tests set up the database and populate it, which just takes time.
Therefore it is recommended to only use database tests when you really need the database. It is better to split your test file into a database-test and a non-database one, to keep the run time short.
Using fixtures¶
Now let’s make use of the fixture file to populate the database for our test.
Therefore we replace the content of the
ext/acme/demo/tests/migrations/database/fixtures/add_database_changes.xml
file with the following content:
<?xml version="1.0" encoding="UTF-8" ?>
<dataset>
<table name="phpbb_acme_demo">
<column>acme_id</column>
<column>acme_name</column>
<row>
<value>1</value>
<value>one</value>
</row>
<row>
<value>2</value>
<value>two</value>
</row>
</table>
</dataset>
Note
You can leave out columns from the tables, if you do not want to specify values for them.
Warning
Database columns that are specified as TEXT_UNI
columns need to be
specified, otherwise your test will fail on some databases.
After we added the content to the database we add a new test at the end of the
file, that queries our table to the
ext/acme/demo/tests/migrations/database/add_database_changes.php
and tests
whether the values are really in the database:
...
public function data_acme_demo_content()
{
return array(
array(1, 'one'),
array(2, 'two'),
);
}
/**
* @dataProvider data_acme_demo_content
*
* @param int $acme_id
* @param string $expected
*/
public function test_acme_demo_content($acme_id, $expected)
{
/** @var \phpbb\db\driver\driver_interface $db */
$db = $this->new_dbal();
$sql = 'SELECT acme_name
FROM ' . $this->table_prefix . 'acme_demo
WHERE acme_id = ' . (int) $acme_id;
$result = $db->sql_query($sql);
$value = $db->sql_fetchfield('acme_name');
$db->sql_freeresult($result);
$this->assertEquals($expected, $value);
}
}
Functional testing¶
Functional tests simulate calling a URL and allow you to filter the output then and check whether certain elements have a specific content.
Note
Again it is recommended to run the functional tests of phpBB first, before writing the tests for the extension.
More details are explained in the Functional Testing section.
Again like with the database changes we need to tell phpBB that the test depends on the extension, then phpBB will take care of enabling the extension before the test execution. Our little test opens the route we added and then checks for the right message, like the unit test we wrote in unit tests at the beginning:
<?php
/**
*
* This file is part of the phpBB Forum Software package.
*
* @copyright (c) phpBB Limited <https://www.phpbb.com>
* @license GNU General Public License, version 2 (GPL-2.0)
*
* For full copyright and license information, please see
* the docs/CREDITS.txt file.
*
*/
namespace acme\demo\tests\functional;
/**
* @group functional
*/
class demo_test extends \phpbb_functional_test_case
{
static protected function setup_extensions()
{
return array('acme/demo');
}
public function test_demo_world()
{
$this->add_lang_ext('acme/demo', 'demo');
$crawler = self::request('GET', 'app.php/demo/world');
$this->assertContains($this->lang('DEMO_HELLO', 'world'), $crawler->filter('h2')->text());
}
public function test_demo_bertie()
{
$this->add_lang_ext('acme/demo', 'demo');
$crawler = self::request('GET', 'app.php/demo/bertie');
$this->assertContains($this->lang('NO_AUTH_SPEAKING', 'bertie'), $crawler->filter('#message p')->text());
}
}
Running this test, however, will fail:
$ ./phpBB/vendor/bin/phpunit -c phpBB/ext/acme/demo/phpunit.xml.dist
PHPUnit 4.1.0 by Sebastian Bergmann.
Configuration read from /home/nickv/phpBB/Ascraeus/phpBB/ext/acme/demo/phpunit.xml.dist
........F
Time: 22.37 seconds, Memory: 17.25Mb
There was 1 failure:
1) acme\demo\tests\functional\demo_test::test_demo_bertie
HTTP status code does not match
Failed asserting that 403 matches expected 200.
/home/nickv/phpBB/Ascraeus/tests/test_framework/phpbb_functional_test_case.php:900
/home/nickv/phpBB/Ascraeus/tests/test_framework/phpbb_functional_test_case.php:859
/home/nickv/phpBB/Ascraeus/tests/test_framework/phpbb_functional_test_case.php:138
/home/nickv/phpBB/Ascraeus/phpBB/ext/acme/demo/tests/functional/demo_test.php:38
FAILURES!
Tests: 9, Assertions: 49, Failures: 1.
The reason is that the test suite compares the response for the correct format
(valid HTML, without debug errors) and a successful status code 200
.
Therefore we need to adjust the bertie test, because we return a 403
status
in the controller, if someone tries to talk to bertie:
public function test_demo_bertie()
{
$this->add_lang_ext('acme/demo', 'demo');
$crawler = self::request('GET', 'app.php/demo/bertie', array(), false);
self::assert_response_html(403);
$this->assertContains($this->lang('NO_AUTH_SPEAKING', 'bertie'), $crawler->filter('#message p')->text());
}
Now the tests will pass correctly:
$ ./phpBB/vendor/bin/phpunit -c phpBB/ext/acme/demo/phpunit.xml.dist
PHPUnit 4.1.0 by Sebastian Bergmann.
Configuration read from /home/nickv/phpBB/Ascraeus/phpBB/ext/acme/demo/phpunit.xml.dist
.........
Time: 22.11 seconds, Memory: 17.00Mb
OK (9 tests, 52 assertions)
Note
Functional tests are slow. Depending on your server, it might take up to 2 seconds per page view. phpBB is installed via page views as well, which takes another 20 to 100 seconds, depending on various configurations, for the first functional tests. Subsequent functional tests do not reinstall the board, so they do not have the long setup time.
Continuous integration with Travis CI¶
As a final step in this tutorial, we want to explain how to set up automated testing of your extension on Travis CI (free of charge, when your project is public). In order to do that, your extension must first be set up as a project repository on GitHub (also free of charge, when your project is public).
If you need help setting up git and creating your GitHub project, please have a look at the Help section on Github, particularly the following two help topics:
Note
It is recommended to use the root of the extension (ext/acme/demo
) also
as root for the Git repository. Otherwise the scripts that phpBB provides for
easy test execution on Travis CI will not work correctly.
View one of phpBB’s official extension repositories as an example: Board Rules.
Travis CI configuration file¶
When you are done with that, we need to add two files to our extension. The
first file is the Travis CI configuration file, .travis.yml
:
Note
If you have trouble generating the file, because it has a leading dot, try
naming the file .travis.yml.
(with a leading and trailing dot). This
will allow you to create the file on most operating systems.
The file should now also be hidden. If you can not see it anymore, your file explorer should have an option to make hidden files visible again.
language: php
dist: trusty
matrix:
include:
- php: 5.5
env: DB=none;NOTESTS=1
- php: 5.4
env: DB=mysqli #myisam
- php: 5.4
env: DB=mysql
- php: 5.4
env: DB=mariadb
- php: 5.4
env: DB=postgres
- php: 5.4
env: DB=sqlite3
- php: 5.5
env: DB=mysqli
- php: 5.6
env: DB=mysqli
- php: 7.0
env: DB=mysqli
- php: 7.1
env: DB=mysqli
- php: 7.2
env: DB=mysqli
- php: nightly
env: DB=mysqli
allow_failures:
- php: nightly
fast_finish: true
env:
global:
- EXTNAME="acme/demo" # CHANGE name of the extension HERE
- SNIFF="1" # Should we run code sniffer on your code?
- IMAGE_ICC="1" # Should we run icc profile sniffer on your images?
- EPV="1" # Should we run EPV (Extension Pre Validator) on your code?
- PHPBB_BRANCH="3.2.x"
branches:
only:
- master
- develop
- /^\d+(\.\d+)?\.x$/
install:
- travis/prepare-phpbb.sh $EXTNAME $PHPBB_BRANCH
- cd ../../phpBB3
- travis/prepare-extension.sh $EXTNAME $PHPBB_BRANCH
- travis/setup-phpbb.sh $DB $TRAVIS_PHP_VERSION
- sh -c "if [ '$EPV' == '1' -a '$NOTESTS' == '1' ]; then cd phpBB; composer remove sami/sami --update-with-dependencies --dev --no-interaction; composer require phpbb/epv:dev-master --dev --no-interaction --ignore-platform-reqs; cd ../; fi"
before_script:
- travis/setup-database.sh $DB $TRAVIS_PHP_VERSION $NOTESTS
script:
- sh -c "if [ '$SNIFF' != '0' ]; then travis/ext-sniff.sh $DB $TRAVIS_PHP_VERSION $EXTNAME $NOTESTS; fi"
- sh -c "if [ '$IMAGE_ICC' != '0' ]; then travis/check-image-icc-profiles.sh $DB $TRAVIS_PHP_VERSION $NOTESTS; fi"
- sh -c "if [ '$EPV' != '0' -a '$NOTESTS' = '1' ]; then phpBB/vendor/bin/EPV.php run --dir='phpBB/ext/$EXTNAME/'; fi"
- sh -c "if [ '$NOTESTS' != '1' ]; then phpBB/vendor/bin/phpunit --configuration phpBB/ext/$EXTNAME/travis/phpunit-$DB-travis.xml --bootstrap ./tests/bootstrap.php; fi"
Note
You should not need to make any changes in this file, apart from the following section, which should be quite self explanatory:
env:
global:
- EXTNAME="acme/demo" # CHANGE name of the extension HERE
- SNIFF="1" # Should we run code sniffer on your code?
- IMAGE_ICC="1" # Should we run icc profile sniffer on your images?
- EPV="1" # Should we run EPV (Extension Pre Validator) on your code?
- PHPBB_BRANCH="3.2.x"
Preparing phpBB¶
The second file we need to create is a helper file called
travis/prepare-phpbb.sh
, which is a script used by Travis CI, to set up
the phpBB installation from GitHub for us:
Warning
You should not edit the content of this file!
#!/bin/bash
#
# This file is part of the phpBB Forum Software package.
#
# @copyright (c) phpBB Limited <https://www.phpbb.com>
# @license GNU General Public License, version 2 (GPL-2.0)
#
# For full copyright and license information, please see
# the docs/CREDITS.txt file.
#
set -e
set -x
EXTNAME=$1
BRANCH=$2
EXTPATH_TEMP=$3
# Copy extension to a temp folder
mkdir ../../tmp
cp -R . ../../tmp
cd ../../
# Clone phpBB
git clone --depth=1 "git://github.com/phpbb/phpbb.git" "phpBB3" --branch=$BRANCH
Note
The prepare-phpbb.sh file needs to have executable permissions or Travis CI tests will fail. You can set the correct permission for this file from a terminal command line interface, e.g.
$ cd path/to/your/extension
$ git update-index --chmod=+x travis/prepare-phpbb.sh
Enable Travis CI on GitHub¶
As a final step you need to enable Travis CI in your GitHub repository.
Open your repository, e.g. https://github.com/phpbb/phpbb-ext-acme-demo,
Go to “Settings”
“Integrations & Services”
Press the “Add Service” button and search for
Travis CI
Now when you commit and push the travis files you created to the master
branch of your repository, the unit, database and functional tests will be executed.
All future commits pushed to your repository, including Pull Requests, will trigger
your tests to execute.
Well written tests help prevent regressions (breaking other parts of your code) by alerting you to any problems resulting from changes to your code while fixing bugs, adding new features and other code changes to your extension.
If your tests fail after committing changes, you will receive a notification email from Travis CI. The error logs from Travis CI can be a little daunting at first, but once you get used to them they can help you pinpoint unforeseen bugs and regressions in your code that must be fixed.
Travis CI also provides Build Status badges. They provide you the code in markdown format so you can add the badge to your repository’s README so visitors can see that the build status of your extension.