Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mocking Library #219

Closed
iigorr opened this issue Nov 17, 2011 · 15 comments
Closed

Mocking Library #219

iigorr opened this issue Nov 17, 2011 · 15 comments

Comments

@iigorr
Copy link
Member

iigorr commented Nov 17, 2011

Scope of Change

A mocking framework will be added to the unittest API.

Rationale

Easily mock interfaces and verify expected behaviour similar to EasyMock (Java), Rhino Mocks (.Net) and many other mocking frameworks.

Functionality

A new namespace unittest.mock is introduced containing everything that enables mock functionallity.

Step-by-step approach

The mock library is still under development and does not yet implement all the features grown-up mocking frameworks do. So here is a feature list and a list of things that are not yet implemented (in order of importance).

Implemented:

  • generating mocks/stubs (all methods return null by default)
  • return values of mocks/stubs
  • repetitions of returns
  • argument matching
  • Behaviour verification

Not implemented

  • Execution order verification
  • Strict mocks (unsatisfied expectation)
  • Rewinding the replay state
  • More predefined argument matchers (by type, regex)

Usage:

Creating a Mock

Consider service MyService that depends on an IContext which let's you check permissions and read some data:

/* IContext.class.php */
<?php
  interface IContext {
    function getData();
    function hasPermission($permission);
  }
 ?>


/* MyService.class.php */
<?php
  uses('tutorial.IContext');
  class MyService {
    private $context= null;
    private $secret="abracadabra!";

    /*
     * Constructor
     *
     * @param IContext context
     */
    public function __construct($context) {
      $this->context=$context;
    }

      public function readContextData() {
        return $this->context->getData();
      }

    public function getSecretStuff() {
    if($this->context->hasPermission("rt=foo,rn=bar"))
        return $this->secret;
      else
        throw new IllegalAccessException("Permission denied!");
    }
  }
  ?>

Now if we want to test the MyService class we have to construct an instance. Probably we would even write a unit test to verify that the construction works:

<?php
  #[@test]
  public function canCreate() {
    new MyService(null);
  }
?>

To create a MyService instance we need an IContext, but in this case it is absolutely irrelevant what the context is, so we just pass null.

When it comes to testing the readContextData method, this won't work as we need a working context.

<?php
  #[@test]
  public function can_call_readContextData() {
    $fixture= new MyService(null);
    $fixture->readContextData();
  }
?>

So we need to initialize an actual IContext. However we might have no concrete class which implements IContext and is easy to instantiate, i.e. without resolving other dependencies like connecting to databases/LDAPs, reading ini-files, configuring stuff etc.

Here is where mocks come into play. We just tell the mock repository to create an object which implements the IContext interface.

<?php
  #[@test]
  public function can_call_readContextData() {
    $mockRepo=new MockRepository();
    $context= $mockRepo->createMock('tutorial.IContext'); //create an IContext
    $mockRepo->replayAll(); //ignore this for a moment.

    //here comes the actual test
    $fixture= new MyService($context);
    $fixture->readContextData(); //returns null
  }
?>

Return Values

By default each method of a mocked object returns null. However we might need the dependent object to return actual stuff.
For example to completely test the method getSecretStuff, we need to check two cases.
1) $this->context->hasPermission("rt=foo,rn=bar") returns TRUE;
2) $this->context->hasPermission("rt=foo,rn=bar") returns FALSE;

To accomplish that we just tell the context to return the value we want it to.

<?php
    $context= $mockRepo->createMock('tutorial.IContext');
    $context->hasPermission(null)->returns(TRUE); //tell hasPermission to return TRUE
?>

So now we can write the following test:

<?php
  #[@test]
  public function getSecretStuff_works_withPermissions() {
    $mockRepo=new MockRepository();
    $context= $mockRepo->createMock('tutorial.IContext');
    $permission= "rt=foo,rn=bar";
    $context->hasPermission($permission)->returns(TRUE); //tell hasPermission to return TRUE
    $mockRepo->replayAll();

    $fixture= new MyService($context);
    $this->assertEquals("abracadabra!", $fixture->getSecretStuff());
  }
?>

To test the exception case (no permissions) we use the following test:

