โฆfor a class to be easy to unit-test, the class must have explicit dependencies that can easily be substituted and clear responsibilities that can easily be invoked and verified.
Freeman, Steve; Pryce, Nat (2009-10-12). Growing Object-Oriented Software, Guided by Tests
function user_multiple_role_edit($accounts, $operation, $rid) { // The role name is not necessary as user_save() will reload the user // object, but some modules' hook_user() may look at this first. $role_name = db_query('SELECT name FROM {role} WHERE rid = :rid', array(':rid' => $rid))->fetchField(); switch ($operation) { case 'add_role': $accounts = user_load_multiple($accounts); foreach ($accounts as $account) { // Skip adding the role to the user if they already have it. if ($account !== FALSE && !isset($account->roles[$rid])) { $roles = $account->roles + array($rid => $role_name); // For efficiency manually save the original account // before applying any changes. $account->original = clone $account; user_save($account, array('roles' => $roles)); } } break;
class AddRoleUser extends ChangeUserRoleBase { public function execute($account = NULL) { $rid = $this->configuration['rid']; // Skip adding the role to the user if they already have it. if ($account !== FALSE && !$account->hasRole($rid)) { // For efficiency manually save the original account before applying // any changes. $account->original = clone $account; $account->addRole($rid); $account->save(); } } }
public function testExecuteAddExistingRole() { $account = $this ->getMockBuilder('Drupal\user\Entity\User') ->disableOriginalConstructor() ->getMock(); }
public function testExecuteAddExistingRole() { $account = $this ->getMockBuilder('Drupal\user\Entity\User') ->disableOriginalConstructor() ->getMock(); $account->expects($this->never()) ->method('addRole'); }
public function testExecuteAddExistingRole() { $account = $this ->getMockBuilder('Drupal\user\Entity\User') ->disableOriginalConstructor() ->getMock(); $account->expects($this->never()) ->method('addRole'); $account->expects($this->any()) ->method('hasRole') ->with($this->equalTo('test_role_1')) ->will($this->returnValue(TRUE)); }
public function testExecuteAddExistingRole() { $account = $this ->getMockBuilder('Drupal\user\Entity\User') ->disableOriginalConstructor() ->getMock(); $account->expects($this->never()) ->method('addRole'); $account->expects($this->any()) ->method('hasRole') ->with($this->equalTo('test_role_1')) ->will($this->returnValue(TRUE)); $config = array('rid' => 'test_role_1'); $role_adder = new AddRoleUser($config,'user_add_role_action', array('type' => 'user')); }
public function testExecuteAddExistingRole() { $account = $this ->getMockBuilder('Drupal\user\Entity\User') ->disableOriginalConstructor() ->getMock(); $account->expects($this->never()) ->method('addRole'); $account->expects($this->any()) ->method('hasRole') ->with($this->equalTo('test_role_1')) ->will($this->returnValue(TRUE)); $config = array('rid' => 'test_role_1'); $role_adder = new AddRoleUser($config,'user_add_role_action', array('type' => 'user')); $role_adder->execute($account); }
class SomeStuff { function __construct(StuffStorageInterface $stuff_storage) { $this->stuffstorage = $stuff_storage; } function getStuff($this_stuff) { return $this->stuffstorage->get($this_stuff); } } interface StuffStorageInterface { function set($id, $value); function get($id); }
class FakeStuffstorage implements StuffStorageInterface { public $stuffs = array(); function set($id, $value) { $this->stuffs[$id] = $value; } function get($id) { return $this->stuffs[$id]; } }
function testStuffWithFake() { $stuff_storage = new FakeStuffStorage; $stuff_storage->set('blah', 'asdf'); $somestuff = new SomeStuff($stuff_storage); $stuff = $somestuff->getStuff('blah'); $this->assertEquals('asdf', $stuff); }
function testStuffWithStubb() { $stuff_storage = $this->getMock('StuffstorageInterface'); $stuff_storage->expects($this->any()) ->method('get') ->will($this->returnValue('asdf')); $somestuff = new SomeStuff($stuff_storage); $stuff = $somestuff->getStuff('blah'); $this->assertEquals('asdf', $stuff); }
Drupal\Component\Reflection\MockFileFinder Drupal\edit_test\MockEditEntityFieldAccessCheck Drupal\system\Tests\FileTransfer\MockTestConnection Drupal\system\Tests\Routing\MockAliasManager Drupal\system\Tests\Routing\MockController Drupal\system\Tests\Routing\MockMatcher Drupal\system\Tests\Routing\MockRouteProvider Drupal\plugin_test\Plugin\CachedMockBlockManager Drupal\plugin_test\Plugin\MockBlockManager Drupal\plugin_test\Plugin\plugin_test\mock_block\MockComplexContextBlock Drupal\plugin_test\Plugin\plugin_test\mock_block\MockLayoutBlock Drupal\plugin_test\Plugin\plugin_test\mock_block\MockLayoutBlockDeriver Drupal\plugin_test\Plugin\plugin_test\mock_block\MockMenuBlock Drupal\plugin_test\Plugin\plugin_test\mock_block\MockMenuBlockDeriver Drupal\plugin_test\Plugin\plugin_test\mock_block\MockTestBlock Drupal\plugin_test\Plugin\plugin_test\mock_block\MockUserLoginBlock Drupal\plugin_test\Plugin\plugin_test\mock_block\MockUserNameBlock
function testStuffWithMock() { $stuff_storage = $this->getMock('StuffstorageInterface'); $stuff_storage->expects($this->once()) ->method('get'); $somestuff = new SomeStuff($stuff_storage); $stuff = $somestuff->getStuff('blah'); }
Mock Objects is a design technique, so programmers should only write mocks for types that they can change. Otherwise they cannot change the design to respond to requirements that arise from the process.
Steve Freeman, Tim Mackinnon, Nat Pryce, Joe Walnes - Mock roles, not objects OOPSLA '04
$processed_path = drupal_lookup_path('source', $base_path, $path_language)); if ($processed_path !== $path) { $path = $processed_path . '/' . implode('/', $subpath); return $path; }
$processed_path = $this->pathProcessor->processInbound($path, $request); if ($processed_path !== $path) { $path = $processed_path . '/' . implode('/', $subpath); return $path; }
# subpathauto.services.yml services: path_processor_subpathauto: class: Drupal\subpathauto\PathProcessor arguments: ['@path_processor_alias'] tags: - { name: path_processor_inbound }
public function testInboundSubPath() { $alias_processor = $this ->getMockBuilder('Drupal\Core\PathProcessor\PathProcessorAlias') ->disableOriginalConstructor() ->getMock(); }
public function testInboundSubPath() { $alias_processor = $this ->getMockBuilder('Drupal\Core\PathProcessor\PathProcessorAlias') ->disableOriginalConstructor() ->getMock(); $alias_processor->expects($this->once()) ->method('processInbound') ->with('content/first-node') ->will($this->returnValue('node/1')); }
public function testInboundSubPath() { $alias_processor = $this ->getMockBuilder('Drupal\Core\PathProcessor\PathProcessorAlias') ->disableOriginalConstructor() ->getMock(); $alias_processor->expects($this->once()) ->method('processInbound') ->with('content/first-node') ->will($this->returnValue('node/1')); $subpath_processor = new PathProcessor($alias_processor); }
public function testInboundSubPath() { $alias_processor = $this ->getMockBuilder('Drupal\Core\PathProcessor\PathProcessorAlias') ->disableOriginalConstructor() ->getMock(); $alias_processor->expects($this->once()) ->method('processInbound') ->with('content/first-node') ->will($this->returnValue('node/1')); $subpath_processor = new PathProcessor($alias_processor); // Look up a subpath of the 'content/first-node' alias. $processed = $subpath_processor->processInbound( 'content/first-node/a', Request::create('content/first-node/a') ); }
public function testInboundSubPath() { $alias_processor = $this ->getMockBuilder('Drupal\Core\PathProcessor\PathProcessorAlias') ->disableOriginalConstructor() ->getMock(); $alias_processor->expects($this->once()) ->method('processInbound') ->with('content/first-node') ->will($this->returnValue('node/1')); $subpath_processor = new PathProcessor($alias_processor); // Look up a subpath of the 'content/first-node' alias. $processed = $subpath_processor->processInbound( 'content/first-node/a', Request::create('content/first-node/a') ); $this->assertEquals('node/1/a', $processed); }
function somemodule_somehook() { return array('someid' => array()); } class HookDiscoveryTest extends PHPUnit_Framework_TestCase { function testGetDefinitions() { /** Setup goes here **/ $discovery = new HookDiscovery('somehook'); $expected = array( 'someid' => array('module' => 'somemodule') ); $this->assertEquals($expected, $discovery->getDefinitions()); } }
namespace Drupal\Core\Plugin\Discovery; class HookDiscovery implements DiscoveryInterface { public function getDefinitions() { $definitions = array(); $modules = module_implements($this->hook); foreach ($modules as $module) {
PHP Fatal error: Call to undefined function module_implements() in core/lib/Drupal/Core/Plugin/Discovery/HookDiscovery.php on line 48
- $modules = module_implements($this->hook); + $modules = \Drupal::moduleHandler() + ->getImplementations($this->hook);
function testGetDefinitions() { $module_handler = $this ->getMock('Drupal\Core\Extension\ModuleHandler'); $module_handler->expects($this->once()) ->method('getImplementations') ->with('somehook') ->will($this->returnValue(array('somemodule'))); $container = new ContainerBuilder(); $container->set('module_handler', $module_handler); \Drupal::setContainer($container);
OK (1 test, 2 assertions)
namespace Drupal\Core\Plugin\Discovery; class HookDiscovery implements DiscoveryInterface { protected function moduleHandler() { if (!$this->moduleHandler) { $this->moduleHandler = \Drupal::moduleHandler(); } return $this->moduleHandler; } public function getDefinitions() { $definitions = array(); $modules = $this->moduleHandler() ->getImplementations($this->hook); foreach ($modules as $module) {
class TestHookDiscovery extends HookDiscovery { function setTestModuleHandler($module_handler) { $this->moduleHandler = $module_handler; } } class HookDiscoveryTest extends PHPUnit_Framework_TestCase { function testGetDefinitions() { $module_handler = $this ->getMock('Drupal\Core\Extension\ModuleHandler'); $module_handler->expects($this->once()) ->method('getImplementations') ->with('somehook') ->will($this->returnValue(array('somemodule'))); $discovery = new TestHookDiscovery('somehook'); $discovery->setTestModuleHandler($module_handler);
OK (1 test, 2 assertions)
namespace Drupal\Core\Plugin\Discovery; class HookDiscovery implements DiscoveryInterface { function __construct($hook, $module_handler) { $this->hook = $hook; $this->moduleHandler = $module_handler; } public function getDefinitions() { $definitions = array(); $modules = $this->moduleHandler ->getImplementations($this->hook); foreach ($modules as $module) {
function testGetDefinitions() { $module_handler = $this ->getMock('Drupal\Core\Extension\ModuleHandler'); $module_handler->expects($this->once()) ->method('getImplementations') ->with('somehook') ->will($this->returnValue(array('somemodule'))); $discovery = new HookDiscovery('somehook', $module_handler);
OK (1 test, 2 assertions)
But it takes so much work to setup my unit test!
People writing tests for poorly designed classes
Elements are coupled if a change in one forces a change in the otherโฆ An elementโs cohesion is a measure of whether its responsibilities form a meaningful unit.
Freeman, Steve; Pryce, Nat (2009-10-12). Growing Object-Oriented Software, Guided by Tests
class LocalTaskManager extends DefaultPluginManager { function __construct(ControllerResolverInterface $cr, Request $request, RouteProviderInterface $route_provider, ModuleHandlerInterface $module_handler, CacheBackendInterface $cache, LanguageManager $language_manager, AccessManager $access_manager) { $this->controllerResolver = $cr; $this->request = $request; $this->routeProvider = $route_provider; $this->accessManager = $access_manager; $this->alterInfo($module_handler, 'local_tasks'); $this->setCacheBackend($cache, $language_manager,'local_task', array('local_task' => TRUE)); }
class LocalTaskManager extends DefaultPluginManager { function __construct( Request $request, RouteProviderInterface $route_provider, AccessManager $access_manager) { $this->request = $request; $this->routeProvider = $route_provider; $this->accessManager = $access_manager; }
class LocalTaskManager extends DefaultPluginManager { function __construct( ModuleHandlerInterface $module_handler, ) { $this->alterInfo($module_handler, 'local_tasks'); }
// DefaultPluginManager protected function findDefinitions() { $definitions = $this->discovery->getDefinitions(); foreach ($definitions as $plugin_id => &$definition) { $this->processDefinition($definition, $plugin_id); } if ($this->alterHook) { $this->moduleHandler->alter($this->alterHook, $definitions); } return $definitions; }
// DefaultPluginManager protected function findDefinitions() { $definitions = $this->discovery->getDefinitions(); foreach ($definitions as $plugin_id => &$definition) { $this->processDefinition($definition, $plugin_id); } if ($this->event) { $this->events()->dispatch($this->event, new PluginEvent($definitions); } return $definitions; } protected function events() { if (!$this->eventDispatcher) { $this->eventDispatcher = \Drupal::eventDispatcher(); } return $this->eventDispatcher; }
We write our tests before we write the code. Instead of just using testing to verify our work after itโs done, TDD turns testing into a design activity. We use the tests to clarify our ideas about what we want the code to do.
Freeman, Steve; Pryce, Nat (2009-10-12). Growing Object-Oriented Software, Guided by Tests