Hướng dẫn expectexceptionmessage phpunit

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.

PHP 7.4 and PHPUnit 9

Using the PHPUnit homepage example (https://phpunit.de/getting-started/phpunit-9.html):

private function ensureIsValidEmail(string $email): void
{
    if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
        throw new InvalidArgumentException(
            sprintf(
                '"%s" is not a valid email address',
                $email
            )
        );
    }
}

The homepage also shows us how to test the exception is thrown using the expectException() method:

public function testCannotBeCreatedFromInvalidEmailAddress(): void
{
    $this->expectException(InvalidArgumentException::class);

    Email::fromString('invalid');
}

That's great. But what if I want to test the exception is not thrown given valid input ?

Looking at the docs (https://phpunit.readthedocs.io/en/9.3/writing-tests-for-phpunit.html#testing-exceptions) there seems to be no mention of an inverse method to expectException() ?

How should I approach this ?

EDIT TO ADD:

Just to make it perfectly clear, I'm looking to test an Email::fromString(''); scenario, i.e. that the exception is not thrown.

asked Sep 1, 2020 at 20:35

Little CodeLittle Code

1,2352 gold badges13 silver badges34 bronze badges

1

If an uncaught or unexpected exception is thrown, the test will fail. You don't have to do anything special, just run the code being tested. If there are no other assertions in the test method, you'll also have to do $this->expectNotToPerformAssertions(); or you'll get a warning that the test doesn't perform any assertions.

public function testCannotBeCreatedFromInvalidEmailAddress(): void
{
    $this->expectNotToPerformAssertions();
    Email::fromString('invalid'); // If this throws an exception, the test will fail.
}

answered Sep 1, 2020 at 20:39

Alex HowanskyAlex Howansky

47.6k8 gold badges74 silver badges95 bronze badges

2

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

I’m refactoring a legacy PHP codebase, and that includes writing new tests. While doing that, I found a class method that accepts only a predetermined set of values, and refuses any other value by throwing an exception. Below is a simplified representation of this class:



class MyClass
{
    protected $value;

    public function setValue($value)
    {
        if (in_array($value, ['foo', 'bar'])) {
            $this->value = $value;
            return $this;
        }

        throw new RuntimeException("{$value} is not a valid value.");
    }
}

Promptly, I wrote a test to check if the method was filtering out invalid input values:



use PHPUnit\Framework\TestCase;

class MyTestCase extends TestCase
{
    protected $instance;

    public function setUp()
    {
        $this->instance = new MyClass();
    }

    public function testSetValueRefusesBadInput()
    {
        $this->expectException(RuntimeException::class);
        $this->instance->setValue('baz');
    }
}

Easy peasy.

But what if the method is not handling valid input values correctly? Since the method accepts only a small set of values, we could test all of them. However, PHPUnit 6 doesn’t have a doNotExpectException assertion, nor does it allow a test without an assertion.

We can easily overcome these limitations by adding an assertion to the end of the test ($this->assertTrue(true)) or by increasing the assertion count ($this->addToAssertionCount(1)). If the MyClass::setValue method implementation is incorrect, an exception will be thrown, otherwise, the assertion will be accounted for and PHPUnit will not “complain” about the test.



use PHPUnit\Framework\TestCase;

class MyTestCase extends TestCase
{
    protected $instance;

    public function setUp()
    {
        $this->instance = new MyClass();
    }

    public function testSomethingWorks()
    {
        $this->instance->setValue('bar');
        $this->addToAssertionCount(1);
    }

    public function testAnotherThingWorks()
    {
        $this->instance->setValue('foo');
        $this->assertTrue(true);
    }
}