Resource icon

Unit testing XenForo addons - tutorial

Sim

Well-known member
Sim submitted a new resource:

Unit testing XenForo addons - tutorial - Learn how to unit test your XenForo addons to make them more robust and error-free

1. Introduction

Unit testing is a process by which individual components (units) of your software are tested. The objective is to isolate a section of code and validate its correctness - ensure that it behaves as expected for the given inputs.

There are multiple levels of testing typically used in software development, including Unit testing, Integration testing, System testing, Acceptance testing and so on. Unit testing is generally the lowest level of testing...

Read more about this resource...
 

Sim

Well-known member
I have been using this package quite a bit to unit test my own addons - but at the moment they are all in private git repositories, so I can't share them for examples.

I will try and put together some examples over the next few days to show some real unit testing scenarios.
 

Jeremy P

XenForo developer
Staff member
This is really awesome. I've always felt that lack of decent test support was one of the larger remaining voids in the XF development space.
 
  • Like
Reactions: Sim

Sim

Well-known member
I must say that I continue to be deeply impressed with the architecture of XenForo v2 - just about everything I've tried to do has had an elegant solution thanks to the thoughtful approach @Kier @Mike and @Chris D have taken in getting the underlying structure right.

About the only thing still causing me grief is the Database layer - if we could just swap that out for SQLite running an in-memory database, we could do so much more from a testing perspective (build and then discard the database for every test!!)

I haven't yet looked to see how much MySQL-specific code there is which might stop us implementing a SQLite adapter for testing purposes.

Then I have to solve the issue of how to instantiate a fully functional XenForo application in the database quickly and efficiently so it can be done (and again, discarded) for every test - I have a few ideas about how we might do that, but haven't actually tried it yet.

Then there will be the task of creating factories to generate content - if we need a bunch of threads and posts created, we need to be able to do that programmatically in our tests. Laravel uses the Faker library with its factories that generate dummy content for testing - I'm hoping we can extend that same library to work with our entities for use with XenForo.

Lots of work still to do here!
 

Sim

Well-known member
I've pushed some test code to BitBucket which shows the assertBbCode function in action: https://bitbucket.org/hampel/content-bbcode-xenforo/src/develop/tests/Feature/PostBbCodeTest.php

PHP:
<?php namespace Tests\Feature;

use Tests\TestCase;

class PostBbCodeTest extends TestCase
{
    protected $boardUrl;

    protected function setUp() : void
    {
        parent::setUp();

        $options = $this->app()->options();

        $this->boardUrl = $options['boardUrl'];
    }

    // ------------------------------------------------

    public function test_bbcode_post_bad_id()
    {
        $bbCode = '[post=foo]view this post[/post]';

        $expectedHtml = '<div class="bbWrapper">[post=foo]view this post[/post]</div>';
        $this->assertBbCode($expectedHtml, $bbCode, 'html');

        $expectedHtml = '[post=foo]view this post[/post]';
        $this->assertBbCode($expectedHtml, $bbCode, 'simpleHtml');

        $expectedHtml = '<div class="bbWrapper">[post=foo]view this post[/post]</div>';
        $this->assertBbCode($expectedHtml, $bbCode, 'emailHtml');
    }

    public function test_bbcode_post_id_in_tag()
    {
        $bbCode = '[post=2]view this post[/post]';

        $expectedHtml = '<div class="bbWrapper"><a href="' . $this->boardUrl . '/posts/2/" target="_blank" class="link link--external">view this post</a></div>';
        $this->assertBbCode($expectedHtml, $bbCode, 'html');

        $expectedHtml = '<a href="' . $this->boardUrl . '/posts/2/">view this post</a>';
        $this->assertBbCode($expectedHtml, $bbCode, 'simpleHtml');

        $expectedHtml = '<div class="bbWrapper"><a href="' . $this->boardUrl . '/posts/2/">view this post</a></div>';
        $this->assertBbCode($expectedHtml, $bbCode, 'emailHtml');
    }

    public function test_bbcode_post_id_in_body()
    {
        $bbCode = '[post]2[/post]';

        $expectedHtml = '<div class="bbWrapper"><a href="' . $this->boardUrl . '/posts/2/" target="_blank" class="link link--external">' . $this->boardUrl . '/posts/2/</a></div>';
        $this->assertBbCode($expectedHtml, $bbCode, 'html');

        $expectedHtml = '<a href="' . $this->boardUrl . '/posts/2/">' . $this->boardUrl . '/posts/2/</a>';
        $this->assertBbCode($expectedHtml, $bbCode, 'simpleHtml');

        $expectedHtml = '<div class="bbWrapper"><a href="' . $this->boardUrl . '/posts/2/">' . $this->boardUrl . '/posts/2/</a></div>';
        $this->assertBbCode($expectedHtml, $bbCode, 'emailHtml');
    }
}
 

