Table of Contents for
PHP 7: Real World Application Development

Version ebook / Retour

Cover image for bash Cookbook, 2nd Edition PHP 7: Real World Application Development by Branko Ajzele Published by Packt Publishing, 2016
  1. Cover
  2. Table of Contents
  3. PHP 7: Real World Application Development
  4. PHP 7: Real World Application Development
  5. PHP 7: Real World Application Development
  6. Credits
  7. Preface
  8. What you need for this learning path
  9. Who this learning path is for
  10. Reader feedback
  11. Customer support
  12. 1. Module 1
  13. 1. Building a Foundation
  14. PHP 7 installation considerations
  15. Using the built-in PHP web server
  16. Defining a test MySQL database
  17. Installing PHPUnit
  18. Implementing class autoloading
  19. Hoovering a website
  20. Building a deep web scanner
  21. Creating a PHP 5 to PHP 7 code converter
  22. 2. Using PHP 7 High Performance Features
  23. Understanding the abstract syntax tree
  24. Understanding differences in parsing
  25. Understanding differences in foreach() handling
  26. Improving performance using PHP 7 enhancements
  27. Iterating through a massive file
  28. Uploading a spreadsheet into a database
  29. Recursive directory iterator
  30. 3. Working with PHP Functions
  31. Developing functions
  32. Hinting at data types
  33. Using return value data typing
  34. Using iterators
  35. Writing your own iterator using generators
  36. 4. Working with PHP Object-Oriented Programming
  37. Developing classes
  38. Extending classes
  39. Using static properties and methods
  40. Using namespaces
  41. Defining visibility
  42. Using interfaces
  43. Using traits
  44. Implementing anonymous classes
  45. 5. Interacting with a Database
  46. Using PDO to connect to a database
  47. Building an OOP SQL query builder
  48. Handling pagination
  49. Defining entities to match database tables
  50. Tying entity classes to RDBMS queries
  51. Embedding secondary lookups into query results
  52. Implementing jQuery DataTables PHP lookups
  53. 6. Building Scalable Websites
  54. Creating a generic form element generator
  55. Creating an HTML radio element generator
  56. Creating an HTML select element generator
  57. Implementing a form factory
  58. Chaining $_POST filters
  59. Chaining $_POST validators
  60. Tying validation to a form
  61. 7. Accessing Web Services
  62. Converting between PHP and XML
  63. Creating a simple REST client
  64. Creating a simple REST server
  65. Creating a simple SOAP client
  66. Creating a simple SOAP server
  67. 8. Working with Date/Time and International Aspects
  68. Using emoticons or emoji in a view script
  69. Converting complex characters
  70. Getting the locale from browser data
  71. Formatting numbers by locale
  72. Handling currency by locale
  73. Formatting date/time by locale
  74. Creating an HTML international calendar generator
  75. Building a recurring events generator
  76. Handling translation without gettext
  77. 9. Developing Middleware
  78. Authenticating with middleware
  79. Using middleware to implement access control
  80. Improving performance using the cache
  81. Implementing routing
  82. Making inter-framework system calls
  83. Using middleware to cross languages
  84. 10. Looking at Advanced Algorithms
  85. Using getters and setters
  86. Implementing a linked list
  87. Building a bubble sort
  88. Implementing a stack
  89. Building a binary search class
  90. Implementing a search engine
  91. Displaying a multi-dimensional array and accumulating totals
  92. 11. Implementing Software Design Patterns
  93. Creating an array to object hydrator
  94. Building an object to array hydrator
  95. Implementing a strategy pattern
  96. Defining a mapper
  97. Implementing object-relational mapping
  98. Implementing the Pub/Sub design pattern
  99. 12. Improving Web Security
  100. Filtering $_POST data
  101. Validating $_POST data
  102. Safeguarding the PHP session
  103. Securing forms with a token
  104. Building a secure password generator
  105. Safeguarding forms with a CAPTCHA
  106. Encrypting/decrypting without mcrypt
  107. 13. Best Practices, Testing, and Debugging
  108. Using Traits and Interfaces
  109. Universal exception handler
  110. Universal error handler
  111. Writing a simple test
  112. Writing a test suite
  113. Generating fake test data
  114. Customizing sessions using session_start parameters
  115. A. Defining PSR-7 Classes
  116. Implementing PSR-7 value object classes
  117. Developing a PSR-7 Request class
  118. Defining a PSR-7 Response class
  119. 2. Module 2
  120. 1. Setting Up the Environment
  121. Setting up Debian or Ubuntu
  122. Setting up CentOS
  123. Setting up Vagrant
  124. Summary
  125. 2. New Features in PHP 7
  126. New operators
  127. Uniform variable syntax
  128. Miscellaneous features and changes
  129. Summary
  130. 3. Improving PHP 7 Application Performance
  131. HTTP server optimization
  132. HTTP persistent connection
  133. Content Delivery Network (CDN)
  134. CSS and JavaScript optimization
  135. Full page caching
  136. Varnish
  137. The infrastructure
  138. Summary
  139. 4. Improving Database Performance
  140. Storage engines
  141. The Percona Server - a fork of MySQL
  142. MySQL performance monitoring tools
  143. Percona XtraDB Cluster (PXC)
  144. Redis – the key-value cache store
  145. Memcached key-value cache store
  146. Summary
  147. 5. Debugging and Profiling
  148. Profiling with Xdebug
  149. PHP DebugBar
  150. Summary
  151. 6. Stress/Load Testing PHP Applications
  152. ApacheBench (ab)
  153. Siege
  154. Load testing real-world applications
  155. Summary
  156. 7. Best Practices in PHP Programming
  157. Test-driven development (TDD)
  158. Design patterns
  159. Service-oriented architecture (SOA)
  160. Being object-oriented and reusable always
  161. PHP frameworks
  162. Version control system (VCS) and Git
  163. Deployment and Continuous Integration (CI)
  164. Summary
  165. A. Tools to Make Life Easy
  166. Git – A version control system
  167. Grunt watch
  168. Summary
  169. B. MVC and Frameworks
  170. Laravel
  171. Lumen
  172. Apigility
  173. Summary
  174. 3. Module 3
  175. 1. Ecosystem Overview
  176. Summary
  177. 2. GoF Design Patterns
  178. Structural patterns
  179. Behavioral patterns
  180. Summary
  181. 3. SOLID Design Principles
  182. Open/closed principle
  183. Liskov substitution principle
  184. Interface Segregation Principle
  185. Dependency inversion principle
  186. Summary
  187. 4. Requirement Specification for a Modular Web Shop App
  188. Wireframing
  189. Defining a technology stack
  190. Summary
  191. 5. Symfony at a Glance
  192. Creating a blank project
  193. Using Symfony console
  194. Controller
  195. Routing
  196. Templates
  197. Forms
  198. Configuring Symfony
  199. The bundle system
  200. Databases and Doctrine
  201. Testing
  202. Validation
  203. Summary
  204. 6. Building the Core Module
  205. Dependencies
  206. Implementation
  207. Unit testing
  208. Functional testing
  209. Summary
  210. 7. Building the Catalog Module
  211. Dependencies
  212. Implementation
  213. Unit testing
  214. Functional testing
  215. Summary
  216. 8. Building the Customer Module
  217. Dependencies
  218. Implementation
  219. Unit testing
  220. Functional testing
  221. Summary
  222. 9. Building the Payment Module
  223. Dependencies
  224. Implementation
  225. Unit testing
  226. Functional testing
  227. Summary
  228. 10. Building the Shipment Module
  229. Dependencies
  230. Implementation
  231. Unit testing
  232. Functional testing
  233. Summary
  234. 11. Building the Sales Module
  235. Dependencies
  236. Implementation
  237. Unit testing
  238. Functional testing
  239. Summary
  240. 12. Integrating and Distributing Modules
  241. Understanding GitHub
  242. Understanding Composer
  243. Understanding Packagist
  244. Summary
  245. Bibliography
  246. Index

