📜 ⬆️ ⬇️

Unit testing in PHP

PHP language is very easy to learn. This, as well as the abundance of literature “Osovoy __ friendly for 24 hours” has generated a large number, to put it mildly, of poor-quality code. As a result, sooner or later, any programmer who goes beyond creating a guest book or a business card website is faced with the question: “But if I add a little here, everything else will not lie down?” Writing an answer to this question and many others can unit- testing.

At the very beginning I want to make a reservation - here we will not talk about TDD and software development methodologies. In this article I will try to show a beginner PHP developer the basics of using unit testing based on the PHPUnit framework.


Instead of the preface


At first, a quite reasonable question arises: Why, if everything works anyway?
Everything is simple here. After the next change, something will stop working as it should - I promise you. And then the search for errors can take a lot of time. Unit testing can reduce this process to a matter of minutes. We will also not exclude moving to another platform and the associated “pitfalls”. That only is the difference in the accuracy of calculations on 64-and 32-bit systems. And once, for the first time, you come across nuances like
')
<?php
print ( int ) ( ( 0.1 + 0.7 ) * 10 ) ;
// 8? ...
// : " BC Math? !"



If the question of necessity has disappeared, then you can proceed to the selection of the tool. Here the range is generally small - PHPUnit ( http://www.phpunit.de/ ) and SimpleTest ( http://www.simpletest.org/ ). Since PHPUnit has already become the de facto standard in testing PHP applications, we’ll dwell on it.

First meeting


Installation issues will not be considered: everything is quite clearly stated on the site . Let's try to better understand how this all works.

Suppose we have a certain class “MyClass”, one of the methods that implements the construction of a number to a power. (I have to apologize here, but all the examples, in general, are sucked from the finger)

MyClass.php
<?php
class MyClass {

public function power ( $x , $y )
{
return pow ( $x , $y ) ;
}
}



I want to check whether it works correctly. On the whole class it is not yet - only one method. We write to test such a test.

MyClassTest.php
<?php
require_once 'PHPUnit/Framework.php' ;
require_once 'MyClass.php' ;

class MyClassTest extends PHPUnit_Framework_TestCase {
public function testPower ( )
{
$my = new MyClass ( ) ;
$this -> assertEquals ( 8 , $my -> power ( 2 , 3 ) ) ;
}
}



A small digression about the format of tests.

Now, looking at the test, we can understand that by testing the exponentiation method we create an instance of the class, call the method we need with predetermined values ​​and check whether the calculations were performed correctly. For this check, the assertEquals () method was used, which takes the expected value as the first required parameter, the actual value as the second and checks if they match. Having stretched our brains and refreshing our knowledge of the multiplication table, we assumed that 2 3 = 8. With this data we will check how our method works.
Run the test:

$ phpunit MyClassTest
.
Time: 0 seconds
OK (1 test, 1 assertion)



The result of the "OK" test. It would seem possible to dwell on this, but sometimes it would be nice to check how many data sets are fed to the method. PHPUnit gives us this opportunity - these are data providers. The data provider is also a public method (the name is irrelevant), which returns an array of data sets for each iteration. To use the provider, you must specify it in the tag @dataProvider to the test.

Let's change our test as follows:

MyClassTest.php
<?php
require_once 'PHPUnit/Framework.php' ;
require_once 'MyClass.php' ;

class MyClassTest extends PHPUnit_Framework_TestCase {

/**
* @dataProvider providerPower
*/

public function testPower ( $a , $b , $c )
{
$my = new MyClass ( ) ;
$this -> assertEquals ( $c , $my -> power ( $a , $b ) ) ;
}

public function providerPower ( )
{
return array (
array ( 2 , 2 , 4 ) ,
array ( 2 , 3 , 9 ) ,
array ( 3 , 5 , 243 )
) ;
}
}



After launch, we will see the following picture:

.F.
Time: 0 seconds
There was 1 failure:
1) testPower(MyClassTest) with data set #1 (2, 3, 9)
Failed asserting that <integer:8> matches expected value <integer:9>.
/home/user/unit/MyClassTest.php:14
FAILURES!
Tests: 3, Assertions: 3, Failures: 1.



