GithubHelp home page GithubHelp logo

mock-chain's Introduction

mock-chain

CircleCI Maintainability Test Coverage GPLv3 license

Create complex mocks/doubles with ease.

Example

Imagine a chain of methods and objects like this:

$body->getSystem('nervous')->getOrgan('brain')->getName();

Creating a mock for the body object with phpunit alone might look like this:

$organ = $this->getMockBuilder(Organ::class)
    ->disableOriginalConstructor()
    ->onlyMethods(['getName'])
    ->getMock();

$organ->method('getName')->willReturn('brain');

$system = $this->getMockBuilder(System::class)
  ->disableOriginalConstructor()
  ->onlyMethods(['getOrgan'])
  ->getMock();

$system->method('getOrgan')->willReturn($organ);

$body = $this->getMockBuilder(Body::class)
  ->disableOriginalConstructor()
  ->onlyMethods(['getSystem'])
  ->getMock();

$body->method('getSystem')->willReturn($system);

The implementation of a simple chain of mocks can become very verbose. The purpose of this library is to make this process simpler. Here is the same mocked object implemented with a mock-chain:

$body = (new Chain($this))
    ->add(Body::class, 'getSystem', System::class)
    ->add(System::class, 'getOrgan', Organ::class)
    ->add(Organ::class, 'getName', 'brain')
    ->getMock();

Documentation

The majority of the work that can be done with this library happens through a single class: The Chain class.

By exploring the few methods exposed by this class, we should be able to understand the full power of the library.

Mocking an Object and a Single Method

With mock-chain we can mock an object and one of its methods in a single line of code.

$mock = (new Chain($this))
      ->add(Organ::class, "getName", "heart")
      ->getMock();

Let's explore what is happening here.

(new Chain($this))

Here, we are calling the constructor of the Chain class to create a Chain object. The extra parenthesis around the call to the constructor allow us to immediately start calling methods without keeping a reference of the Chain object itself.

The Chain class is a "better" interface around the mocking capabilities provided by phpunit, but all the mocking power comes from phpunit. This is why the constructor of the Chain class takes a PHPUnit\Framework\TestCase object.

->add(Organ::class, "getName", "heart")

The add method is used to inform the Chain object of the structure of the mock or mocks that we wish to create.

The first argument to add is the full name of the class for which we want to create a mock object. In our example we want to create an Organ object.

The class name is the only required parameter in the add method, but more often than not we want to mock a call to a method of an object. The extra, optional, parameters allow exactly that.

The second parameter is the name of a method in the Organ class: getName.

The third parameter is what we want the mocked object to return when getName is called. In our example we want to return the string "heart".

Finally,

->getMock()

returns the mock object constructed by the Chain class.

We can easily check in a test that our mock object is working as expected:

$this->assertEquals("heart", $mock->getName());

Mocking an Object and Multiple Methods

To mock multiple methods, we simply call add multiple times.

$mock = (new Chain($this))
        ->add(Organ::class, "getName", "heart")
        ->add(Organ::class, "shoutName", "HEART")
        ->getMock();

Chain assumes that each class name is used to generate a single mock object of that class. So, this chain does not create two mock Organ objects, but a single Organ object with both getName and shoutName mocked.

Because it is common to mock multiple methods for a single object, the Chain class provides a method to make this operation less verbose: addd (with three Ds).

With the addd method we can simplify our example like this:

$mock = (new Chain($this))
        ->add(Organ::class, "getName", "heart")
        ->addd("shoutName", "HEART")
        ->getMock();

When addd is used, the Chain assumes that the method is a mock of whatever the last named class was before addd was called. In our case it is the Organ class.

The impact is very subtle, but we have found that in complex mocks, using addd also provides a visual break to easily see the different types of objects being mocked.

Returning Mocks

The third parameter of the add method can be given anything to be return by the mocked method: strings, arrays, objects, booleans, etc.

We can even return another mocked object. Addressing this scenario is the main reason this library exist, and why it is called mock-chain: We want to be able to define chains of mocked objects and methods easily.

To accomplish our goal we simply return the class name of the mock object we want to return.

$mock = (new Chain($this))
        ->add(System::class, "getOrgan", Organ::class)
        ->add(Organ::class, "getName", "heart")
        ->addd("shoutName", "HEART")
        ->getMock();

It is important to note that in this new example the main mock object returned by getMock is of the System class. Whatever the first named class that is registered with the Chain is, becomes the root of the chain. Any other mocks will only be accessible through interactions with the root object.

A second mock object of class Organ is also being defined, and it is accessible through the getOrgan method from the mocked System object.

Given this structure, we can make assertions across our mocks:

$this->assertEquals("heart",
    $mock->getOrgan("blah")->getName());

Mocking Different Returns with Sequences

Through some paths of our code, we might need the same mocked object to respond differently under different circumstances. There are multiple ways to accomplish this with mock-chain, but the simplest way is to use the Sequence class.

A Sequence allows us to define a number of things that should be returned, in order, every time a method is called.

$organNames = (new Sequence())
        ->add("heart")
        ->add("lungs");

$mock = (new Chain($this))
  ->add(Organ::class, "getName", $organNames)
  ->getMock();

$this->assertEquals("heart", $mock->getName());
$this->assertEquals("lungs", $mock->getName());

