Unit Testing

With the Laravel Framework and PHPUnit

Presented by Andrea Pollitt

Unit Tests

What are they and why are they important?

  • Tests for the smallest unit of the code
  • Assures the code works as intended
  • Provides a guideline for keeping it simple

The smallest unit (Part I)


class RestaurantBill
{
  public function calculateGratuity()
  {
    return $this->total * ($this->percent / 100);
  }
}
        

The smallest unit (Part II)


private function multiply($p1, $p2) {
  return $p1 * $p2;
}

private function divide($p1, $p2) {
  return $p1 / $p2;
}

public function calculateGratuity() {
  return $this->multiply($this->total, divide($this->tip_percent, 100));
}
        

The smallest unit (Part III)


private $serviceRating; // how was the service? 0-10

private function multiply($p1, $p2) {
  return $p1 * $p2;
}

private function divide($p1, $p2) {
  if ($p2 === 0) throw new \Exception('Division by zero');
  return $p1 / $p2;
}

private function factorInHappiness($amount) {
  try {
    $result = $this->divide(10, $this->serviceRating);
  } catch (\Exception $e) {
    return 1;
  }

  return $amount - $result;
}

public function calculateGratuity() {
  $amount = $this->multiply($this->total, divide($this->tipPercent, 100));
  $finalAmount = $this->factorInHappiness($amount);

  return $finalAmount;
}
        

Working as intended!

  • Writing new methods, or refactoring old ones
  • Imagine a large codebase, and changing one small portion of it
  • Does everything work after the change?
  • Did you remember to return the value you were expecting?

Enforce simplicity

  • Short, clear methods
  • Single responsibility
  • Test Driven Development (TDD)

Installing PHPUnit

From the app directory:
  1. mkdir tests && cd tests
  2. move the phpunit.xml file into the tests dir
  3. wget https://phar.phpunit.de/phpunit.phar
  4. chmod +x phpunit.phar
  5. ./phpunit.phar

You can also move it to your /bin directory and run it globally Installation Guide

phpunit.xml


bootstrap="../../bootstrap/autoload.php"


    
        .
        vendor
    

        

Setting Up composer.json


"require": {
    "laravel/framework": "4.1.*",
    "kriswallsmith/buzz": "dev-master"
},
"require-dev": {
    "mockery/mockery": "0.8@stable",
    "phpunit/phpunit": "3.7.*@stable"
},
        

Directory Structure for Tests


# ls tests/*
tests/ExampleTest.php  tests/phpunit.phar
tests/phpunit.xml  tests/RoutesTest.php  tests/TestCase.php

tests/controllers:
UserControllerTest.php

tests/models:
UserTest.php
      

Testing Routes


class RoutesTest extends TestCase
{
    public function testGetRoot()
    {
        View::shouldReceive('make');
        $this->call('GET', '/');
    }
...
}

        

Route::get('/', function()
{
    return View::make('hello');
});
        

class RoutesTest extends TestCase
{
...
    public function testGetUsers()
    {
        // Mock the database
        $user = Mockery::mock('Eloquent', 'User');
        $user->shouldReceive('all')->andReturn(array(1));
        $this->call('GET', 'users');
        $this->assertViewHas('users');
    }
...
}
        

Route::get('users', function() {
    $users = User::all();
    return View::make('users')->with('users', $users);
});
        

class RoutesTest extends TestCase
{
...
    public function testGetProfile()
    {
        // this tests the controller, not the route
        //$this->call('GET', 'profile/1');
    }
}
        

Route::get('profile/{profile_id}', 'UserController@getProfile');
        

Testing Models

TDD

class UserTest extends TestCase
{
    public function testGetFollowers()
    {
        $factory = Mockery::mock('FollowerFactory');
        $browser = Mockery::mock('\Buzz\Browser');
        $user = new User();

        $browser->shouldReceive('get');
        $factory->shouldReceive('create');
        $user->getFollowers();
    }
}
        

public function getFollowers()
{
    $url = self::GITHUB_API.$this->github_username.self::GITHUB_FOLLOWERS;
    $followers = $this->browser->get($url);
    return $this->followerFactory->create($followers);
}
        

PHP Fatal error:  Call to a member function get() on a
non-object in /vagrant/blog/app/models/User.php on line 37
        

When trying to mock the User model:


1) RoutesTest::testGetUsers
ReflectionException: Class FollowerFactory does not exist
        

public function configure(
    \Buzz\Browser $browser,
    FollowerFactory $factory
) {
    $this->browser = $browser;
    $this->followerFactory = $factory;
}
        

Testing Controllers


class UserController extends BaseController
{
    public function getProfile($profile_id)
    {
        $user = User::where('id', '=', $profile_id)->first();
        return View::make('profile')->with('user', $user);
    }
}
        

class UserControllerTest extends TestCase
{
    public function setUp()
    {
        $this->user = Mockery::mock('Eloquent', 'User');
        $this->builder = Mockery::mock('Eloquent', 'Builder');
        parent::setUp();
    }

    public function testGetProfile()
    {
        $this->user->shouldReceive('where')->andReturn($this->builder);
        $this->builder->shouldReceive('first');
    }
}
        

Links

Questions!