<?php
  #[@test, @expect('lang.IllegalAccessException')]
  public function getSecretStuff_throwsException_whithoutPermissions() {
    $mockRepo=new MockRepository();
    $context= $mockRepo->createMock('tutorial.IContext');
    $permission= "rt=foo,rn=bar";
    $context->hasPermission($permission)->returns(FALSE); //no permissions
    $mockRepo->replayAll();

    $fixture= new MyService($context);
    $this->assertEquals("abracadabra!", $fixture->getSecretStuff());
  }
?>

Repetitions

Note that the defintion of the return value is only valid for one call

<?php
    $context->hasPermission($permission)->returns(TRUE);
    $mockRepo->replayAll();

    $context->hasPermission($permission); //first call -> TRUE
    $context->hasPermission($permission); //second call -> null
?>

Thus if you expect that hasPermission is called twice, you need to define the return values for both calls, i.e.

<?php
    $context->hasPermission($permission)->returns(TRUE); //first call
    $context->hasPermission($permission)->returns(FALSE); //second call
    $mockRepo->replayAll();

    $context->hasPermission($permission); //first call -> TRUE
    $context->hasPermission($permission); //second call -> FALSE
?>

A third call to hasPermission would again yield null.

If you want the same value to be returned a specific number of times you can use ->repeat($numOfRepetions) or ->repeatAny() to repeat the same value permanently.

<?php
    $context->hasPermission($permission)->returns(TRUE)->repeat(2); //first call
    $mockRepo->replayAll();    

    $context->hasPermission($permission); //first call -> TRUE
    $context->hasPermission($permission); //second call -> TRUE //repeat
    $context->hasPermission($permission); //third call -> NULL
?>

Argument Matching

When methods have a wide range of input values, it might be difficult to define all expectations. Just imagine if getSecretStuff would check 10 different permissions. We would have to specify an expectation for each of them. For such cases there is a generic argument matching mechanism.

We can use Arg::any() if the argument is irrelevant.

<?php    
$context->hasPermission(Arg::any())->returns(TRUE); //return TRUE, for any argument 
?>

If specific argument matching rules are required, we may implement an own argument matching class. This class has to implement the IArgumentMatching interface.

<?php
    class MySpecialMatcher extends Object implements IArgumentMatching {
      /**
       * Special matching.
       *
       * @return boolean
       */
      public function machtes($value) {
        //implement matching logic here
      }
    }
?>

The object is passed as a parameter:

<?php
    $context->hasPermissions(new MySpecialMatcher())->retuns("foo")->repeatAny();

    $mockRepo->replayAll();
    $context->hasPermissions("foobar"); //"foobar" is passed to the matches 
                                        // method of the MySpecialMatcher object
?>

Record/Replay/Verfiy

Now about that $mockRepo->replayAll() call. This is part of the Record/Replay/Verfiy paradigm, that is used for the mocking framework. In the Record phase you tell your mocks how they should behave. With replayAll you activate the Replay Phase. Now when you call methods on your mocks they will actually return the stuff you have recorded. In Verification phase you check whether your recorded expectations are fulfilled.

See http://ayende.com/Blog/archive/2007/12/26/The-RecordReplayVerify-model.aspx

Behavior verification

The verification of a mock is just a check, that ensures that all the calls that were defined in the recording phase are also performed in the replay phase.

You can use $mock->_verifyMock() to verify a single mock, or $mockRepo->verifyAll() to verify all mocks of a mock repository.

Example:
Suppose you want to write a test, that ensures that, no matter what, hasPermissions is called when you call getSecretStuff():

<?php
  #[@test]
  public function getSecretStuff_should_call_hasPermissions() {
    $mockRepo= new MockRepository();
    $context= $mockRepo->createMock('tutorial.IContext');
    $context->hasPermission(Arg::any())->returns(TRUE); //expect a call to hasPermission with any argument
    $mockRepo->replayAll();

    $fixture= new MyService($context);
    $fixture->getSecretStuff();
    $mockRepo->verifyAll(); //fail if hasPermission is not called
  }
?>

If hasPermissions is not called in the above test, then a ExpectationViolationException is thrown, saying "Expectation not met for 'hasPermission'. expected#: 1 called#: 0"

Property Behavior (State Verification)

Somethimes you have properties, that you expect to be set after a test run. The mock library provides a special mode for properties called "Property Behavior". It may be applied to properties that provide a getter and a setter method.

E.g.

<?php
  #[@test]
  public function getSecretStuff_should_call_hasPermissions() {
    $mockRepo= new MockRepository();
    $context= $mockRepo->createMock('tutorial.IContext');
    $context->getName()->propertyBehavior(); //
    $mockRepo->replayAll();

    $context->setName('MyContext'); //will set the property "Name" to 'MyContext'
    $this->assertEqual("MyContext", $context->getName());
  }
?>

This applies only to methods starting with "get" and "set".

Security considerations

n/a

Speed impact

Unittests will run faster if simple mocks are used instead of complex instantiation of dependency objects.

Dependencies

n/a

Related documents

@thekid
Copy link
Member

thekid commented Nov 21, 2011

Readability

Please add some formatting, especially for sourcecode.

Questions

Where does $mockery come from in the above example in this context:

$sessionMock= $mockery->createMock('Session');

Quality

This cannot be a real-life example:

$sessionMock->getId()->return(7); //return 7 if getId is called

...as return is a keyword an definitely not allowed as method name.

Details

Please also go into some more detail about the mocking API - what about expecting method arguments? What about member access mocking? How can I make the mock throw exceptions? ...

@iigorr
Copy link
Member Author

iigorr commented Nov 21, 2011

Readability

Please add some formatting, especially for sourcecode.

Ok, updated.

Questions

Where does $mockery come from in the above example in this context:

$sessionMock= $mockery->createMock('Session');

$mockery= new Mockery(); //updated

Quality

This cannot be a real-life example:

$sessionMock->getId()->return(7); //return 7 if getId is called

...as return is a keyword an definitely not allowed as method name.

Typo. The method is called "returns". Fixed

Details

Please also go into some more detail about the mocking API - what about expecting method arguments? What about > member access mocking? How can I make the mock throw exceptions? ...