Writing a simple test

The primary means of testing PHP code is to use PHPUnit, which is based on a methodology called Unit Testing. The philosophy behind unit testing is quite simple: you break down your code into the smallest possible logical units. You then test each unit in isolation to confirm that it performs as expected. These expectations are codified into a series of assertions. If all assertions return TRUE, then the unit has passed the test.

Note

In the case of procedural PHP, a unit is a function. For OOP PHP, the unit is a method within a class.

How to do it...

  1. The first order of business is to either install PHPUnit directly onto your development server, or download the source code, which is available in the form of a single phar (PHP archive) file. A quick visit to the official website for PHPUnit (https://phpunit.de/) lets us download right from the main page.
  2. It is a best practice, however, to use a package manager to both install and maintain PHPUnit. For this purpose, we will use a package management program called Composer. To install Composer, visit the main website, https://getcomposer.org/, and follow the instructions on the download page. The current procedure, at the time of writing, is as follows. Note that you need to substitute the hash of the current version in place of <hash>:
    php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
    php -r "if (hash_file('SHA384', 'composer-setup.php') === '<hash>') { 
        echo 'Installer verified'; 
    } else { 
        echo 'Installer corrupt'; unlink('composer-setup.php'); 
    } echo PHP_EOL;"
    php composer-setup.php
    php -r "unlink('composer-setup.php');"

    Tip

    Best practice

    The advantage of using a package management program such as Composer is that it will not only install, but can also be used to update any external software (such as PHPUnit) used by your application.

  3. Next, we use Composer to install PHPUnit. This is accomplished by creating a composer.json file that contains a series of directives outlining project parameters and dependencies. A full description of these directives is beyond the scope of this book; however, for the purposes of this recipe, we create a minimal set of directives using the key parameter require. You will also note that the contents of the file are in JavaScript Object Notation (JSON) format:
    {
      "require-dev": {
        "phpunit/phpunit": "*"
      }
    }
  4. To perform the installation from the command line, we run the following command. The output is shown just after:
    php composer.phar install
    
    How to do it...
  5. PHPUnit and its dependencies are placed in a vendor folder that Composer will create if it does not already exist. The primary command to invoke PHPUnit is then symbolically linked into the vendor/bin folder. If you place this folder in your PATH, all you need do is to run this command, which checks the version and incidentally confirms the installation:
    phpunit --version
    

