Don't use Mocking libraries

in #php6 years ago (edited)

I am all for testing. Whether you always write unit tests in advance as Test Driven Development (TDD) advocates call for, write them after, write them alongside, a little of each, I don't care. Tests are your friend. You want tests, and specifically you want good tests.

There's a lot of opinions on what constitutes a "good" test, of course, and much is subjective to the type of code you're working on. However, since the release of PHP 7 I've found that while writing tests... I am never using a mocking library. In fact, I'm going to go as far and say that

You should never use a mocking library in PHP 7.

Before all of you gasp, clutch your pearls, and send ninja hit squads after me, let me justify that position.

Disclaimers

First, to be fair, since PHP 7 came out my PHP code has been mostly greenfield components. It's quite possible that what I'm recommending here wouldn't apply in a convoluted legacy application. Or perhaps it would; I just haven't had the chance to try it.

Second, I am not against mocking (as my Twitter followers will attest). Mocking an object is often a very useful thing to do in testing, although it can be easily overused. It is libraries specifically dedicated to creating mocks that I find little to no use for anymore.

What is a Mock?

As usual, let's start with Wikipedia:

In object-oriented programming, mock objects are simulated objects that mimic the behavior of real objects in controlled ways. A programmer typically creates a mock object to test the behavior of some other object, in much the same way that a car designer uses a crash test dummy to simulate the dynamic behavior of a human in vehicle impacts.

There's also the related concepts of "Fakes" and "Stubs". Again, quoting Wikipedia:

Classification between mocks, fakes, and stubs is highly inconsistent across the literature.

(Naturally)

For our purposes, I'm referring to any and all objects created for the sole or primary purpose of testing some other object, whatever the specifics.

What is a Mocking library?

Not to be confused with a Mocking Bird, a mocking library is a library that provides some automated way to define and create a mock object. Usually it has some sort of domain-specific-language (DSL) to do so, and will produce an object that will behave as though it were a real object but with the configured behavior. That object can then be used in place of a real object to test the behavior of the object being tested.

Most testing advocates I know recommend being sparing with mock objects. Over-mocking is an indication that your code is too tightly coupled. You should not mock someone else's class (a good life rule in general), but only mock your own bridge code. Mocks make the tests harder to read. Etc.

Another factor to consider is whether you need to mock a class at all. If you're testing the behavior of a given object, and that object's behavior is meaningless without some other object... why are you testing them separately? The "unit" of a unit test does not have to be a class or method. Not every method needs a dedicated test. Every behavior you expect to behave a certain way needs unit tests, but that may correspond to a single method, a set of methods on an object, or a set of objects acting together.

Further, if you have value objects in your system (including domain objects), you really shouldn't be mocking them. There's no point to. They're "just" data, and if it's super-hard to create that data object then it's a sign that you have something else wrong.

"If it's hard to test it means you're doing it wrong" is a really valuable guideline to follow. And if you need to mock something, that means it's at least somewhat hard to test. So minimize how often you need a mock to begin with.

About that DSL

Still, sometimes you need a mock. Maybe you're just satisfying a requirement like a logger that is irrelevant to your current test, maybe you're trying to simulate a broken service object, or maybe setting up a real object in the specific way you want would be highly cumbersome. OK, time to mock it.

Tell me, how readable is this PHPUnit code?

$mock_repository = $this
  ->getMockBuilder(ThingieRepository::class)
  ->disableOriginalConstructor()
  ->getMock();

$obj1 = new Thingie();
$obj1->setVal('abc');

$obj2 = $this->getMockBuilder(Thingie::class)
  ->disableOriginalConstructor()
  ->getMock();
$obj1 ->method('getVal')->willReturn('def');

$map = [ [1, $obj1], [2, $obj2] ];

$mock_repository
  ->method('load')->will($this->returnValueMap($map));

$subject = new ClassUnderTest($mock_repository);
$this->assertTrue($subject->findThings());

What does that Mock actually do? It takes 3 calls to get it in the first place. Then we have to configure it using a DSL that specifies various methods in strings. Oh, actually it's 2 mocks. And... I couldn't actually tell you what it is going to do.

Other mocking libraries have other syntaxes, but they're all some custom meta language that you need to learn to both write and read.

