Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to mock a non-abstract trait method with phpunit?

I have a trait (that is used by another trait) with a non-abstract method I need to mock, but when I try to do so I get the error message:

Trying to configure method getOfficeDays which cannot be configured because it does not exist, has not been specified, is final, or is static

This is the code I'm trying to test:

trait OfficeDaysTrait {
  public function getOfficeDays(): array {
    // This normally calls the database, that's why I want to mock it in tests
    return [
     [
       'day' => 'mon',
       'openingTime' => '08:00:00',
       'closingTime' => '17:00:00'
     ]
    ];
  }
}

trait EmployeeAttendenceTrait {
  use OfficeDaysTrait;

  public function isLate($today)
  {
     $late = false;
     $officeDays = $this->getOfficeDays();

     // compare logic with today.. whether the employee is late or not

     return $late;
  }
}

And this is my test method body:

$mock = $this->getMockForTrait(EmployeeAttendenceTrait::class); 

$mock->expects($this->once())
 ->method('getOfficeDays')
 ->will([])
;

$this->assertTrue($mock->isLate());

This syntax is based on the example shown in phpunit's documentation for testing traits

Why am I getting this error message and how can I mock getOfficeDays to be able to test my isLate method?

like image 231
Md. Kausar Alam Avatar asked Oct 22 '19 11:10

Md. Kausar Alam


1 Answers

Methods which have not been mocked cannot have expectations

I'll go into a bit more detail below but this:

$mock = $this->getMockForTrait(EmployeeAttendenceTrait::class); 

Is mocking no methods, as such $mock is the equivalent of:

class Testing extends PHPUnitsInternalMockStub
{
    use EmployeeAttendenceTrait;
}

$mock = new Testing;

Check the error message

The error message reads:

Trying to configure method getOfficeDays which cannot be configured because it does not exist, has not been specified, is final, or is static

Let's take a look at each possibility (almost in turn)

It does not exist

The code looks to clearly have a method named getOfficeDays - if in doubt using get_class_methods would clarify:

<?php

use PHPUnit\Framework\TestCase;

class SOTest extends TestCase
{

  public function testMethods()
  {
    $mock = $this->getMockForTrait(EmployeeAttendenceTrait::class);
    print_r(get_class_methods($mock));
  }
}

Which outputs:

R                                                                   1 / 1 (100%)Array
(
    [0] => __clone
    [1] => expects
    [2] => method
    [3] => __phpunit_setOriginalObject
    [4] => __phpunit_getInvocationMocker
    [5] => __phpunit_hasMatchers
    [6] => __phpunit_verify
    [7] => isLate
    [8] => getOfficeDays
)


Time: 569 ms, Memory: 12.00MB

There was 1 risky test:

1) SOTest::testMethods
This test did not perform any assertions

So it's not that.

is final

public function getOfficeDays(): array {

The method signature clearly does not declare the method as final - so it's not that.

or is static

public function getOfficeDays(): array {

The method signature clearly is also not static - so it's not that either.

has not been specified

By logical deduction this has to be the problem - and it's the only one which requires some understanding of using phpunit.

The docs for getMockForTrait read (emphasis added):

The getMockForTrait() method returns a mock object that uses a specified trait. All abstract methods of the given trait are mocked. This allows for testing the concrete methods of a trait.

Since none of the trait methods are abstract, no methods are expected to be mocked with the syntax in the question. Looking deeper, the method signature for getMockForTrait has more arguments:

    /**
     * Returns a mock object for the specified trait with all abstract methods
     * of the trait mocked. Concrete methods to mock can be specified with the
     * `$mockedMethods` parameter.
     *
     * @throws RuntimeException
     */
    public function getMockForTrait(
        string $traitName, 
        array $arguments = [], 
        string $mockClassName = '', 
        bool $callOriginalConstructor = true, 
        bool $callOriginalClone = true, 
        bool $callAutoload = true, 
        array $mockedMethods = [], 
        bool $cloneArguments = true): MockObject

There is a parameter for specifying explicitly mocked Methods, therefore writing the test like so:

class SOTest extends TestCase
{

  public function testMethods()
  {
    $mock = $this->getMockForTrait(
      EmployeeAttendenceTrait::class, 
      [],   # These
      '',   # are
      true, # the
      true, # defaults
      true, #
      ['getOfficeDays']
    );

    $mock->expects($this->once())
     ->method('getOfficeDays')
     ->willReturn([]);

    $today = new DateTime(); // or whatever is appropriate
    $this->assertTrue($mock->isLate($today));
  }
}

Will generate a valid mock object and allow the test to run.

like image 166
AD7six Avatar answered Nov 03 '22 08:11

AD7six