Running simple tests

  1. For the purposes of this illustration, let's assume we have a chap_13_unit_test_simple.php file that contains the add() function:
    <?php
    function add($a = NULL, $b = NULL)
    {
      return $a + $b;
    }
  2. Tests are then written as classes that extend PHPUnit\Framework\TestCase. If you are testing a library of functions, at the beginning of the test class, include the file that contains function definitions. You would then write methods that start with the word test, usually followed by the name of the function you are testing, and possibly some additional CamelCase words to further describe the test. For the purposes of this recipe, we will define a SimpleTest test class:
    <?php
    use PHPUnit\Framework\TestCase;
    require_once __DIR__ . '/chap_13_unit_test_simple.php';
    class SimpleTest extends TestCase
    {
      // testXXX() methods go here
    }
  3. Assertions form the heart of any set of tests. The See also section gives you the documentation reference for the complete list of assertions. An assertion is a PHPUnit method that compares a known value against a value produced by that which you wish to test. An example is assertEquals(), which checks to see whether the first argument equals the second. The following example tests a method called add() and confirms 2 is the return value for add(1,1):
    public function testAdd()
    {
      $this->assertEquals(2, add(1,1));
    }
  4. You can also test to see whether something is not true. This example asserts that 1 + 1 does not equal 3:
    $this->assertNotEquals(3, add(1,1));
  5. An assertion that is extremely useful when used to test a string is assertRegExp(). Assume, for this illustration, that we are testing a function that produces an HTML table out of a multidimensional array:
    function table(array $a)
    {
      $table = '<table>';
      foreach ($a as $row) {
        $table .= '<tr><td>';
        $table .= implode('</td><td>', $row);
        $table .= '</td></tr>';
      }
      $table .= '</table>';
      return $table;
    }
  6. We can construct a simple test that confirms that the output contains <table>, one or more characters, followed by </table>. Further, we wish to confirm that a <td>B</td> element exists. When writing the test, we build a test array that consists of three sub-arrays containing the letters A—C, D—F, and G—I. We then pass the test array to the function, and run assertions against the result:
    public function testTable()
    {
      $a = [range('A', 'C'),range('D', 'F'),range('G','I')];
      $table = table($a);
      $this->assertRegExp('!^<table>.+</table>$!', $table);
      $this->assertRegExp('!<td>B</td>!', $table);
    }
  7. To test a class, instead of including a library of functions, simply include the file that defines the class to be tested. For the sake of illustration, let's take the library of functions shown previously and move them into a Demo class:
    <?php
    class Demo
    {
      public function add($a, $b)
      {
        return $a + $b;
      }
    
      public function sub($a, $b)
      {
        return $a - $b;
      }
      // etc.
    }
  8. In our SimpleClassTest test class, instead of including the library file, we include the file that represents the Demo class. We need an instance of Demo in order to run tests. For this purpose, we use a specially designed setup() method, which is run before each test. Also, you will note a teardown() method, which is run immediately after each test:
    <?php
    use PHPUnit\Framework\TestCase;
    require_once __DIR__ . '/Demo.php';
    class SimpleClassTest extends TestCase
    {
      protected $demo;
      public function setup()
      {
        $this->demo = new Demo();
      }
      public function teardown()
      {
        unset($this->demo);
      }
      public function testAdd()
      {
        $this->assertEquals(2, $this->demo->add(1,1));
      }
      public function testSub()
      {
        $this->assertEquals(0, $this->demo->sub(1,1));
      }
      // etc.
    }

    Note

    The reason why setup() and teardown() are run before and after each test is to ensure a fresh test environment. That way, the results of one test will not influence the results of another test.