In this example we are creating a Sequence of organ names, and we are telling the chain that this sequence of things should be returned when the getName method in our Organ mock is called.

Our assertions confirm the expected behavior by showing that "heart" is returned when getName is first called, and "lungs" when getName is called a second time. If getName was to be called a third or fourth time, "lungs" would be returned again.

Similarly to how we can return anything from mocked methods, including other mocks, we can do the same with sequences.

$organs = (new Sequence())
        ->add(Organ::class)
        ->add("lungs");

$mock = (new Chain($this))
  ->add(System::class, "getOrgan", $organs)
  ->add(Organ::class, "getName", "heart")
  ->getMock();

$this->assertEquals("heart", $mock->getOrgan("blah")->getName());
$this->assertEquals("lungs", $mock->getOrgan("blah"));

Here we are returning a mock of Organ as the first element of the sequence, and a string as the second without any issues.

Mocking Different Returns with Options

Options give us a bit more power than Sequence by allowing us to take into account the input to the mocked methods as we decide what should be returned.

$organs = (new Options())
  ->add("heart", Organ::class)
  ->add("lungs", "yep, the lungs");

$mock = (new Chain($this))
  ->add(System::class, "getOrgan", $organs)
  ->add(Organ::class, "getName", "heart")
  ->getMock();

$this->assertEquals("yep, the lungs",
    $mock->getOrgan("lungs"));
$this->assertEquals("heart",
    $mock->getOrgan("heart")->getName());

In this Options object we are defining that a call to getOrgan with an input of "hearts" should return our Organ mock, but a call to getOrgan with an input of "lungs" should return the string "yep, the lungs". Notice in the assertions that the order of the options does not matter.

If we are dealing with more complex methods that take multiple inputs/arguments, Options have two mechanisms to deal with these scenarios: index and JSON string.

Index

$organs = (new Options())
  ->add("heart",Organ::class)
  ->add("lung", "yep, the left lung")
  ->index(0);

$mock = (new Chain($this))
  ->add(System::class, "getOrganByNameAndIndex", $organs)
  ->add(Organ::class, "getName", "heart")
  ->getMock();

$this->assertEquals("yep, the left lung",
  $mock->getOrganByNameAndIndex("lung", 0));
$this->assertEquals("heart",
  $mock->getOrganByNameAndIndex("heart", 0)->getName());

In this example we have a more complex method getOrganByNameAndIndex that takes 2 arguments: an organ name and an index. If during the process of mocking we determine that we only care about one of the arguments to our method, we could model that by using the index method of the Options class. In this example, we are describing that we only care about the first argument, the organ name, when determining what to return.

JSON string

$organs = (new Options())
  ->add(json_encode(["lung", 0]),"yep, the left lung")
  ->add(json_encode(["lung", 1]), "yep, the right lung");

$mock = (new Chain($this))
  ->add(System::class, "getOrganByNameAndIndex", $organs)
  ->add(Organ::class, "getName", "heart")
  ->getMock();

$this->assertEquals("yep, the left lung",
  $mock->getOrganByNameAndIndex("lung", 0));
$this->assertEquals("yep, the right lung",
  $mock->getOrganByNameAndIndex("lung", 1));

When we have complex methods with multiple arguments that we want to take into account when making decisions about what to return, we can always create a JSON string of an array representing the inputs to our method.

In our example, when the inputs to getOrganByNameAndIndex are "lung" and 0, we want to return "yep, the left lung". But, if the inputs to our method are "lung", and 1, we would like to return "yep, the right lung".

mock-chain's People

Contributors

dafeder avatar fmizzell avatar paul-m avatar thierrydallacroce avatar

Stargazers

 avatar  avatar

Watchers

 avatar  avatar

mock-chain's Issues

Unlock PHPUnit 9.5 by changing onlyMethods() usages

The Issue:

Updating to PHPUnit 9.5.x leads to this error:

$ composer show phpunit/phpunit | grep versions
versions : * 9.5.27
$ ./vendor/bin/phpunit 
PHPUnit 9.5.27 by Sebastian Bergmann and contributors.

.....E.....                                                       11 / 11 (100%)

Time: 00:00.057, Memory: 4.00 MB

There was 1 error:

1) MockChainTest\ChainTest::testNonExistentMethod
PHPUnit\Framework\MockObject\CannotUseOnlyMethodsException: Trying to configure method "blah" with onlyMethods(), but it does not exist in class "MockChainTest\Anatomy\Organ". Use addMethods() for methods that do not exist in the class

/var/www/html/src/Chain.php:183
/var/www/html/src/Chain.php:144
/var/www/html/src/Chain.php:115
/var/www/html/test/ChainTest.php:122

ERRORS!
Tests: 11, Assertions: 28, Errors: 1.

This is because PHPUnit has changed how onlyMethods() and addMethods() is used.

Solution:

  • Determine how this issue intersects with our implementation.

After some analysis, it turns out that the problem here is that PHPUnit changed the exception class and message for methods which don't exist on the mocked class.

The test fails because it doesn't know about this change after PHPUnit 9.5.0.

The behavior we want seems to be that we'd see the exception (and fail whatever test we're writing) if the method doesn't exist on the class. This PR expands the test to account for the different exceptions thrown by different versions of PHPUnit.

  • Fix the issue.

Adds a class_exists() conditional to check for the different types of exceptions that can be thrown, and account for the different error messages given by PHPUnit.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.