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 holding a Master's degree in computer science from BZU Multan, Pakistan University. With a background spanning various industries, particularly in home automation and engineering, I have honed my skills in crafting clear and concise content. Proficient in leveraging infographics and diagrams, I strive to simplify complex concepts for readers. My strength lies in thorough research and presenting information in a structured and logical format.

Follow Us:
LinkedinTwitter

Leave a Comment

Your email address will not be published. Required fields are marked *