This sucks.

Now, compare with this:

$fake_repository = new class extends ThingieRepository {
  public function __construct() {}

  public function load($id) {
    switch ($id) {
      case 1:
        $obj1 = new Thingie();
        $obj1->setVal('abc');
        return $obj1;
      case 2:
        return new class extends Thingie {
          public function getVal() {
            return 'def';
          }
        };
    }
  }
};

$subject = new ClassUnderTest($fake_repository);
$this->assertTrue($subject->findThings());

Boom. We're still defining a mock class. We're defining the exact same behavior of the mock class. But we're doing it as a for-reals PHP class. We're just doing it inline thanks to PHP's support for anonymous classes, introduced in PHP 7.0 nearly 3 years ago.

I would argue that using anonymous classes for mock objects is superior in every possible way.

  • It's easier to read. It's just plain PHP.
  • It's faster. This is pure native PHP, at full speed. No custom setup, no hidden eval() happening somewhere deep in your testing framework, just plain old PHP.
  • There's no custom syntax to learn. Methods, values, returns, are all the exact same code you're used to.
  • You can still control precisely how it behaves without learning a new syntax. In this case, for instance, we've said that load() gets called twice and will return a different object depending on the ID it's called with, something that is trivially obvious from reading it. That's not the case with the convoluted mess that is $mock_repository->method('load')->will($this->returnValueMap($map));
  • It's refactorable. If you rename a method or a class or an interface in your IDE, your IDE can, generally, update all references to it throughout your code base. (If it can't, it's a pretty terrible IDE.) If you have method and class names all wrapped up in strings that gets harder for it to do. This way, it's all just normal boring PHP syntax. The IDE can update your mocks for you automatically without you even thinking about it!
  • Your IDE will help you write it, simply because (that's right), it's plain PHP.

You can make an anonymous class implement an interface; it can extend a real class and just override certain parts of it. It can have a custom constructor that pulls in context from your test. It slices, it dices, it makes julienne fries!

(Fake) Code reuse

Of course, sometimes you need to reuse a mock object. You may need it in multiple tests, or in different contexts but slightly different. With a mocking library you can create the mock once in a setUp() method and store it in an object property on your test class. Well, you can do the exact same thing with an anonymous class object. But it can go one steps better.

For one, if you need to do some additional setup on the class it's super simple to just add an extra method on it that's not part of the interface being tested and call that in your test. For example:

$logger = new class implements Psr\Log\LoggerInterface {
   use LoggerTrait;

   protected $stream;

   public function log($level, $message, array $context = [])
   {
       fwrite($this->stream, $message);
   }

   public function setStream($stream)
   {
       $this->stream = $stream;
   }
};

It's immediately obvious what this test class is doing. Create that in your setUp() method, save it, and then in each test method give it a new stream. Or, if you want to go even simpler, make $stream public and just set it directly. Go ahead, it's just a mock. The usual rules about public properties don't apply.

All with plain PHP.

(Not fake) Code reuse

Sometimes, though, anonymous classes can get too clunky to read as well. They do run into readability scaling issues just like anonymous functions do. And sometimes you want to reuse them so much that redefining them in a single setUp() method is too annoying. So what do you do.

Make it a class.

Yes, a for reals, honest to goodness concrete class. All it needs to do is implement the interface you're testing. (Because if it doesn't have an interface, what are you doing?) For instance, I'm working on a library that can make use of a PSR-11 dependency injection container to do lazy loading. So here's the class I wrote to test it:

class MockContainer implements ContainerInterface
{
   protected $services = [];

   public function addService(string $id, $service)
   {
       $this->services[$id] = $service;
   }

   public function get($id)
   {
       if (isset($this->services[$id])) {
           return $this->services[$id];
       }
       throw new class extends \Exception implements NotFoundExceptionInterface {};
   }

   public function has($id)
   {
       return isset($this->services[$id]);
   }
}

It's maybe 20 lines long. It's stupidly simple, won't scale, doesn't lazy-anything... and it doesn't need to. It's just for testing. But it's also completely self-evident what it's doing. I just tuck this class away in my tests folder (which is totally legit!), and then use it in a test like this:

$container = new MockContainer();
$container->addService('some_service', new SomethingImTesting());