Banxix

Well-known member
I'm also struggling to make proper and thoroughful tests for XF. To think that we need to build a large system without tests is a nightmare.
 

Sim

Well-known member
I'm working on releasing a couple of updated addons which will include extensive unit tests - hopefully they will be useful as examples of the types of things you can do.
 

Banxix

Well-known member
I'm working on releasing a couple of updated addons which will include extensive unit tests - hopefully they will be useful as examples of the types of things you can do.

That would be great.

As of right now, to write tests for XF, one need to read whole (or part) of XF code base thoroughly. For example, cache system of entity will make two entity objects being equal in some circumstances (heard from DBTech). Sometime, it is not a viable solution to read and understand core methods in a short time, if you're not familiar enough.
 

Sim

Well-known member
Sim updated Unit testing XenForo addons - tutorial with a new update entry:

v1.1.0 update

  • Feature: added new functionality to Interacts with Extension
    • isolateAddon
  • Feature: Interacts with Registry - adds:
    • fakesRegistry
  • Feature: Interacts with Filesystem - adds:
    • swapFs
    • mockFs
  • bugfix: after mocking the database, set up the entity manager again, so we get the mocked database
  • bugfix: should pass options array through to parent
  • bugfix: cleaned up function visibility for consistency
  • bugfix: override...

Read the rest of this update entry...
 

Sim

Well-known member
I have recently released a new addon - Geoblock Registration, for which I have written unit tests which are available for viewing in the source code on Bitbucket (the unit tests are not included in the addon release files) - https://bitbucket.org/hampel/geoblock-xenforo/src/master/tests/Unit/

Hopefully this will give people some ideas about how you might go about writing unit tests for your own addons using this testing framework.

Each test file concentrates on a single class and each method tests a specific code path through one of the public methods.

I have another new addon which I will be releasing in the next week or two which will also have extensive unit testing available - I will post a link to the source code once that is available - it will utilise some other aspects of the framework not used by this addon.
 

Sim

Well-known member
Sim updated Unit testing XenForo addons - tutorial with a new update entry:

v1.2.0 updates

  • Feature: added new functionality to Interacts with Container
    • mockService
  • Feature: Interacts with Http, new function:
    • fakesHttp

mockService
Mock a service factory builder in the container.

Parameters
  • shortName - the short name of the service class to be mocked
  • mock - optional - the mock closure to define expectations on
Example:
PHP:
<?php namespace Tests\Unit;

use...

Read the rest of this update entry...
 

Earl

Active member
This guide is amazing. I always wanted to use unittests with xenforo
 
Last edited:
  • Like
Reactions: Sim

Earl

Active member
for developers who use docker,
I'm sharing this zsh function.
You can add this to .zshrc
Bash:
xfcupdate()
{
    if [[ ! -a composer.json ]]; then
        <<EOF > composer.json
{
  "require-dev": {
    "hampel/xenforo-test-framework": "^1.0",
    "nesbot/carbon": "^2.25"
  },
  "autoload-dev": {
    "psr-4": {
      "Tests\\": "tests/"
    }
  }
}

EOF
echo composer.json has been created.

else
echo composer.json is already exists.
    fi

    docker exec -it xf2-dev_xf2_1 composer update -d src/addons/$1 #modify this code and put your docker container name
    docker exec -it xf2-dev_xf2_1 cp -r src/addons/$1/vendor/hampel/xenforo-test-framework/tests  src/addons/$1/ #modify this code and put your docker container name
}

then all you have to do is, navigate into your add-on root (cd /srv/www/xenforo/src/addons/Vendorly/Addonista/), execute xfcupdate vendor/add-onID (xfcupdate Vendorly/Addonista) command in terminal.
It will create the composer.json file and execute composer update in docker.
You have to put your docker container name in that zsh function. (Docker users know what they are doing)
 

Earl

Active member
@Sim
Do I have to call parent::setUp() if i need to use setUp() function?

PHP:
protected function setUp(): void
{
    parent::setUp();

    $this->validator = \XF::app()->validator('email');

}
otherwise it's passing an error:
Error: Class 'XF' not found
 

Sim

Well-known member
@Sim
Do I have to call parent::setUp() if i need to use setUp() function?

