What to mock in a Magento 2 unit test

You write unit tests for a Magento 2 module and need to mock a core class, like the store model. This class has a few dependencies which have more dependencies and it’s getting complicated to mock. Although PHPUnit 5.4 has a createMock() method that automatically stubs methods and let them return new stubs if they have a return type.

Or: you mock a service contract like getList() in the product repository, which takes a search criteria instance as parameter, which must be returned by a search criteria factory. Then it returns a product search result, which has a getItems() method, which returns a array of products. So you end up mocking countless dependendencies for a single method call

Do these situations sound familiar? It does not need to be like that, though! Let me show you a sane way to deal with Magento dependencies.

First, you should only mock interfaces, not classes. This way, the test does not accidentally execute code of the real class, which would require mocking injected dependencies. You also force yourself to only use methods which are part of the public API.

But this does not help with complicated APIs like in the repository described above.

Some smart people at ThoughtWorks came up with the guideline “Only mock types you own” 1

Instead [programmers] should write thin wrappers to implement the application abstractions in terms of the underlying infrastructure. Those wrappers will have been defined as part of a need-driven test.

This should not be taken as a strict rule, otherwise you end up with the other extreme, having wrappers over wrappers for everything. But it can be extremely helpful to simplify the API and tailor it to your needs.

Simplify complicated API

Here is a possible wrapper for the product repository, if you need to find products by manufacturer:

interface ProductRepository
{
    /**
     * @api
     * @return ProductInterface[]
     */
    public function productsByManufacturer($manufacturer);
}

There, no search criteria and no search result. It is easier to mock and also easier to use. The calling code is only interested in manufacturer and products, we leave the framework details in the implementation of our wrapper.

You might ask, how to test this wrapper. The answer is, it should be tested via integration tests of the whole feature, without mocks. A unit test does not make sense for it.

Add missing API

I use the same approach if I need a feature that does not have a service contract (yet). For example to check the customer login status:

interface CustomerSession
{
    /**
     * @api
     * @return bool
     */
    public function isLoggedIn();
}

The implementation is simply a wrapper around \Magento\Customer\Model\Session\Proxy which forwards the isLoggedIn() call. This way I dodged mocking the session directly, which would be no fun at all.

Use fakes instead of mocks

There are several types of test dummys, the most popular probably stubs (where you specify what they should return for which method calls) and mocks (where you can make assertions on how they are used, additionally). In PHPUnit both are created with getMock(), so they are often confused.

But there are more, and one that I learned to like is the fake, which does not even need a mocking framework. It is a real implementation of the interface, specifically for tests. A typical example would be a InMemoryMailer implementation for an email interface, which saves the mail in memory instead of sending them, so it can be inspected by the test.

But even the CustomerSession from above could benefit from a fake implementation, like this:

final class FakeSession implements CustomerSession
{
  private $loggedIn = false;
  public function isLoggedIn()
  {
    return $this->loggedIn;
  }
  // test methods:
  public function login()
  {
    $this->loggedIn = true;
  }
  public function logout()
  {
    $this->loggedIn = false;
  }
}

Then this test code

$session = $this->createMock(CustomerSession::class);
$session->method('isLoggedIn')->willReturn(true);

becomes:

$session = new FakeSession();
$session->login();
  • more succinct
  • more readable
  • reusable accross tests
  • works with all test frameworks

The more effort you have to put into mocking something, the more benefits you will get from fake objects instead.

Summary

I recommend the following rules of thumb:

  • Mock service contracts (API interfaces), not concrete classes
  • If the API is too complex, write a simplified wrapper, mock the wrapper
  • If the API is lacking methods that you need, write your own, mock that
  • Provide fake implementations for your interfaces

Notes:

  1. Freeman, Steve, et al. “Mock Roles, not ObjectsCompanion to the 19th annual ACM SIGPLAN conference on Object-oriented programming systems, languages, and applications. ACM, 2004.

2 Replies to “What to mock in a Magento 2 unit test”

  1. Thank you for this blog post. Do you know if there’s any work started for providing fakes for the main M2 core classes?

    We have started to create very simple in memory repositories for instance to support testing and would love to share with others.

    1. Good question! I don’t know of any work in that direction, but I’ll find out. If you share your work, I’d like to help to get it integrated into the core, it would be a valuable addition!

Comments are closed.