Test multiple PHP exceptions

There are cases when you have a base exception class which is extended by other exceptions. For a package called User, you could have a UserException which is inherited by UserNotFoundException, UserPasswordException and so on.

The base exception of your package allows you to catch any thrown exception from inside the package.

So there’s a base exception, a specific one and the service using them:

class BaseException extends Exception
{
}

class NoItemsException extends BaseException
{
}

class Service
{
    /**
     * @param array $items
     * @throws NoItemsException
     */
    public function save(array $items)
    {
        if (count($items) === 0) {
            throw new NoItemsException('Nothing to save');
        }
    }
}

And you want to catch all package exceptions:

$service = new Service();
try {
    $service->save([]);
} catch (BaseException $e) {
    die($e->getMessage());
}

And a test for the case:

class ServiceTest extends TestCase
{
    /**
     * @expectedException NoItemsException
     */
    public function testSaveWithNoItems()
    {
        $service = new Service();
        $service->save([]);
    }
}

How do you make sure NoItemsException extends BaseException?
With the above test, if you change NoItemsException to extend Exception instead of BaseException, the test will still pass, but the behaviour won’t be the expected one, because you won’t be catching the exception anymore:

Fatal error: Uncaught NoItemsException: Nothing to save... in /src/Service.php on line 20

NoItemsException: Nothing to save... in /src/Service.php on line 20

Your test must explicitly test both exceptions:

class ServiceTest extends TestCase
{
    public function testSaveWithNoItems()
    {
        $service = new Service();

        try {
            $service->save([]);
            $this->fail('No exception thrown');
        } catch (\Exception $e) {
            $this->assertInstanceOf(BaseException::class, $e);
            $this->assertInstanceOf(NoItemsException::class, $e);
        }
    }
}

PHP unit testing with real coverage

If you really need to cover all your code by tests, watch out for your short if statements.

Given the following class:

<?php

class Person
{
    /**
     * @var string
     */
    private $gender;

    /**
     * @param string $gender
     */
    public function setGender(string $gender)
    {
        $this->gender = $gender;
    }

    /**
     * @return string
     */
    public function getTitle() : string
    {
        return $this->gender === 'f' ? 'Mrs.' : 'Mr.';
    }
}

And a PHP Unit test:

<?php

use PHPUnit\Framework\TestCase;

class PersonTest extends TestCase
{
    /**
     * @dataProvider gendersAndTitle
     * @param $gender
     * @param $expectedTitle
     */
    public function testTitle($gender, $expectedTitle)
    {
        $person = new Person();
        $person->setGender($gender);

        $title = $person->getTitle();
        $this->assertEquals($expectedTitle, $title);
    }

    public function gendersAndTitle() : array
    {
        return [
            ['f', 'Mrs.'],
        ];
    }
}

If you run the test with coverage, you get a 100% coverage. But the data provider has only data for the “f/Mrs.” case, so the else branch of the short if is not actually tested, though the tested code reached the line while running the test.

Update the getTitle method from Person class using the normal if statement:

public function getTitle() : string
{
    if ($this->gender === 'f') {
        return 'Mrs.';
    }

    return 'Mr.';
}

Execute the test again and you get 80% coverage.

Here’s a Dockerfile to quickly test it yourself:

FROM php:7.2-cli-alpine3.8

RUN apk add --update --no-cache make alpine-sdk autoconf && \
    pecl install xdebug && \
    docker-php-ext-enable xdebug && \
    apk del alpine-sdk autoconf && \
    wget -O phpunit https://phar.phpunit.de/phpunit-6.phar && chmod +x phpunit

WORKDIR /src

Save the Person class to Person.php file and the test to PersonTest.php.

docker build -t phpunit-coverage .
docker run --rm -ti -v $PWD:/src phpunit-coverage sh

./phpunit --bootstrap Person.php --coverage-html coverage --whitelist Person.php .

See the coverage directory (index.html) created after running the test.

Clean up when you’re done:

docker rmi phpunit-coverage

Unit testing and interfaces

  • Good code needs tests
  • Tests require good design
  • Good design implies decoupling
  • Interfaces help decouple
  • Decoupling lets you write tests
  • Tests help having good code

Good code and unit testing come hand in hand, and sometimes the bridge between them are interfaces. When you have an interface, you can easily “hide” any implementation behind it, even a mock for a unit test.

An important subject of unit testing is managing external dependencies. The tests should directly cover the unit while using fake replacements (mocks) for the dependencies.

I was given the following code and asked to write tests for it:

package mail

import (
   "fmt"
   "net"
   "net/smtp"
   "strings"
)

func ValidateHost(email string) (err error) {
   mx, err := net.LookupMX(host(email))
   if err != nil {
      return err
   }

   client, err := smtp.Dial(fmt.Sprintf("%s:%d", mx[0].Host, 25))
   if err != nil {
      return err
   }

   defer func() {
      if er := client.Close(); er != nil {
         err = er
      }
   }()

   if err = client.Hello("checkmail.me"); err != nil {
      return err
   }
   if err = client.Mail("testing-email-host@gmail.com"); err != nil {
      return err
   }
   return client.Rcpt(email)
}

func host(email string) (host string) {
   i := strings.LastIndexByte(email, '@')
   return email[i+1:]
}

The first steps were to identify test cases and dependencies: Continue reading Unit testing and interfaces