PHP:
protected function setUp(): void
{
    parent::setUp();

    $this->validator = \XF::app()->validator('email');

}
otherwise it's passing an error:
Error: Class 'XF' not found

Yes, if you are over-riding the setUp function in your own test class, you absolutely must run parent::setUp(), otherwise the framework won't be instantiated. All of the magic happens in the parent setUp function.
 

Earl

Active member
Yes, if you are over-riding the setUp function in your own test class, you absolutely must run parent::setUp(), otherwise the framework won't be instantiated. All of the magic happens in the parent setUp function.
Oh, I see. So I put my test setups after the parent::setUp(); line and it seems fine.
But, what about the tearDown()?
I guess I have to do the same with tearDown(); But, where should I put my testing code tear down? Before parent::tearDown(); line or after?

Then again, things were working fine until I created the second test file.

I have $this->validator = $this->app()->validator('email'); in 1st test file setUp()

And I have $this->testUser = $this->app()->repository('XF:User')->getUserByNameOrEmail('Earl'); in second test file setUp()

when I execute unittest, it gives this error in terminal:


Code:
PHPUnit 8.5.5 by Sebastian Bergmann and contributors.

E..                                                                 3 / 3 (100%)

Time: 101 ms, Memory: 14.00 MB

There was 1 error:

1) Tests\Unit\EmailChangeTest::test_check_is_mail_changing_denied_or_not
LogicException: A second app cannot be setup. Tried to set Hampel\Testing\App after setting Hampel\Testing\App

/var/www/html/src/XF.php:337
/var/www/html/src/XF.php:363
/var/www/html/src/addons/Earl/IDVerification/tests/CreatesApplication.php:19
/var/www/html/src/addons/Earl/IDVerification/vendor/hampel/xenforo-test-framework/src/TestCase.php:108
/var/www/html/src/addons/Earl/IDVerification/tests/Unit/EmailChangeTest.php:27

ERRORS!
Tests: 3, Assertions: 44, Errors: 1.

I can only run tests individually by marking all other tests incomplete by adding these lines in setUp() except the one single test .

PHP:
        $this->markTestIncomplete(
            'This test has not been implemented yet.'
        );
I cannot test all at the same time. Is that a bug?
 

Sim

Well-known member
But, what about the tearDown()?
I guess I have to do the same with tearDown(); But, where should I put my testing code tear down? Before parent::tearDown(); line or after?

It doesn't really matter, but I suggest that you put your teardown code before the call to parent::tearDown() so that cleaning up the testing environment (in the parent) is the last thing that happens.

Then again, things were working fine until I created the second test file.

I have $this->validator = $this->app()->validator('email'); in 1st test file setUp()

And I have $this->testUser = $this->app()->repository('XF:User')->getUserByNameOrEmail('Earl'); in second test file setUp()

when I execute unittest, it gives this error in terminal:

Can you share your phpunit.xml file ?

I do recall coming across this issue, but I can't recall exactly what was causing it.

In the meantime, just make those calls directly i the test functions rather than at the setUp level.
 

Earl

Active member
It doesn't really matter, but I suggest that you put your teardown code before the call to parent::tearDown() so that cleaning up the testing environment (in the parent) is the last thing that happens.



Can you share your phpunit.xml file ?

I do recall coming across this issue, but I can't recall exactly what was causing it.

In the meantime, just make those calls directly i the test functions rather than at the setUp level.

I didn't change anything in it
XML:
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
         backupStaticAttributes="false"
         bootstrap="vendor/autoload.php"
         colors="true"
         convertErrorsToExceptions="true"
         convertNoticesToExceptions="true"
         convertWarningsToExceptions="true"
         processIsolation="false"
         stopOnFailure="false">
    <testsuites>
        <testsuite name="Unit">
            <directory suffix="Test.php">./tests/Unit</directory>
        </testsuite>

        <testsuite name="Feature">
            <directory suffix="Test.php">./tests/Feature</directory>
        </testsuite>
    </testsuites>
</phpunit>

In the meantime, just make those calls directly i the test functions rather than at the setUp level.
Hah! The error disappeared after cleaning SetUp() and tearDown() methods.
Then again, I put everything back to SetUp() but followed this part of your instruction about tearDown()
I suggest that you put your teardown code before the call to parent::tearDown() so that cleaning up the testing environment (in the parent) is the last thing that happens.

The Error disappeared again. 😊
I don't know what was causing the issue. Be that as it may, the error wouldn't come as long as I put that parent::tearDown(); line to the end.
 
Last edited:
  • Like
Reactions: Sim
Top