Testing database Model classes

  1. When testing a class, such as a Model class, that has database access, other considerations come into play. The main consideration is that you should run tests against a test database, not the real database used in production. A final point is that by using a test database, you can populate it in advance with appropriate, controlled data. setup() and teardown() could also be used to add or remove test data.
  2. As an example of a class that uses the database, we will define a class VisitorOps. The new class will include methods to add, remove, and find visitors. Note that we've also added a method to return the latest SQL statement executed:
    <?php
    require __DIR__ . '/../Application/Database/Connection.php';
    use Application\Database\Connection;
    class VisitorOps
    {
    
    const TABLE_NAME = 'visitors';
    protected $connection;
    protected $sql;
    
    public function __construct(array $config)
    {
      $this->connection = new Connection($config);
    }
    
    public function getSql()
    {
      return $this->sql;
    }
    
    public function findAll()
    {
      $sql = 'SELECT * FROM ' . self::TABLE_NAME;
      $stmt = $this->runSql($sql);
      while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
        yield $row;
      }
    }
    
    public function findById($id)
    {
      $sql = 'SELECT * FROM ' . self::TABLE_NAME;
      $sql .= ' WHERE id = ?';
      $stmt = $this->runSql($sql, [$id]);
      return $stmt->fetch(PDO::FETCH_ASSOC);
    }
    
    public function removeById($id)
    {
      $sql = 'DELETE FROM ' . self::TABLE_NAME;
      $sql .= ' WHERE id = ?';
      return $this->runSql($sql, [$id]);
    }
    
    public function addVisitor($data)
    {
      $sql = 'INSERT INTO ' . self::TABLE_NAME;
      $sql .= ' (' . implode(',',array_keys($data)) . ') ';
      $sql .= ' VALUES ';
      $sql .= ' ( :' . implode(',:',array_keys($data)) . ') ';
      $this->runSql($sql, $data);
      return $this->connection->pdo->lastInsertId();
    }
    
    public function runSql($sql, $params = NULL)
    {
      $this->sql = $sql;
      try {
          $stmt = $this->connection->pdo->prepare($sql);
          $result = $stmt->execute($params);
      } catch (Throwable $e) {
          error_log(__METHOD__ . ':' . $e->getMessage());
          return FALSE;
      }
      return $stmt;
    }
    }
  3. For tests that involve a database, it is recommended that you use a test database instead of the live production database. Accordingly, you will need an extra set of database connection parameters that can be used to establish a database connection in the setup() method.
  4. It's possible that you wish to establish a consistent block of sample data. This could be inserted into the test database in the setup() method.
  5. Finally, you may wish to reset the test database after each test, which is accomplished in the teardown() method.

Using mock classes

  1. In some cases, the test will access complex components that require external resources. An example is a service class that needs access to a database. It is a best practice to minimize database access in a test suite. Another consideration is that we are not testing database access; we are only testing the functionality of one specific class. Accordingly, it is sometimes necessary to define mock classes that mimic the behavior of the their parent class, but that restrict access to external resources.

    Tip

    Best practice

    Limit actual database access in your tests to the Model (or equivalent) classes. Otherwise, the time it takes to run the entire set of tests could become excessive.

  2. In this case, for illustration, define a service class, VisitorService, which makes use of the VisitorOps class discussed earlier:
    <?php
    require_once __DIR__ . '/VisitorOps.php';
    require_once __DIR__ . '/../Application/Database/Connection.php';
    use Application\Database\Connection;
    class VisitorService
    {
      protected $visitorOps;
      public function __construct(array $config)
      {
        $this->visitorOps = new VisitorOps($config);
      }
      public function showAllVisitors()
      {
        $table = '<table>';
        foreach ($this->visitorOps->findAll() as $row) {
          $table .= '<tr><td>';
          $table .= implode('</td><td>', $row);
          $table .= '</td></tr>';
        }
        $table .= '</table>';
        return $table;
      }
  3. For test purposes, we add a getter and setter for the $visitorOps property. This allows us to insert a mock class in place of the real VisitorOps class:
    public function getVisitorOps()
    {
      return $this->visitorOps;
    }
    
    public function setVisitorOps(VisitorOps $visitorOps)
    {
      $this->visitorOps = $visitorOps;
    }
    } // closing brace for VisitorService
  4. Next, we define a VisitorOpsMock mock class that mimics the functionality of its parent class. Class constants and properties are inherited. We then add mock test data, and a getter in case we need access to the test data later:
    <?php
    require_once __DIR__ . '/VisitorOps.php';
    class VisitorOpsMock extends VisitorOps
    {
      protected $testData;
      public function __construct()
      {
        $data = array();
        for ($x = 1; $x <= 3; $x++) {
          $data[$x]['id'] = $x;
          $data[$x]['email'] = $x . 'test@unlikelysource.com';
          $data[$x]['visit_date'] = 
            '2000-0' . $x . '-0' . $x . ' 00:00:00';
          $data[$x]['comments'] = 'TEST ' . $x;
          $data[$x]['name'] = 'TEST ' . $x;
        }
        $this->testData = $data;
      }
      public function getTestData()
      {
        return $this->testData;
      }
  5. Next, we override findAll() to return test data using yield, just as in the parent class. Note that we still build the SQL string, as this is what the parent class does:
    public function findAll()
    {
      $sql = 'SELECT * FROM ' . self::TABLE_NAME;
      foreach ($this->testData as $row) {
        yield $row;
      }
    }
  6. To mock findById() we simply return that array key from $this->testData. For removeById(), we unset the array key supplied as a parameter from $this->testData:
    public function findById($id)
    {
      $sql = 'SELECT * FROM ' . self::TABLE_NAME;
      $sql .= ' WHERE id = ?';
      return $this->testData[$id] ?? FALSE;
    }
    public function removeById($id)
    {
      $sql = 'DELETE FROM ' . self::TABLE_NAME;
      $sql .= ' WHERE id = ?';
      if (empty($this->testData[$id])) {
          return 0;
      } else {
          unset($this->testData[$id]);
          return 1;
      }
    }
  7. Adding data is slightly more complicated in that we need to emulate the fact that the id parameter might not be supplied, as the database would normally auto-generate this for us. To get around this, we check for the id parameter. If not set, we find the largest array key and increment:
    public function addVisitor($data)
    {
      $sql = 'INSERT INTO ' . self::TABLE_NAME;
      $sql .= ' (' . implode(',',array_keys($data)) . ') ';
      $sql .= ' VALUES ';
      $sql .= ' ( :' . implode(',:',array_keys($data)) . ') ';
      if (!empty($data['id'])) {
          $id = $data['id'];
      } else {
          $keys = array_keys($this->testData);
          sort($keys);
          $id = end($keys) + 1;
          $data['id'] = $id;
      }
        $this->testData[$id] = $data;
        return 1;
      }
    
    } // ending brace for the class VisitorOpsMock

