简体   繁体   中英

How do I test for multiple exceptions with PHPUnit?

When testing for exceptions with PHPUnit, what is the best way to require that every statement or assertion must throw an exception in order for the test to pass?

I basically want to do something like this:

public function testExceptions()
{

    $this->setExpectedException('Exception');

    foo(-1); //throws exception
    foo(1); //does not throw exception

}

//Test will fail because foo(1) did not throw an exception

I've come up with the following, which does the job, but is quite ugly IMO.

public function testExceptions()
{

    try {
        foo(-1);
    } catch (Exception $e) {
        $hit = true;
    }

    if (!isset($hit))
        $this->fail('No exception thrown');

    unset($hit);

    try {
        foo(1);
    } catch (Exception $e) {
        $hit = true;
    }

    if (!isset($hit))
        $this->fail('No exception thrown');

    unset($hit);

}

I think this is a very common situation in unit testing. The approach I use in this cases is using phpunit dataProviders . All works as expected, and test code become more clear and concise.

class MyTest extends PHPUnit\Framework\TestCase
{
    public function badValues(): array
    {
       return [
           [-1],
           [1]
       ];
    }


    /**
     * @dataProvider badValues
     * @expectedException Exception
     */
    public function testFoo($badValue): void
    {
        foo($badValue);
    }
}

As exceptions are such big events in the program flow, testing multiple ones in a single test is problematic.

The easiest thing is is to simply split it into two tests - the first requires an exception to be able to pass, the second simply runs, and would fail it it did throw one. You could add some other tests in the second if you wanted (confirming a return value maybe), but I'd be inclined to make sure it still did just the one essential thing, according to the naming of it.

/**
 * @expectedException Exception
 */
public function testBadFooThrowsException()
{
    // optional, can also do it from the '@expectedException x'
    //$this->setExpectedException('Exception');
    foo(-1); //throws exception -- good.
}

public function testFooDoesNotThrowException()
{
    foo(1); //does not throw exception
}

Slightly cleaner code (but I'd still suggest splitting your tests:

try {
    foo(-1);
    $this->fail('No exception thrown');
} catch (Exception $e) {}

This doesn't make sense to me.

I guess you are trying to test multiple separate things with one test case which is bad practice.

When foo() throws the expected exception the test case is successful and bar() won't run.

Just create two separate test cases which is a lot less code then what you produced in the second listing.

Or explain why it would make sense to run bar() , after foo() failed with an exception, when it will throw an exception too.

Expanding on @dave1010's answer, here is how I solved this issue. This allows you to keep all these "assertions" neat and tidy within one test. You simply define an array of variables that should fail the test, and then loop through each one and see if an exception is raised. If any fail (no exception thrown), the test fails, otherwise the test passes.

<?php

public function testSetInvalidVariableType()
{
    $invalid_vars = array(
        '',                 // Strings
        array(),            // Arrays
        true,               // Booleans
        1,                  // Integers
        new \StdClass       // Objects
    );

    foreach ($invalid_vars as $var) {
        try {
            $object->method($var);
            $this->fail('No exception thrown for variable type "' . gettype($var) . '".');
        } catch (\Exception $expected) {
        }
    }
}

You could create a method like this in your test case class

public function assertThrowsException(\Closure $closure, string $exception)
{
    try {
        $closure();
        $this->fail("Closure doesn't throw any exception");
    } catch (PHPUnit\Framework\AssertionFailedError $e) {
        throw $e;
    } catch (\Exception $e) {
        $this->assertInstanceOf($exception, $e, "Wrong exception type");
    }
}

Then call it like

$this->assertThrowsException(function() {
   foo(1);
}, SomeException::class);

$this->assertThrowsException(function() {
   foo(-1);
}, AnotherException::class);

Some improvements might be needed here, there might be some edge cases, but I think it could take you a long way.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM