Unit and Functional tests, huh?! Whats the difference?

Published: Jan 17 2020 | Last Updated: Jan 18 2020

How and why you should separate your PHP test suites.

I recently attempted to answer a question on stack overflow in regards to PHPUnit testing. Although I feel I wasn't able to convey my answer in a understandable way. So what better way than to try again here...

Unit tests vs Functional Tests

A unit test is a test written that checks the behavior of a specific isolated block of code, without depending on the real input from external dependencies. So what are external dependencies? Anything outside of the method/function under test that said method/function relies upon. I.e. a different class method within the codebase, an API response from another server, etc... In unit testing, mocking dependencies allows us to test a code block without having to rely upon it's dependencies.

A functional test on the other hand is a test written to ensure that all of the code within a code base interacts with each other and its dependencies as expected. When writing functional tests, you don't usually mock your dependencies. More on this below.

Unit Tests

Look at the following two classes. In all, \Store::class has one dependency, \FruitBasket, and \FruitBasket has a dependency of it's own, \Connection::class, which provides a connection to a "server far away".


class FruitBasket
{
    public $serverFarAway;
    
    public $fruitArray = ['apple', 'orange']

    public function __construct(\Connection $serverFarAwayConnection)
    {
        $this-serverFarAway = $serverFarAwayConnection
    }

    public function getByType(string $type): string
    {
        $key = in_array($type, $this->fruitArray);
    
        if (!$key) {
            return $this-fruit[$key];
        }
        
        return $this->getFruitByTypeFromServerFarAway($type);
    }

    protected function getFruitByTypeFromServerFarAway(string $type): string
    {
        return $this-serverFarAway->getFruit($type);
    }
}


class Store
{
    public $basket;

    public function __construct(\FruitBasket $basket)
    {
        $this->basket = $basket;
    }

    public function getFruit(string $typeOfFruit): string
    {
        return $this-basket->getByType($typeOfFruit);
    }
}

To unit test \Store::class, we must mock \FruitBasket before testing the \Store::getFruit() method.


//UnitTest.php

use PHPUnit\Framework\TestCase;

class UnitTest extends TestCase
{
    public function testGetFruitReturnsOrangeWhenOrangeIsTheParam(): void
    {
        $mockBasket = $this-createMock(\FruitBasket::class);
        $mockBasket->expects($this-once())
            ->method('getByType')
            ->with('orange')
            ->willReturn('Orange')
        ;

        $store = new Store($mockBasket);
        $result = $store->getFruit('orange');
        
        self::assertSame('Orange', $result);
    }
}

The above test is actually performing 2 assertions. The first, self::assertSame is obvious. We are testing that $store->getFruit() returns the result from \FruitBasket::getByType(). The 2nd assertion is that \FruitBasket::getByType() is actually being called by \Store::getFruit() exactly 1 time. We are also ensuring that when we call \Store::getFruit('orange'), our 'orange' parameter is being passed to \FruitBasket::getByType(). We are accomplishing this without actually using a real \FruitBasket object in the test.

The next unit tests we would write would be for \FruitBasket::class;


//UnitTest2.php

use PHPUnit\Framework\TestCase;

class UnitTest2 extends TestCase
{
    public $mockConnection;
    
    protected function setUp(): void
    {
        $this-mockConnection = $this-createMock(\Connection::class);
    }

    public function testGetByTypeReturnsFruitInFruitAway(): void
    {
        $expectedResult = 'apple';
        
        $basket = new \FruitBasket($this-mockConnection);
        
        $result = $basket->getByType('apple');
                
        self::assertSame($expectedResult, $result);
    }
}

We are essentially doing the same as before, expect this time we are using the inherited TestCase::setUp() method. setUp()