Using anonymous classes as mock objects

  1. A nice variation on mock objects involves the use of the new PHP 7 anonymous class in place of creating a formal class that defines mock functionality. The advantage of using an anonymous class is that you can extend an existing class, which makes the object appear legitimate. This approach is especially useful if you only need to override one or two methods.
  2. For this illustration, we will modify VisitorServiceTest.php presented previously, calling it VisitorServiceTestAnonClass.php:
    <?php
    use PHPUnit\Framework\TestCase;
    require_once __DIR__ . '/VisitorService.php';
    require_once __DIR__ . '/VisitorOps.php';
    class VisitorServiceTestAnonClass extends TestCase
    {
      protected $visitorService;
      protected $dbConfig = [
        'driver'   => 'mysql',
        'host'     => 'localhost',
        'dbname'   => 'php7cookbook_test',
        'user'     => 'cook',
        'password' => 'book',
        'errmode'  => PDO::ERRMODE_EXCEPTION,
      ];
        protected $testData;
  3. You will notice that in setup(), we define an anonymous class that extends VisitorOps. We only need to override the findAll() method:
    public function setup()
    {
      $data = array();
      for ($x = 1; $x <= 3; $x++) {
        $data[$x]['id'] = $x;
        $data[$x]['email'] = $x . 'test@unlikelysource.com';
        $data[$x]['visit_date'] = 
          '2000-0' . $x . '-0' . $x . ' 00:00:00';
        $data[$x]['comments'] = 'TEST ' . $x;
        $data[$x]['name'] = 'TEST ' . $x;
      }
      $this->testData = $data;
      $this->visitorService = 
        new VisitorService($this->dbConfig);
      $opsMock = 
        new class ($this->testData) extends VisitorOps {
          protected $testData;
          public function __construct($testData)
          {
            $this->testData = $testData;
          }
          public function findAll()
          {
            return $this->testData;
          }
        };
        $this->visitorService->setVisitorOps($opsMock);
    }
  4. Note that in testShowAllVisitors(), when $this->visitorService->showAllVisitors() is executed, the anonymous class is called by the visitor service, which in turn calls the overridden findAll():
    public function teardown()
    {
      unset($this->visitorService);
    }
    public function testShowAllVisitors()
    {
      $result = $this->visitorService->showAllVisitors();
      $this->assertRegExp('!^<table>.+</table>$!', $result);
      foreach ($this->testData as $key => $value) {
        $dataWeWant = '!<td>' . $key . '</td>!';
        $this->assertRegExp($dataWeWant, $result);
      }
    }
    }

