views:

627

answers:

3

I'm mostly convinced of the benefits of unit testing, and I would like to start applying the concept to a large existing codebase written in PHP. Less than 10% of this code is object-oriented.

I've looked at several unit testing frameworks (PHPUnit, SimpleTest, and phpt). However, I haven't found examples for any of these that test procedural code. What's the best framework for my situation and are there any examples of unit testing PHP using non-OOP code?

+1  A: 

You could try to include your non-oop code into a test class using

require_once 'your_non_oop_file.php' # Contains fct_to_test()

And with phpUnit you define your test function :

testfct_to_test() {
   assertEquals( result_expected, fct_to_test(), 'Fail with fct_to_test' );
}
Luc M
This looks like an interesting solution, however what if most of your logic doesn't involve functions but instead simple loops and conditionals?
Wally Lawless
Then you've got yourself a big ol' helping of spaghetti code.
Frank Farmer
I assumed that Travis has not php code with html.
Luc M
@luc M -- You assume correctly. It's well structured code: data, logic, and presentation are separated. It's just not OOP.
Travis Beale
+3  A: 

What unit tests do well, and what you should use them for, is when you have a piece of code that you give some number of inputs, and you expect to get some number of outputs back out. The idea being, when you add functionality later, you can run your tests and make sure it's still performing the old functionality the same way.

So, if you have a procedural code-base, you can accomplish this calling your functions in the test methods

require 'my-libraries.php';
class SomeTest extends SomeBaseTestFromSomeFramework {
 public function testSetup() {
  $this->assertTrue(true);
 }

 public function testMyFunction() {
  $output = my_function('foo',3);

  $this->assertEquals('expected output',$output);
 }
}

This trick with PHP code bases is, often your library code will interfere with the running of your test framework, as your codebase and the test frameworks will have a lot of code related to setting up an application environment in a web browser (session, shared global variables, etc.). Expect to spend sometime getting to a point where you can include your library code and run a dirt simple test (the testSetup function above).

If your code doesn't have functions, and is just a series of PHP files that output HTML pages, you're kind of out of luck. Your code can't be separated into distinct units, which means unit testing isn't going to be of much use to you. You'd be better off spending your time at the "acceptance testing" level with products like Selenium and Watir. These will let you automated a browser, and then check the pages for content as specific locations/in forms.

Alan Storm
I can definitely separate my code into distinct units. Your example looks promising, I take it this isn't using any particular framework?
Travis Beale
Yeah, that's just pseudo test-code, but actual SImpleTest tests and PHPUnit tests look remarkably similar.
Alan Storm
+5  A: 

You can unit-test procedural PHP, no problem. And you're definitely not out of luck if your code is mixed in with HTML.

At the application or acceptance test level, your procedural PHP probably depends on the value of the superglobals ($_POST, $_GET, $_COOKIE, etc.) to determine behavior, and ends by including a template file and spitting out the output.

To do application-level testing, you can just set the superglobal values; start an output buffer (to keep a bunch of html from flooding your screen); call the page; assert against stuff inside the buffer; and trash the buffer at the end. So, you could do something like this:

public function setUp()
{
    if (isset($_POST['foo'])) {
        unset($_POST['foo']);
    }
}

public function testSomeKindOfAcceptanceTest()
{
    $_POST['foo'] = 'bar';
    ob_start();
    include('fileToTest.php');
    $output = ob_get_flush();
    $this->assertContains($someExpectedString, $output);
}

Even for enormous "frameworks" with lots of includes, this kind of testing will tell you if you have application-level features working or not. This is going to be really important as you start improving your code, because even if you're convinced that the database connector still works and looks better than before, you'll want to click a button and see that, yes, you can still login and logout through the database.

At lower levels, there are minor variations depending on variable scope and whether functions work by side-effects (returning true or false), or return the result directly.

Are variables passed around explicitly, as parameters or arrays of parameters between functions? Or are variables set in many different places, and passed implicitly as globals? If it's the (good) explicit case, you can unit test a function by (1) including the file holding the function, then (2) feeding the function test values directly, and (3) capturing the output and asserting against it. If you're using globals, you just have to be extra careful (as above, in the $_POST example) to carefully null out all the globals between tests. It's also especially helpful to keep tests very small (5-10 lines, 1-2 asserts) when dealing with a function that pushes and pulls lots of globals.

Another basic issue is whether the functions work by returning the output, or by altering the params passed in, returning true/false instead. In the first case, testing is easier, but again, it's possible in both cases:

// assuming you required the file of interest at the top of the test file
public function testShouldConcatenateTwoStringsAndReturnResult()
{
  $stringOne = 'foo';
  $stringTwo = 'bar';
  $expectedOutput = 'foobar';
  $output = myCustomCatFunction($stringOne, $stringTwo);
  $this->assertEquals($expectedOutput, $output);
}

In the bad case, where your code works by side-effects and returns true or false, you still can test pretty easily:

/* suppose your cat function stupidly 
 * overwrites the first parameter
 * with the result of concatenation, 
 * as an admittedly contrived example 
 */
public function testShouldConcatenateTwoStringsAndReturnTrue()
    {
      $stringOne = 'foo';
      $stringTwo = 'bar';
      $expectedOutput = 'foobar';
      $output = myCustomCatFunction($stringOne, $stringTwo);
      $this->assertTrue($output);
      $this->Equals($expectedOutput, $stringOne);
    }

Hope this helps.

jared