2

I'm very new to unit testing in general and PHPUnit in particular, so please forgive me if this is a simple question. I have done some googling, but I don't know enough to know what to search for.

So I have a function:

function convert_timezone($dt, $tzFrom, $tzTo, $format) {
    $newDate = '';

    $tzFromFull = timezone_name_from_abbr($tzFrom);
    $tzToFull = timezone_name_from_abbr($tzTo);

    if( $tzFromFull != $tzToFull ) {
        $dtFrom = new DateTimeZone($tzFromFull);
        $dtTo = new DateTimeZone($tzToFull);

        try {
            // find the offsets from GMT for the 2 timezones
            $current = new DateTime(date('c',$dt));
            $offset1 = $dtFrom->getOffset($current);
            $offset2 = $dtTo->getOffset($current);
            $offset = $offset2 - $offset1;

            // apply the offset difference to the current time
            $newDate = date($format, $current->format('U') + $offset);
        } catch (Exception $e) {
            $newDate = date($format.' (T)', $dt);
        }
    } else {
        $newDate = date($format, $dt);
    }

    return $newDate;
}

Here's the test function:

function test_convert_timezone() {
   $dt = mktime(0,0,0,1,1,2000);
   $tzFrom = 'CDT';
   $tzTo = 'EDT';
   $format = 'd/m/Y g:i a';
   $result = convert_timezone($dt, $tzFrom, $tzTo, $format);
   $this->assertEquals($result, '01/01/2000 1:00 am');
}

When I run the tests directly in Netbeans, the test passes. But when I use the command-line option to generate a code coverage report, it tells me that this function is not completely covered because the test isn't hitting the exception.

I know if I could alter the original function, I could force it to throw an error. But I can't do that. I need a way, in the test function, to pass something in that will make the function being tested hit its exception. Where do I start?

7
  • Erm... what if you just create another test with data that will cause DateTime to throw an exception, then check for the expected result (of statements in catch block) with PHPUnit?
    – raina77ow
    Commented Apr 30, 2013 at 17:11
  • We're implementing unit testing on a site that's already built, so no - we can't write code based on the tests. As far as writing another test, as I said - I'm very new at this. Is it kosher to have more than one test function for a single function being tested?
    – EmmyS
    Commented Apr 30, 2013 at 17:13
  • Of course. If your function has branches, how can you check for all of them in a single test? It's not ok to have multiple asserts in a single test, as it defeats the principle of test isolation (check the discussion started by this question for details on that).
    – raina77ow
    Commented Apr 30, 2013 at 17:16
  • OK, I'm confused. If I write a test that specifically designed to throw an error, won't that make the test fail? How do I write a test where the "success" result is predicated on the actual function failing?
    – EmmyS
    Commented Apr 30, 2013 at 17:35
  • Well, you actually can check for exceptions (with @expectedException annotation, for example) in PHPUnit tests. But the point is, you don't have to do this, as your function doesn't throw exceptions - it handles them with $newDate = date($format.' (T)', $dt); line. Now your task is to make the test that will hit this line - that is, will cause new DateTime to throw an exception.
    – raina77ow
    Commented Apr 30, 2013 at 17:39

1 Answer 1

1

You are pretty much not able to test that branch as the code stands.

Your try-catch block doesn't actually catch any exceptions. (date throws an error which isn't caught. getOffset doesn't either) Ironically new DateTimeZone can if you pass in a timezone constant that isn't appropriate DateTimeZone::__construct(). So if you can refactor it and pass in timezones that are not listed as valid you can get the exception (which you should as it seems that it is your intention to catch the exceptions here).

function convert_timezone($dt, $tzFrom, $tzTo, $format) {
    $newDate = '';

    $tzFromFull = timezone_name_from_abbr($tzFrom);
    $tzToFull = timezone_name_from_abbr($tzTo);

    if( $tzFromFull != $tzToFull ) {
        try {
            $dtFrom = new DateTimeZone($tzFromFull);
            $dtTo = new DateTimeZone($tzToFull);


            // find the offsets from GMT for the 2 timezones
            $current = new DateTime(date('c',$dt));
            $offset1 = $dtFrom->getOffset($current);
            $offset2 = $dtTo->getOffset($current);
            $offset = $offset2 - $offset1;

            // apply the offset difference to the current time
            $newDate = date($format, $current->format('U') + $offset);
        } catch (Exception $e) {
            $newDate = date($format.' (T)', $dt);
        }
    } else {
        $newDate = date($format, $dt);
    }

    return $newDate;
}

Then the test would be

function test_convert_timezone() {
   $dt = mktime(0,0,0,1,1,2000);
   $tzFrom = 'foo';
   $tzTo = 'EDT';
   $format = 'd/m/Y g:i a';
   $result = convert_timezone($dt, $tzFrom, $tzTo, $format);
   $this->assertEquals($result, '01/01/2000 1:00 am');
}

In this test, timezone_name_from_abbr will return false which will cause the DateTimeZone constructor to throw an exception reaching your branch.

IMO, you want to avoid constructing objects that you are going to use in your functions. For this code, I would pass in the two DateTimeZone objects as well as a DateTime object and perform the operations on them. Moving the creation of these other objects into a different function/class. This way you could mock the DateTimeZone and DateTime objects themselves and have more control over what is happening. This code it isn't too bad but it can cause trouble with trying to test other things.

Also, be careful about catching a generic exception in your code. If you are using a mock object in a try-catch block, your code can catch a FailedTestException (thrown when a mock is not called with the right parameters or similar) which will be caught by your code. Making it look like your test is passing when it actually isn't. Again not a problem here due to the code and the lack of mocks but I wanted to make you aware of it.

1
  • Thanks for the detailed answer. I'll keep it in mind for the future. Unfortunately for this round of testing, we are unable to change our source code at all. We need to write the tests to cover the code as it stands.
    – EmmyS
    Commented May 6, 2013 at 15:24

Not the answer you're looking for? Browse other questions tagged or ask your own question.