Using Mock Builder

  1. Another technique is to use getMockBuilder(). Although this approach does not allow a great deal of finite control over the mock object produced, it's extremely useful in situations where you only need to confirm that an object of a certain class is returned, and when a specified method is run, this method returns some expected value.
  2. In the following example, we copied VisitorServiceTestAnonClass; the only difference is in how an instance of VisitorOps is supplied in setup(), in this case, using getMockBuilder(). Note that although we did not use with() in this example, it is used to feed controlled parameters to the mocked method:
    <?php
    use PHPUnit\Framework\TestCase;
    require_once __DIR__ . '/VisitorService.php';
    require_once __DIR__ . '/VisitorOps.php';
    class VisitorServiceTestAnonMockBuilder extends TestCase
    {
      // code is identical to VisitorServiceTestAnon
      public function setup()
      {
        $data = array();
        for ($x = 1; $x <= 3; $x++) {
          $data[$x]['id'] = $x;
          $data[$x]['email'] = $x . 'test@unlikelysource.com';
          $data[$x]['visit_date'] = 
            '2000-0' . $x . '-0' . $x . ' 00:00:00';
          $data[$x]['comments'] = 'TEST ' . $x;
          $data[$x]['name'] = 'TEST ' . $x;
      }
      $this->testData = $data;
        $this->visitorService = 
          new VisitorService($this->dbConfig);
        $opsMock = $this->getMockBuilder(VisitorOps::class)
                        ->setMethods(['findAll'])
                        ->disableOriginalConstructor()
                        ->getMock();
                        $opsMock->expects($this->once())
                        ->method('findAll')
                        ->with()
                        ->will($this->returnValue($this->testData));
                        $this->visitorService->setVisitorOps($opsMock);
      }
      // remaining code is the same
    }

    Note

    We have shown how to create simple one-off tests. In most cases, however, you will have many classes that need to be tested, preferably all at once. This is possible by developing a test suite, discussed in more detail in the next recipe.

How it works...

First, you need to install PHPUnit, as discussed in steps 1 to 5. Be sure to include vendor/bin in your PATH so that you can run PHPUnit from the command line.

Running simple tests

Next, define a chap_13_unit_test_simple.php program file with a series of simple functions, such as add(), sub() and so on, as discussed in step 1. You can then define a simple test class contained in SimpleTest.php as mentioned in steps 2 and 3.

Assuming phpunit is in your PATH, from a terminal window, change to the directory containing the code developed for this recipe, and run the following command:

phpunit SimpleTest SimpleTest.php

You should see the following output:

Running simple tests

Make a change in SimpleTest.php so that the test will fail (step 4):

public function testDiv()
{
  $this->assertEquals(2, div(4, 2));
  $this->assertEquals(99, div(4, 0));
}

Here is the revised output:

Running simple tests

Next, add the table() function to chap_13_unit_test_simple.php (step 5), and testTable() to SimpleTest.php (step 6). Re-run the unit test and observe the results.

To test a class, copy the functions developed in chap_13_unit_test_simple.php to a Demo class (step 7). After making the modifications to SimpleTest.php suggested in step 8, re-run the simple test and observe the results.

Testing database model classes

First, create an example class to be tested, VisitorOps, shown in step 2 in this subsection. You can now define a class we will call SimpleDatabaseTest to test VisitorOps. First of all, use require_once to load the class to test. (We will discuss how to incorporate autoloading in the next recipe!) Then define key properties, including test database configuration and test data. You could use php7cookbook_test as the test database:

<?php
use PHPUnit\Framework\TestCase;
require_once __DIR__ . '/VisitorOps.php';
class SimpleDatabaseTest extends TestCase
{
  protected $visitorOps;
  protected $dbConfig = [
    'driver'   => 'mysql',
    'host'     => 'localhost',
    'dbname'   => 'php7cookbook_test',
    'user'     => 'cook',
    'password' => 'book',
    'errmode'  => PDO::ERRMODE_EXCEPTION,
  ];
  protected $testData = [
    'id' => 1,
    'email' => 'test@unlikelysource.com',
    'visit_date' => '2000-01-01 00:00:00',
    'comments' => 'TEST',
    'name' => 'TEST'
  ];
}

Next, define setup(), which inserts the test data, and confirms that the last SQL statement was INSERT. You should also check to see whether the return value was positive:

public function setup()
{
  $this->visitorOps = new VisitorOps($this->dbConfig);
  $this->visitorOps->addVisitor($this->testData);
  $this->assertRegExp('/INSERT/', $this->visitorOps->getSql());
}

After that, define teardown(), which removes the test data and confirms that the query for id = 1 comes back as FALSE:

public function teardown()
{
  $result = $this->visitorOps->removeById(1);
  $result = $this->visitorOps->findById(1);
  $this->assertEquals(FALSE, $result);
  unset($this->visitorOps);
}

