Testing overriden trait method execution
Asked Answered
V

1

7

I have situation like this. I have some 3rd party trait (I don't want to test) and I have my trait that uses this trait and in some case runs 3rd party trait method (in below example I always run it).

When I have code like this:

use Mockery;
use PHPUnit\Framework\TestCase;

class SampleTest extends TestCase
{
    /** @test */
    public function it_runs_parent_method_alternative()
    {
        $class  = Mockery::mock(B::class)->makePartial();

        $class->shouldReceive('fooX')->once();

        $this->assertSame('bar', $class->foo());
    }

    protected function tearDown()
    {
        Mockery::close();
    }
}

trait X {
    function foo() {
        $this->something->complex3rdpartyStuff();
    }
}

trait Y2 {

    function foo() {
        $this->fooX();
        return 'bar';
    }
}

class B {
    use Y2, X {
        Y2::foo insteadof X;
        X::foo as fooX;
    }
}

it will work fine however I don't want code to be organized like this. In above code in class I use both traits but in code I want to test in fact trait uses other trait as mentioned at the beginning.

However when I have code like this:

<?php

use Mockery;
use PHPUnit\Framework\TestCase;

class SampleTest extends TestCase
{
    /** @test */
    public function it_runs_parent_method()
    {
        $class  = Mockery::mock(A::class)->makePartial();

        $class->shouldReceive('fooX')->once();

        $this->assertSame('bar', $class->foo());
    }

    protected function tearDown()
    {
        Mockery::close();
    }
}

trait X {
    function foo() {
        $this->something->complex3rdpartyStuff();
    }
}

trait Y {
    use X {
        foo as fooX;
    }

    function foo() {
        $this->fooX();
        return 'bar';
    }
}

class A {
    use Y;
}

I'm getting:

undefined property $something

so it seems Mockery is not mocking in this case X::foo method any more. Is there are way to make possible to write such tests with code organized like this?

Vietcong answered 4/8, 2018 at 8:45 Comment(0)
W
2

So far it is not possible to mock deeper aliased methods. You can proxy aliased method call using local method and allowing mocking protected methods.

Check code below

use Mockery;
use PHPUnit\Framework\TestCase;

class SampleTest extends TestCase
{
    /** @test */
    public function it_runs_parent_method()
    {
        $mock  = Mockery::mock(A::class)->shouldAllowMockingProtectedMethods()->makePartial();

        $mock->shouldReceive('proxyTraitCall')->once();

        $this->assertSame('bar', $mock->foo());
    }

    protected function tearDown()
    {
        Mockery::close();
    }
}

trait X {
    function foo() {
        $this->something->complex3rdpartyStuff();
    }
}

trait Y {
    use X {
        foo as fooX;
    }

    function foo() {
        $this->proxyTraitCall();
        return 'bar';
    }

    function proxyTraitCall() {
        return $this->fooX();
    }
}

If you autoload trait you can try to overload it using Mockery.

/** @test */
public function it_runs_parent_method()
{
    $trait = Mockery::mock("overload:" . X::class);
    $trait->shouldReceive('foo')->once();

    $class  = Mockery::mock(A::class)->makePartial();

    $this->assertSame('bar', $class->foo());
}

Don't test implementation details. Test it like you use it.

Class user have to know only public interface to use it, why test should be any different? Fact that one internal method call different one is implementation detail and testing this breaks encapsulation. If someday you will switch from trait to class method without changing class behaviour you will have to modify tests even though class from the outside looks the same.

From Pragmatic Unit Testing by Dave Thomas and Andy Hunt

Most of the time, you should be able to test a class by exercising its public methods. If there is significant functionality that is hidden behind private or protected access, that might be a warning sign that there's another class in there struggling to get out.

Whitford answered 13/8, 2018 at 7:50 Comment(7)
It's not the case, I need to override 3rd party method with trait and I have no other way to do it. And I would like to have tests for it as it's delivered as Composer package.Justus
@MarcinNabiałek I'm not sure if I get it right. You have trait that provides some functionality and which you want to mock? Can you wrap this trait in class and provide it as dependency? You could provide alternative implementation thenWhitford
Unfortunately I can't. I need to use trait that overrides other trait method and in some cases launches this method. That's why I would like to have possibility to mock original trait method because this is 3rd party and I only want to test if it's launched.Justus
@MarcinNabiałek I think I don't understand your problem. Is it open source, can you provide link to original use case?Whitford
github.com/mnabialek/laravel-quick-migrations/blob/master/src/… this is the file from package I developed. Original trait: github.com/laravel/framework/blob/5.6/src/Illuminate/Foundation/… Use case - replace use DatabaseMigrations; with use QuickDatabaseMigrations; in test class without any other changes to run those quickerJustus
@MarcinNabiałek I get it now, you can't mock aliased method, proxy call to make it mockableWhitford
I haven't thought about it but yes - it seems solution is to extract running this method to separate method and test if this method was called. It's not 100% coverage but it's better than no test :)Justus

© 2022 - 2024 — McMap. All rights reserved.