I have created an introductory tutorial how to use the library. (http://experiments.xp-forge.net/xml/view?arena,mocks,INTRODUCTION.txt)
Its linked now in the RFC under "Related Documents"
As soon as the RFC will be accepted I could add the documentation to the github wiki.

@thekid
Copy link
Member

thekid commented Nov 21, 2011

Can you add the tutorial to this RFC? The experiments checkout no longer exists as central repository here on GitHub and this link will most probably lead to nowhere in the future. Sorry for being pedantic about this, but we want to keep all the necessary information in the RFC repository to be able to look back at our decisions in their entirety in the future. Thanks for understanding!

@iigorr
Copy link
Member Author

iigorr commented Nov 21, 2011

No problem. I'd be glad to upload it. I just wasn't sure where to put it? Comment? Wiki? As file at /rfc/219/tutorial.txt?

@thekid
Copy link
Member

thekid commented Nov 21, 2011

Why not add it to the functionality section - or alternatively, as apidoc for package-info.xp in the commits.

@iigorr
Copy link
Member Author

iigorr commented Nov 21, 2011

Ok, added it to functionality. As I said, it's written as an introductory tutorial. The style might not be appropriate for a RFC ;)

@iigorr
Copy link
Member Author

iigorr commented Nov 24, 2011

You can find the pull request issue here: xp-framework/xp-framework#83

@kiesel
Copy link
Member

kiesel commented Dec 16, 2011

+1 for this RFC. I'd propose to target 5.8.4 with this.

@thekid
Copy link
Member

thekid commented Dec 17, 2011

+0.9, I'm not happy with Mockery as class name - as far as I understand, it's a product name (and that of an American movie from 1927), anyways, we usually name classes after what they do, so this should be something like MockCreator or MockBuilder (or if we're aiming for brevity, even just Mock or Mocks). If we can resolve this issue I'd like to see this in 5.8.4, too.

@ghost ghost assigned iigorr Dec 17, 2011
@iigorr
Copy link
Member Author

iigorr commented Dec 18, 2011

How about MockRepository? It is not a pure builder/creator/factory as the "Mockery" knows it's created mock objects. This way you can switch the state of all mocks and verify them. This is also the name RhinoMocks use.

@thekid
Copy link
Member

thekid commented Dec 18, 2011

MockRepository sounds good, @kiesel ?

@kiesel
Copy link
Member

kiesel commented Dec 18, 2011

Yap, +1.

@thekid
Copy link
Member

thekid commented Dec 18, 2011

OK, set status to accepted. @iigorr, can you update this RFC with the new name, then update the pull request accordingly? We're ready to go then!

@iigorr
Copy link
Member Author

iigorr commented Dec 19, 2011

Ok, done.

thekid added a commit to xp-framework/xp-framework that referenced this issue Dec 19, 2011
@thekid thekid closed this as completed Dec 20, 2011
thekid added a commit to xp-framework/remote that referenced this issue Nov 10, 2013
# See xp-framework/rfc#219
# Implementation in pull request #83
thekid added a commit to xp-framework/doclet that referenced this issue Nov 10, 2013
# See xp-framework/rfc#219
# Implementation in pull request #83
thekid added a commit to xp-framework/zip that referenced this issue Nov 10, 2013
# See xp-framework/rfc#219
# Implementation in pull request #83
thekid added a commit to xp-framework/rdbms that referenced this issue Nov 11, 2013
# See xp-framework/rfc#219
# Implementation in pull request #83
thekid added a commit to xp-framework/csv that referenced this issue Nov 11, 2013
# See xp-framework/rfc#219
# Implementation in pull request #83
thekid added a commit to xp-framework/parser that referenced this issue Nov 11, 2013
# See xp-framework/rfc#219
# Implementation in pull request #83
thekid added a commit to xp-framework/rest that referenced this issue Nov 11, 2013
# See xp-framework/rfc#219
# Implementation in pull request #83
thekid added a commit to xp-framework/scriptlet that referenced this issue Nov 11, 2013
# See xp-framework/rfc#219
# Implementation in pull request #83
thekid added a commit to xp-framework/imaging that referenced this issue Nov 12, 2013
# See xp-framework/rfc#219
# Implementation in pull request #83
thekid added a commit to xp-framework/ftp that referenced this issue Nov 12, 2013
# See xp-framework/rfc#219
# Implementation in pull request #83
thekid added a commit to xp-framework/http that referenced this issue Nov 12, 2013
# See xp-framework/rfc#219
# Implementation in pull request #83
thekid added a commit to xp-framework/ldap that referenced this issue Nov 12, 2013
# See xp-framework/rfc#219
# Implementation in pull request #83
thekid added a commit to xp-framework/mail that referenced this issue Nov 12, 2013
# See xp-framework/rfc#219
# Implementation in pull request #83
thekid added a commit to xp-framework/news that referenced this issue Nov 12, 2013
# See xp-framework/rfc#219
# Implementation in pull request #83
thekid added a commit to xp-framework/irc that referenced this issue Nov 12, 2013
# See xp-framework/rfc#219
# Implementation in pull request #83
thekid added a commit to xp-framework/sieve that referenced this issue Nov 12, 2013
# See xp-framework/rfc#219
# Implementation in pull request #83
thekid added a commit to xp-framework/webdav that referenced this issue Nov 12, 2013
# See xp-framework/rfc#219
# Implementation in pull request #83
thekid added a commit to xp-framework/spelling that referenced this issue Nov 12, 2013
# See xp-framework/rfc#219
# Implementation in pull request #83
thekid added a commit to xp-framework/xml that referenced this issue Nov 12, 2013
# See xp-framework/rfc#219
# Implementation in pull request #83
thekid added a commit to xp-framework/telephony that referenced this issue Nov 12, 2013
# See xp-framework/rfc#219
# Implementation in pull request #83
thekid added a commit to xp-framework/mocks that referenced this issue Aug 6, 2015
@thekid
Copy link
Member

thekid commented Jan 9, 2018

Updated to work w/ current XP Framework releases:

https://github.com/xp-framework/mocks/releases/tag/v7.0.0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants