phpspec / phpspec

SpecBDD Framework for PHP
http://www.phpspec.net
Other
1.88k stars 279 forks source link

[Question] 4 ways to write a test. What is the recommended way? #1242

Open Malian opened 5 years ago

Malian commented 5 years ago

Hi ! I am new to phpspec/prophecy ecosystem and I encounter troubles to write a "simple" test.

I designed a Timesheet object that owns several Occupations. These occupations are instancied inside a method of the Timesheet object; fillOccupation. I would like to design a Counter that count occupations that take place in the morning (This is for the example purpose but it is not so far from my real use case).

My objects:

<?php
namespace App;

class Timesheet
{
    private $date;
    private $occupations;

    public function __construct(\DateTimeImmutable $date)
    {
        $this->date = $date;
        $this->occupations = [];
    }

    public function getDate(): \DateTimeImmutable
    {
        return $this->date;
    }

    public function getOccupations(): array
    {
        return $this->occupations;
    }

    public function fillOccupation($dayOfMonth, $amOrPm): void
    {
        $this->occupations[] = new Occupation($this, $dayOfMonth, $amOrPm);
    }
}

class Occupation
{
    private $timesheet;
    private $date;
    private $amOrPm;

    public function __construct(Timesheet $timesheet, string $dayOfMonth, string $amOrPm)
    {
        // ManyToOne with doctrine
        $this->timesheet = $timesheet;
        $this->amOrPm = $amOrPm;
        $this->date = new \DateTimeImmutable($timesheet->getDate()->format('Y-m-') . $dayOfMonth);
    }

    public function isAm(): bool
    {
        return $this->amOrPm === 'am';
    }
}

final class Counter
{
    public function count(Timesheet $timesheet): int
    {
        $count = 0;

        foreach ($timesheet->getOccupations() as $occupation) {
           if ($occupation->isAm()) {
               $count += 1;
           }
        }

        return $count;
    }
}

I would like to test my Counter service. I wrote 4 tests to describe the same behavior. These test are green but I do not know what is the recommended way to write a test in this case.

namespace spec\App;

use App\Occupation;
use App\Timesheet;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;

class CounterSpec extends ObjectBehavior
{
    function it_should_count_v1(Timesheet $timesheet)
    {
        $timesheet->getDate()->willReturn(new \DateTimeImmutable('now'));
        $timesheet->getOccupations()->willReturn([
            new Occupation($timesheet->getWrappedObject(), '01', 'am')
        ]);

        $this->count($timesheet)->shouldReturn(1);
    }

    function it_should_count_v2(Timesheet $timesheet)
    {
        $timesheet->getOccupations()->will(function () {
            $this->getDate()->willReturn(new \DateTimeImmutable('now'));
            return [
                new Occupation($this->reveal(), '01', 'am')
            ];
        });

        $this->count($timesheet)->shouldReturn(1);
    }

    function it_should_count_v3(Timesheet $timesheet, Occupation $occupation)
    {
        $occupation->isAm()->willReturn(true);

        $timesheet->getOccupations()->willReturn([
            $occupation
        ]);

        $this->count($timesheet)->shouldReturn(1);
    }

    function it_should_count_v4()
    {
        $timesheet = new Timesheet(new \DateTimeImmutable('now'));
        $timesheet->fillOccupation('01', 'am');

        $this->count($timesheet)->shouldReturn(1);
    }
}

Could you please help me to understand the phpspec way to write a test ? When do I need to use a mock? When do I need to instantiate my object ?

Thank you!

ddziaduch commented 5 years ago

All tests are correct, but they do different types of testing.

The V4 test looks like Functional Test. It tests the whole functionality of all 3 classes 🙂

I think V3 is the best as Unit Test. It focuses only on the behavior of tested class 🙂