I’ve been working on Drupal 8 for some time, and was curious about PHPUnit and the value it brings to module development. PHPUnit lets developers write code with full confidence using the TDD (Test Driven Development) approach. According to TDD: “All code is guilty until proven innocent.” It’s a very good idea to have some tests ready which can tell us about any bugs or mistakes while we’re developing our application. This would let us fix them on the fly, and allow us to push the developed code to production with full confidence. So let’s get started.
What is a Unit and a Unit Test?
The word ‘unit’ refers to a block of code, method or an individual or independent class. A ‘unit test’ is a test case to check if your unit is benign and will not harm the application. Unit testing is the process which uncovers bugs in a single block of code. Simply, it’s a test to see if our code works.
What is PHPUnit and why use it?
We cannot ensure that all bugs have been resolved in our code. Even after thorough testing, it is possible that some bugs still remain uncovered. Another possibility is that we may end up introducing new bugs while trying to fix existing ones, or while adding new features. This leads to rework and can prove time-consuming.
One of the test tools to help address the problem above is PHPUnit, with which we can write unit tests in PHP. It is one of the most popular testing frameworks for PHP. (This is not to say that manual testing is not an essential part of the process.)
Here are some of the benefits of PHPUnit.
- It takes only a few seconds to run PHPUnit tests. Running a large number of tests might take longer.
- Regression testing efforts are significantly reduced.
- Code refactoring ensures that nothing gets broken when you change something.
- The probability of code being bug-free is higher.
Test Driven Development (TDD)
Test Driven Development is the process of writing unit tests first (wanting them to fail) and then writing the code and refactoring it to make the tests pass. This kind of approach helps us build our unit code with what is actually required in it.
Laws of TDD
- Write a test that fails.
- Write the logic to pass the test.
- Refactor the logic.
The benefits of doing this include:
- pushes the developer to plan the functionality ahead of time.
- encourages developers to choose methods that are simple, concise, and testable.
- Drupal 8 code base
Installing PHPUnit for Drupal 8 with the help of composer
The recommended way to install PHPUnit is with composer, which is the dependency manager.
In the root directory of the Drupal project, run the following command(s):
Or if you want only PHPUnit to be part of your project, then you can run the following command.
composer require --dev phpunit/phpunit
So, with that done, we should have PHPUnit installed for our project. Let’s check by running the following command:
If it returns something like PHPUnit 4.8.36 by Sebastian Bergmann and contributors, then congratulations! You have just installed PHPUnit.
Before we dive into writing tests, we need to follow some basic rules. These rules are required conventions to write tests with PHPUnit.
- Unit test class file should fall under Unit dir.
- Unit test class should extend UnitTestCase class.
- Unit test file name always follows pattern *Test.php
- All test member functions are public.
- Method name follows pattern test*
e.g.: testSetName(), testGetName()
Structuring the project
This ensures that our tests are identified automatically. Create a custom module, or if you already have one, then you can make use of that to start writing tests. I will create a new module for the files to be tested. My custom module (phpunit_example) directory structure looks like this:
Writing our first Unit Test
Create a new file at: phpunit_example/src/Unit.php
Create a new file at: phpunit_example/tests/src/Unit/UnitTest.php
In UnitTest.php we need to extend UnitTestCase in our UnitTest class.
These are what you add to your tests to 'assert' that something is valid, for example, an AssertEquals would be used to check the result against another input for a match. The best practice is to have one assertion for each test.
In PHPUnit testing we have some useful methods which we can use to set up the environment for our tests.
The setUp() method is called before every test. In the setUp() method, we are initializing $unit object which will be used in every test.
tearDown() is where you clean up the objects against which you tested. In the tearDown() method, we are just clearing the objects when our test method has finished running.
With the testSetLength() method, we are asserting that the initial value of $length is 0 and then assigning value 9 to $length variable. Next, we are checking the assigned value by using the assertEquals() method.
The assertEquals() method takes two arguments. The first argument is an expected value and the second argument is the value that we want to check. As 9 is equal to 9, so the assertEquals() method gives us a successful test result.
With the testGetLength() method we are setting the $length to 9 and checking if it is not equal to 10 using the assertNotEquals() method.
The assertNotEquals() method takes two arguments. The first argument is an expected value and the second argument is the value that we want to check. As 9 != 10, the assertNotEquals() method gives us a successful test result.
Annotations are metadata attached to your source code which are read at runtime. We have used the following annotations in our example. Learn more about annotations here.
- @covers: specifies that a given test “covers” a segment of code
- @group: specifies that a given test belongs to a “group”.
Please refer to @Annotations for more useful annotations.
Running a PHPUnit test
There are different ways in which we can run our PHPUnit tests. We can either run a full unit test, a test suite or one specific test as shown in following commands:
To run all the tests available from the custom modules directory:
vendor/bin/phpunit -c core/phpunit.xml.dist modules/custom
Here, phpunit.xml.dist is the configuration file for PHPUnit. This holds various attributes like bootstrap, color, etc, which helps PHPUnit to perform tests.
To run all tests (Unit, Kernel, Functional) from a particular module:
vendor/bin/phpunit -c core/phpunit.xml.dist modules/custom/phpunit_example
To run only all unit tests from a specific module:
vendor/bin/phpunit -c core/phpunit.xml.dist modules/custom/phpunit_example/tests/src/Unit
To run a specific unit test:
vendor/bin/phpunit -c core/phpunit.xml.dist modules/custom/phpunit_example/tests/src/Unit/UnitTest.php
To run a specific group test:
vendor/bin/phpunit -c core/phpunit.xml.dist modules/custom --group phpunit_example
Setting up our own PHPUnit.xml
We can have our own php-unit.xml where we specify the tests which we intend to run. To do this, copy the core/php-unit.xml.dist file and paste it into the root folder of your project and replace its contents with the following:
Now run vendor/bin/phpunit, and that’s it. You are now able to run your tests from the project root. Awesome!
This is a very simple configuration file, but it sets two important properties: <directory>modules/custom/phpunit_example/tests</directory> tells PHPUnit where your tests will be located, and colors="true" makes sure your test results are in color. So we don’t need to specify this explicitly every time we run our tests.
There are other attributes as well, but I have used only two for this example. You can explore more in your project.
In our example, we have created a test-suite unit in php-unit.xml.dist. To run a specific test suite, run the following command:
vendor/bin/phpunit --testsuite unit
Output for all our tests performed
PHPUnit 4.8.36 by Sebastian Bergmann and contributors.
Time: 350 ms, Memory: 6.00MB
OK (2 tests, 3 assertions)
This should help you get started with PHPUnit testing with Drupal 8. You now know how to set up PHPUnit with your Drupal 8 project, test basics, and writing and running unit tests. Tests can be enhanced and made heavier and more complex depending upon the application. We will also explore the use of mocking in future posts. You can learn more from the documentation and follow examples.
There is a downside too with unit tests—they don’t guarantee that several components or the whole system actually work as intended for the end user. In such cases, we need kernel and browser (functional) tests, which we will cover in later posts.
If you have a question or would like to contribute to the discussion, please leave a comment.