$container->addService('logger', new class implements LoggerInterface {
    Use LoggerTrait;
    public function log($level, $message, array $context = []) {}
}));

And on we go. I can now use this just like a real container because... it is. And anything that is supposed to get a logger out of it gets a logger, just a fake one that does nothing because logging isn't what I'm testing. And it's all plain, ordinary, fast, readable PHP.

Assertions

You can of course call assertions on these lightweight mocks. Just give them an extra method that keeps track of the thing you're trying to test, and check a value afterward in the assertion. Make the properties you're using for that public if you want. It's a test, that's fine. For example, I have a test where I expect certain objects to be called in a certain order. So I have a fake object that just records when it's called and then I assert that its record is what I expect:

$this->assertEquals('BCAEDF', implode($event->result()));

Upper bounds

Of course, this approach also has its own upper bounds. Eventually you could get test classes that are so big that they're their own real code that deserves its own tests. That generally indicates one of three things:

  • Why yes, that is real code that is worth being promoted out of the test directory and being made a real class, because apparently it's that useful. I've done this before, and I've gotten some good and useful utilities out of it.
  • The code you're trying to test is too complicated and convoluted in the first place. Stop that and refactor it so it's easier to test.
  • Your tests are too precise and you're testing implementation, not behavior. (Any test that asserts how many times a method on a mock is called is probably wrong.)

Or sometimes a combination of the above.

Conclusion

So yes, by all means, test your code. By all means, use mocks where appropriate (although that's less often than you think). But no, you don't need a mocking library to do it. Native PHP functionality and having a good design in the first place provide everything you need with no extra overhead or syntax to learn.

Enjoy your faster, easier to read tests and better designed code!

Sort:  

Great write up. I laughed at this:

You should not mock someone else's class (a good life rule in general),

I generally prefer functional tests over unit tests with mocking and such. So many people flip out and say, “but, but, what if you database dies or something else breaks due to the combination of everything together?” Etc. In my experience that’s usually the kind of thing that happens in production and code with high unit test coverage can’t handle it, and the devs have little experience recovering from it. Functional tests (from request, all the way to the db and back again to response) seem to give me the most bang for the buck and can be really helpful when refactoring something internal to ensure it didn’t have some larger consequence.

The line between unit and functional test is much fuzzier than the purists would argue. Generally speaking, the more contained something is the more tests it should have.

If you're building a library (which is most of the PHP I've been writing lately), the emphasis should be on small-unit tests. If you're building an application that is mostly just stitching together other libraries (which most should be), then yes, you'll probably have mainly functional/end-to-end tests. That's because the lower level tests are already written for you by the library author (who may be you, or not).

My current project is Tukio, a reference implementation of the in-progress PSR-14 standard for PHP. (That's where the examples in the article came from.) There's by design no external resources to think about (DB, etc.), so everything I'm doing is basically just feeding it data, where the data is sometimes a callable or class instance. So... I just make callables and class instances, kthxbye.

If you're making a library that uses a network connection, mocking that in your tests is crucial because you don't want to introduce the 500 extra variables of a network connection into your tests. You want to simulate the collection of variables you care about and test those. (Fake a 200 response, fake a timeout, etc.)

I am also, on the side, working on a simple photo gallery in Symfony 4. I'll be honest... it has no tests at all at the moment. That's because it's maybe 20 lines of code, plus templates, plus a bunch of external libraries. There's barely anything there to test, and I'm mostly just fiddling with it to see what happens at this point. At some point I'll probably put some functional tests around it, on principle, but at this point the fake image data I'd have to generate for it and the tooling to support it is not worth it. The libraries I'm using are all from Symfony and the PHP League, though, so I'm confident they're all very well tested. (Haven't hit a bug yet.)

As I've said elsewhere, best practices are contextual.

Congratulations @crell! You have completed the following achievement on the Steem blockchain and have been rewarded with new badge(s) :

Award for the total payout received

Click on the badge to view your Board of Honor.
If you no longer want to receive notifications, reply to this comment with the word STOP

Support SteemitBoard's project! Vote for its witness and get one more award!

Coin Marketplace

STEEM 0.31
TRX 0.34
JST 0.053
BTC 103051.72
ETH 3935.91
SBD 4.10