I will describe in more detail. The point that many people could have mistaken for a typo in the first test is actually not - this is a successfully passed test. F (ailure) - respectively, the test is not passed. So in this case, 3 tests were conducted, one of which failed. In the extended description we were told which one, with which set of input data, with which real and with what expected result. If 2 3 really were 9, then we would see an error in our scenario in this way.

Here, it seems to me, it makes sense to digress from a few abstract practices and go to a very specific theory. Namely, to describe what kind of assert-methods we have for checking the behavior of the tested scripts.

The two most simple ones are assertFalse () and assertTrue () . Check whether the resulting value is false and true, respectively. Next come the already mentioned assertEquals () and the reverse one assertNotEquals () . There are nuances in their use. So when comparing floating-point numbers, it is possible to specify the accuracy of the comparison. Also, these methods are used to compare instances of the DOMDocument class, arrays, and any objects (in the latter case, equality will be established if the attributes of the objects contain the same values). It is also worth mentioning assertNull () and assertNotNull () that check if the parameter matches the data type NULL (yes, do not forget that in PHP it is a separate data type). Possible comparisons are not limited to this. There is no point in the framework of this article to engage in the reprint of the documentation, therefore I will give, if possible, a structured list of all possible methods. More details can be found here.

Basic comparison methods
assertTrue () / assertFalse ()
assertEquals () / assertNotEquals ()
assertGreaterThan ()
assertGreaterThanOrEqual ()
assertLessThan ()
assertLessThanOrEqual ()
assertNull () / assertNotNull ()
assertType () / assertNotType ()
assertSame () / assertNotSame ()
assertRegExp () / assertNotRegExp ()

Array comparison methods
assertArrayHasKey () / assertArrayNotHasKey ()
assertContains () / assertNotContains ()
assertContainsOnly () / assertNotContainsOnly ()

OOP specific methods
assertClassHasAttribute () / assertClassNotHasAttribute ()
assertClassHasStaticAttribute () / assertClassNotHasStaticAttribute ()
assertAttributeContains () / assertAttributeNotContains ()
assertObjectHasAttribute () / assertObjectNotHasAttribute ()
assertAttributeGreaterThan ()
assertAttributeGreaterThanOrEqual ()
assertAttributeLessThan ()
assertAttributeLessThanOrEqual ()

File comparison methods
assertFileEquals () / assertFileNotEquals ()
assertFileExists () / assertFileNotExists ()
assertStringEqualsFile () / assertStringNotEqualsFile ()

XML comparison methods
assertEqualXMLStructure ()
assertXmlFileEqualsXmlFile () / assertXmlFileNotEqualsXmlFile ()
assertXmlStringEqualsXmlFile () / assertXmlStringNotEqualsXmlFile ()
assertXmlStringEqualsXmlString () / assertXmlStringNotEqualsXmlString ()

miscellanea
assertTag ()
assertThat ()

Exceptions


If you are not tired yet, then we will return to the practice, namely to exception handling. To begin with, we modify our test class — we introduce into it a method that will throw this exception.

MyClass.php
<?php
class MathException extends Exception { } ;

class MyClass {

// ...

public function divide ( $x , $y )
{
if ( ! ( boolean ) $y )
{
throw new MathException ( 'Division by zero' ) ;
}
return $x / $y ;
}
}



Now you need to create a test that will complete successfully if this exception is thrown on a specific set of data. You can set the required exception in at least two ways - by adding @expectedException to the test or by calling the setExpectedException () method in the test.

MyClassTest.php
<?php
require_once 'PHPUnit/Framework.php' ;
require_once 'MyClass.php' ;

class MyClassTest extends PHPUnit_Framework_TestCase {

// ...

/**
* @expectedException MathException
*/

public function testDivision1 ( )
{
$my = new MyClass ( ) ;
$my -> divide ( 8 , 0 ) ;
}

public function testDivision2 ( )
{
$this -> setExpectedException ( 'MathException' ) ;
$my = new MyClass ( ) ;
$my -> divide ( 8 , 0 ) ;
}
}



Tests, in general, are absolutely identical. The choice of the method is left to your discretion. In addition to the mechanisms provided directly by PHPUnit, for testing exceptions, you can use the standard try {...} catch (), for example, like this:

MyClassTest.php
<?php
require_once 'PHPUnit/Framework.php' ;
require_once 'MyClass.php' ;

class MyClassTest extends PHPUnit_Framework_TestCase {

// ...

public function testDivision3 ( )
{
$my = new MyClass ( ) ;
try {
$my -> divide ( 8 , 2 ) ;
} catch ( MathException $e ) {
return ;
}
$this -> fail ( 'Not raise an exception' ) ;
}
}



In this example, we also see a method of test termination not considered earlier by calling the method fail () . The test output will be as follows:

F
Time: 0 seconds
There was 1 failure:
1) testDivision3(MyClassTest)
Not raise an exception
/home/user/unit/MyClassTest.php:50



Accessories


We have mastered the basic testing methods. Is it possible to improve our test? Yes. The class written from the beginning of this article conducts several tests, each of which creates an instance of the class being tested, which is absolutely unnecessary, because PHPUnit provides the fixtures mechanism for our use. You can set them using the protected setUp () method, which is called once before each test begins. After the end of the test, the tearDown () method is called , in which we can do garbage collection. Thus, the corrected test may look like this:

MyClassTest.php
<?php
require_once 'PHPUnit/Framework.php' ;
require_once 'MyClass.php' ;

class MyClassTest extends PHPUnit_Framework_TestCase {

protected $fixture ;

protected function setUp ( )
{
$this -> fixture = new MyClass ( ) ;
}

protected function tearDown ( )
{
$this -> fixture = NULL ;
}

/**
* @dataProvider providerPower
*/

public function testPower ( $a , $b , $c )
{
$this -> assertEquals ( $c , $this -> fixture -> power ( $a , $b ) ) ;
}

public function providerPower ( )
{
return array (
array ( 2 , 2 , 4 ) ,
array ( 2 , 3 , 8 ) ,
array ( 3 , 5 , 243 )
) ;
}

// …

}



Test suites


After the code of several classes will be covered by tests, it becomes rather inconvenient to run each test separately. Here test sets can come to our aid - several tests connected by a single task can be combined into a set and run accordingly. Sets are implemented by the PHPUnit_Framework_TestSuite class. You need to create an instance of this class and add the necessary tests to it using the addTestSuite () method. Also with the help of the addTest () method it is possible to add another set.

SpecificTests.php
<?php
require_once 'PHPUnit/Framework.php' ;
//
require_once 'MyClassTest.php' ;

class SpecificTests
{
public static function suite ( )
{
$suite = new PHPUnit_Framework_TestSuite ( 'MySuite' ) ;
//
$suite -> addTestSuite ( 'MyClassTest' ) ;
return $suite ;
}
}



AllTests.php
<?php
require_once 'PHPUnit/Framework.php' ;
//
require_once 'SpecificTests.php' ;

class AllTests
{
public static function suite ( )
{
$suite = new PHPUnit_Framework_TestSuite ( 'AllMySuite' ) ;
//
$suite -> addTest ( SpecificTests :: suite ( ) ) ;
return $suite ;
}
}



Now imagine a set of tests for a script that works with the database. Do we really have to connect to the database in each test? No - do not have to. To do this, you can create your own class inherited from PHPUnit_Framework_TestSuite , define its setUp () and tearDown () methods to initialize the database interface and simply pass it to the test using the sharedFixture attribute. We will leave the databases for later, but for now we will try to create our own set of tests for an existing class.

MyClassTest.php
<?php
require_once 'PHPUnit/Framework.php' ;
require_once 'MyClass.php' ;

class MyClassTest extends PHPUnit_Framework_TestCase {

protected $fixture ;

protected function setUp ( )
{
$this -> fixture = $this -> sharedFixture ;
}

protected function tearDown ( )
{
$this -> fixture = NULL ;
}

/**
* @dataProvider providerPower
*/

public function testPower ( $a , $b , $c )
{
$this -> assertEquals ( $c , $this -> fixture -> power ( $a , $b ) ) ;
}

public function providerPower ( )
{
return array (
array ( 2 , 2 , 4 ) ,
array ( 2 , 3 , 8 ) ,
array ( 3 , 5 , 243 )
) ;
}

// …

}



MySuite.php
<?php
require_once 'MyClassTest.php' ;

