PHPUnicorn – Visualizing PHPUnit Tests

Hardware components:
475267 240424 01 front zoom kankcmvqdh
Raspberry Pi Zero Wireless
× 1
Unicorn pHAT
× 1
2A Micro-USB Power Adapter
× 1
Software apps and online services:
PHPUnit
Hand tools and fabrication machines:
09507 01
Soldering iron (generic)

PHPUnicorn - Visualizing PHPUnit Tests

STORY

Goal

I wanted to create a desktop gadget to visualize the progress of unit tests run via PHPUnit.

I've named this project PHPUnicorn (by combining “PHPUnit” with “Unicorn pHAT”).

Hardware & Assembly

The hardware requirements for this project:

  • Raspberry Pi Zero Wireless (W)
  • MicroSD card flashed with Raspbian Lite
  • 2 amp micro-USB power adapter
  • Unicorn pHAT by Pimoroni
  • A 2×20 header
  • A soldering iron

Put the header between the Pi and Unicorn pHAT and solder it into place.

Load Raspbian onto the microSD card, configure networking, enable SSH, and install Python 3. The unicorn-hat Python library will also need to be installed.

Extending PHPUnit

PHPUnit allows you to easily add listeners via the phpunit.xml file:

<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php">
    <!-- Existing configuration here --->
    <listeners>
        <listener class="ColinODell\PHPUnicorn\PHPUnicornListener">
            <arguments>
                <string>192.168.80.93</string>
                <string>5005</string>
            </arguments>
        </listener>
    </listeners>
</phpunit>

Each listener is notified when tests begin, when they finish, and what the results are. Here's the code I wrote to do just that:

<?php
namespace ColinODell\PHPUnicorn; 
use Exception; 
use PHPUnit_Framework_AssertionFailedError; 
use PHPUnit_Framework_Test; 
use PHPUnit_Framework_TestSuite; 
use PHPUnit_Framework_Warning; 
class PHPUnicornListener extends \PHPUnit_Framework_BaseTestListener 
{ 
   const NO_RESULT = 'N'; 
   const ERROR = 'E'; 
   const FAILURE = 'F'; 
   const INCOMPLETE = 'I'; 
   const RISKY = 'R'; 
   const SKIPPED = 'S'; 
   const PASSED = 'P'; 
   const WARNING = 'W'; 
   const TOTAL = 'T'; 
   const COMPLETED = 'C';
   private $currentTestPassed = false; 
   private $counts = []; 
   /** 
    * @var string 
    */ 
   private $host; 
   /** 
    * @var int 
    */ 
   private $port; 
   /** 
    * @var resource 
    */ 
   private $socket; 
   /** 
    * @param string $host 
    * @param int    $port 
    */ 
   public function __construct($host, $port) 
   { 
       $this->host = $host; 
       $this->port = $port; 
       $this->socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP); 
       $this->resetCounts(); 
       $this->broadcast(); 
   } 
   /** 
    * Ensure socket is closed 
    */ 
   public function __destruct() 
   { 
       socket_close($this->socket); 
       $this->socket = null; 
   } 
   /** 
    * {@inheritdoc} 
    */ 
   public function addError(PHPUnit_Framework_Test $test, Exception $e, $time)
   { 
       $this->counts[self::ERROR]++; 
       $this->currentTestPassed = false; 
   } 
   /** 
    * {@inheritdoc} 
    */ 
   public function addFailure(PHPUnit_Framework_Test $test, PHPUnit_Framework_AssertionFailedError $e, $time) { 
       $this->counts[self::FAILURE]++; 
       $this->currentTestPassed = false; 
   } 
   /** 
    * {@inheritdoc} 
    */ 
   public function addWarning(PHPUnit_Framework_Test $test, PHPUnit_Framework_Warning $e, $time) 
   { 
       $this->counts[self::WARNING]++; 
       $this->currentTestPassed = false; 
   } 
   /** 
    * {@inheritdoc} 
    */ 
   public function addIncompleteTest(PHPUnit_Framework_Test $test, Exception $e, $time) { 
       $this->counts[self::INCOMPLETE]++; 
       $this->currentTestPassed = false; 
   } 
   /** 
    * {@inheritdoc} 
    */ 
   public function addRiskyTest(PHPUnit_Framework_Test $test, Exception $e, $time) { 
       $this->counts[self::RISKY]++; 
       $this->currentTestPassed = false; 
   } 
   /** 
    * {@inheritdoc} 
    */ 
   public function addSkippedTest(PHPUnit_Framework_Test $test, Exception $e, $time) 
   { 
       $this->counts[self::SKIPPED]++; 
       $this->currentTestPassed = false; 
   } 
   /** 
    * A test suite has started. 
    * 
    * {@inheritdoc} 
    */ 
   public function startTestSuite(PHPUnit_Framework_TestSuite $suite) 
   { 
       // Test suites can contain child test suites.  This function is always 
       // called with the top-most parent, so use its count method to determine 
       // how many tests there are (it'll count all sub-children recursively). 
       if ($this->counts[self::TOTAL] == 0) { 
           $this->counts[self::TOTAL] = $suite->count(); 
       } 
   } 
   /** 
    * A test suite ended. 
    * 
    * {@inheritdoc} 
    */ 
   public function endTestSuite(PHPUnit_Framework_TestSuite $suite) 
   { 
       // Send the results over to the Pi 
       $this->broadcast(); 
   } 
   /** 
    * A single test started. 
    * 
    * {@inheritdoc} 
    */ 
   public function startTest(PHPUnit_Framework_Test $test) 
   { 
       // There's no method like addPassed(), so we'll assume the test passes 
       // unless one of the other add___() methods are called. 
       $this->currentTestPassed = true; 
   } 
   /** 
    * A test ended. 
    * 
    * {@inheritdoc} 
    */ 
   public function endTest(PHPUnit_Framework_Test $test, $time) 
   { 
       if ($this->currentTestPassed) { 
           $this->counts[self::PASSED]++; 
       } 
       $this->counts[self::COMPLETED]++; 
       $this->broadcast(); 
   } 
   private function resetCounts() 
   { 
       $this->counts = [ 
           self::NO_RESULT => 0, 
           self::ERROR => 0, 
           self::FAILURE => 0, 
           self::WARNING => 0, 
           self::INCOMPLETE => 0, 
           self::RISKY => 0, 
           self::SKIPPED => 0, 
           self::PASSED => 0, 
           self::TOTAL => 0, 
           self::COMPLETED => 0, 
       ]; 
   } 
   private function broadcast() 
   { 
       if ($this->counts[self::TOTAL] == 0) { 
           // Ask the Pi to clear the screen when we first start 
           $message = 'reset'; 
       } else { 
           $message = json_encode($this->counts); 
       } 
       socket_sendto($this->socket, $message, strlen($message), 0, $this->host, $this->port); 
   } 
}  

The code is fairly straight-forward – as each test result comes in, we increment the corresponding result in $this->counts and broadcast the latest counts to the Raspberry Pi via a UDP packet. These are encoded in a JSON string to keep the size low:

Read More: PHPUnicorn – Visualizing PHPUnit Tests

 

 

About The Author

Ibrar Ayyub

I am an experienced technical writer with a Master's degree in computer science from BZU Multan University. I have written for various industries, mainly home automation, and engineering. I have a clear and simple writing style and am skilled in using infographics and diagrams. I am a great researcher and is able to present information in a well-organized and logical manner.

Scroll to Top