The first test is for findAll(). First, confirm the data type of the result. You could take the topmost element using current(). We confirm there are five elements, that one of them is name, and that the value is the same as that in the test data:

public function testFindAll()
{
  $result = $this->visitorOps->findAll();
  $this->assertInstanceOf(Generator::class, $result);
  $top = $result->current();
  $this->assertCount(5, $top);
  $this->assertArrayHasKey('name', $top);
  $this->assertEquals($this->testData['name'], $top['name']);
}

The next test is for findById(). It is almost identical to testFindAll():

public function testFindById()
{
  $result = $this->visitorOps->findById(1);
  $this->assertCount(5, $result);
  $this->assertArrayHasKey('name', $result);
  $this->assertEquals($this->testData['name'], $result['name']);
}

You do not need to bother with a test for removeById() as this is already done in teardown(). Likewise, there is no need to test runSql() as this is done as part of the other tests.

Using mock classes

First, define a VisitorService service class as described in steps 2 and 3 in this subsection. Next, define a VisitorOpsMock mock class, which is discussed in steps 4 to 7.

You are now in a position to develop a test, VisitorServiceTest, for the service class. Note that you need provide your own database configuration as it is a best practice to use a test database instead of the production version:

<?php
use PHPUnit\Framework\TestCase;
require_once __DIR__ . '/VisitorService.php';
require_once __DIR__ . '/VisitorOpsMock.php';

class VisitorServiceTest extends TestCase
{
  protected $visitorService;
  protected $dbConfig = [
    'driver'   => 'mysql',
    'host'     => 'localhost',
    'dbname'   => 'php7cookbook_test',
    'user'     => 'cook',
    'password' => 'book',
    'errmode'  => PDO::ERRMODE_EXCEPTION,
  ];
}

In setup(), create an instance of the service, and insert VisitorOpsMock in place of the original class:

public function setup()
{
  $this->visitorService = new VisitorService($this->dbConfig);
  $this->visitorService->setVisitorOps(new VisitorOpsMock());
}
public function teardown()
{
  unset($this->visitorService);
}

In our test, which produces an HTML table from the list of visitors, you can then look for certain elements, knowing what to expect in advance as you have control over the test data:

public function testShowAllVisitors()
{
  $result = $this->visitorService->showAllVisitors();
  $this->assertRegExp('!^<table>.+</table>$!', $result);
  $testData = $this->visitorService->getVisitorOps()->getTestData();
  foreach ($testData as $key => $value) {
    $dataWeWant = '!<td>' . $key . '</td>!';
    $this->assertRegExp($dataWeWant, $result);
  }
}
}

You might then wish to experiment with the variations suggested in the last two subsections, Using Anonymous Classes as Mock Objects, and Using Mock Builder.

There's more...

Other assertions test operations on numbers, strings, arrays, objects, files, JSON, and XML, as summarized in the following table:

Category

Assertions

General

assertEquals(), assertFalse(), assertEmpty(), assertNull(), assertSame(), assertThat(), assertTrue()

Numeric

assertGreaterThan(), assertGreaterThanOrEqual(), assertLessThan(), assertLessThanOrEqual(), assertNan(), assertInfinite()

String

assertStringEndsWith(), assertStringEqualsFile(), assertStringStartsWith(), assertRegExp(), assertStringMatchesFormat(), assertStringMatchesFormatFile()

Array/iterator

assertArrayHasKey(), assertArraySubset(), assertContains(), assertContainsOnly(), assertContainsOnlyInstancesOf(), assertCount()

File

assertFileEquals(), assertFileExists()

Objects

assertClassHasAttribute(), assertClassHasStaticAttribute(), assertInstanceOf(), assertInternalType(), assertObjectHasAttribute()

JSON

assertJsonFileEqualsJsonFile(), assertJsonStringEqualsJsonFile(), assertJsonStringEqualsJsonString()

XML

assertEqualXMLStructure(), assertXmlFileEqualsXmlFile(), assertXmlStringEqualsXmlFile(), assertXmlStringEqualsXmlString()

See also...