Search code examples
sortingphpunitshuffle

Testing that an array is ordered randomly


I am testing my code with PHPunit. My code has several ordering-methods: by name, age, count and random. Below the implementation and test for sorting by count. These are pretty trivial.

class Cloud {
  //...
  public function sort($by_property) {
    usort($this->tags, array($this, "cb_sort_by_{$by_property}"));
    return $this;
  }

  private function cb_sort_by_name($a, $b) {
    $al = strtolower($a->get_name());
    $bl = strtolower($b->get_name());
    if ($al == $bl) {
      return 0;
    }
    return ($al > $bl) ? +1 : -1;
  }

  /**
   * Sort Callback. High to low
   */
  private function cb_sort_by_count($a, $b) {
    $ac = $a->get_count();
    $bc = $b->get_count();
    if ($ac == $bc) {
      return 0;
    }
    return ($ac < $bc) ? +1 : -1;
  }
}

Tested with:

  /**
   * Sort by count. Highest count first.
   */
  public function testSortByCount() {
    //Jane->count: 200, Blackbeard->count: 100
    //jane and blackbeard are mocked "Tags".
    $this->tags = array($this->jane, $this->blackbeard);

    $expected_order = array("jane", "blackbeard");
    $given_order = array();

    $this->object->sort("count");

    foreach($this->object->get_tags() as $tag) {
      $given_order[] = $tag->get_name();
    }

    $this->assertSame($given_order, $expected_order);
  }

But now, I want to add "random ordering"

  /**
   * Sort random.
   */
  public function testSortRandom() {
    //what to test? That "shuffle" got called? That the resulting array
    // has "any" ordering?
  }

The implementation could be anything from calling shuffle($this->tags) to a usort callback that returns 0,-1 or +1 randomly. Performance is an issue, but testability is more important.

How to test that the array got ordered randomly? AFAIK it is very hard to stub global methods like shuffle.


Solution

  • I decided to implement this with a global-wrapper:

    class GlobalWrapper {
      public function shuffle(&$array);
        shuffle($array);
      }
    }
    

    In the sort, I call shuffle through that wrapper:

    public function sort($by_property) {
      if ($by_property == "random") {
        $this->global_wrapper()->shuffle($this->tags);
      }
      //...
    }
    

    Then, in the tests I can mock that GlobalWrapper and provide stubs for global functions that are of interest. In this case, all I am interested in, is that the method gets called, not what it outputs[1].

     public function testSortRandomUsesShuffle() {
       $global = $this->getMock("GlobalWrapper", array("shuffle"));
       $drupal->expects($this->once())
         ->method("shuffle");
       $this->object->set_global_wrapper($drupal);
    
       $this->object->sort("random");
     }
    

    [1] In reality I have Unit Tests for this wrapper too, testing the parameters and the fact it does a call-by-ref. Also, this wrapper was already implemented (and called DrupalWrapper) to allow me to stub certain global functions provided by a third party (Drupal). This implementation allows me to pass in the wrapper using a set_drupal() and fetch it using drupal(). In above examples, I have called these set_global_wrapper() and global_wrapper().