class MySuite extends PHPUnit_Framework_TestSuite {

protected $sharedFixture ;

public static function suite ( )
{
$suite = new MySuite ( 'MyTests' ) ;
$suite -> addTestSuite ( 'MyClassTest' ) ;
return $suite ;
}

protected function setUp ( )
{
$this -> sharedFixture = new MyClass ( ) ;
}

protected function tearDown ( )
{
$this -> sharedFixture = NULL ;
}

}



Here we put a copy of the class under test in sharedFixture , and in the test we simply used it - the solution is not very beautiful (I would even say, not beautiful at all), but it gives a general idea of ​​test suites and transfer of accessories between tests. If you visualize the sequence of the method call, you get something like this:

MySuite::setUp()
MyClassTest::setUp()
MyClassTest::testPower()
MyClassTest::tearDown()
MyClassTest::setUp()
MyClassTest::testDivision()
MyClassTest::tearDown()
...
MySuite::tearDown()



Additional features


In addition to all the above, it may be necessary to check not only the calculations and the behavior of the script, but also the output and speed of testing. For this purpose, the extensions PHPUnit_Extensions_OutputTestCase and PHPUnit_Extensions_PerformanceTestCase respectively are provided. Let's add one more method to our tested class, and check if it works correctly.

MyClass.php
<?php
class MyClass {

// ...

public function square ( $x )
{
sleep ( 2 ) ;
print $x * $x ;
}

}



MyClassTest.php
<?php
require_once 'PHPUnit/Framework.php' ;
require_once 'PHPUnit/Extensions/OutputTestCase.php' ;
require_once 'PHPUnit/Extensions/PerformanceTestCase.php' ;
require_once 'MyClass.php' ;

class MyClassOutputTest extends PHPUnit_Extensions_OutputTestCase {

protected $fixture ;

protected function setUp ( )
{
$this -> fixture = $this -> sharedFixture ;
}

protected function tearDown ( )
{
$this -> fixture = NULL ;
}

public function testSquare ( )
{
$this -> expectOutputString ( '4' ) ;
$this -> fixture -> square ( 2 ) ;
}
}

class MyClassPerformanceTest extends PHPUnit_Extensions_PerformanceTestCase {

protected $fixture ;

protected function setUp ( )
{
$this -> fixture = $this -> sharedFixture ;
}

protected function tearDown ( )
{
$this -> fixture = NULL ;
}

public function testPerformance ( )
{
$this -> setMaxRunningTime ( 1 ) ;
$this -> fixture -> square ( 4 ) ;
}
}

class MyClassTest extends PHPUnit_Framework_TestCase {

// …

}



You can set the expected output of the script using the expectOutputString () or expectOutputRegex () methods. And for the setMaxRunningTime () method, the estimated time to work is specified in seconds. In order for these tests to run along with those already written, they just need to be added to our suite:

MySuite.php
<?php
require_once 'MyClassTest.php' ;

class MySuite extends PHPUnit_Framework_TestSuite {

protected $sharedFixture ;

public static function suite ( )
{
$suite = new MySuite ( 'MyTests' ) ;
$suite -> addTestSuite ( 'MyClassTest' ) ;
$suite -> addTestSuite ( 'MyClassOutputTest' ) ;
$suite -> addTestSuite ( 'MyClassPerformanceTest' ) ;
return $suite ;
}

// ...

}



Skipping tests


And finally, let's consider a situation in which some tests need to be skipped for some reason. For example, in the case when there is no PHP extension on the tested machine, you can make sure that it doesn’t mark the test as missed by adding the following to its code:

if ( ! extension_loaded ( 'someExtension' ) ) {
$this -> markTestSkipped ( 'Extension is not loaded.' ) ;
}



Or in the case when the test is written for code that is not already in the script (not a rare situation for TDD), it can be marked as not implemented using the markTestIncomplete () method

At last


Probably at this point you can stop. The topic of unit testing of this article is far from complete - there is still the use of mock objects, testing database operations, code coverage analysis and much more. But I hope that the goal - to familiarize beginners with the basic features of PHPUnit and push to use unit tests, as one of the means to achieve greater efficiency - has been achieved.
Good luck, and stable applications.

Source: https://habr.com/ru/post/56289/


All Articles