I have thought about this for a while, and also used similar things before to test protected properties and method on classes. The only other way to really do this is to create a wrapper class that extends the class you want to test OR just rely on the methods being implicitly tested from other methods.

We could add some helper methods for doing this, so we can just test protected 'things'.

Issues such as #1929214: Consolidate methods in EntityListController that are not in EntityListControllerInterface, add them or mark them protected could benefit from this.

So, I was thinking something like this:

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

damiankloip’s picture

Title: Add helper methods to testing protected class methods and properties in DrupalUnitTestBase. » Add methods for testing protected class methods and properties in DrupalUnitTestBase.
damiankloip’s picture

Status: Active » Needs review

Status: Needs review » Needs work
Issue tags: -VDC

The last submitted patch, d8.protected-test-methods.patch, failed testing.

damiankloip’s picture

Status: Needs work » Needs review
Issue tags: +VDC

d8.protected-test-methods.patch queued for re-testing.

damiankloip’s picture

Random, I think..

damiankloip’s picture

FileSize
2.79 KB

I have just changed the assert for a getProtectedProperty, this seems more useful in the longrun, as you wont always want to pass an expected result into that all the time but get the return value?

I have also added an example usage in Drupal\system\Tests\Module\ModuleApiTest::testModuleImplements().

Status: Needs review » Needs work

The last submitted patch, 1929014-3.patch, failed testing.

dawehner’s picture

That's a wonderful patch if used correctly. In general we should try to avoid internal behavior but test the general behavior, but sure there are great use-cases for it.

damiankloip’s picture

Status: Needs work » Needs review
Issue tags: -VDC

#6: 1929014-3.patch queued for re-testing.

Status: Needs review » Needs work
Issue tags: +VDC

The last submitted patch, 1929014-3.patch, failed testing.

damiankloip’s picture

Status: Needs work » Needs review
FileSize
6.36 KB

Here is an implementation in views that I actually want to use this for.

damiankloip’s picture

Title: Add methods for testing protected class methods and properties in DrupalUnitTestBase. » Add methods for testing protected class methods and properties in TestBase.
tim.plunkett’s picture

Title: Add methods for testing protected class methods and properties in TestBase. » Add methods for testing protected class methods and properties in DrupalUnitTestBase.
+++ b/core/modules/simpletest/lib/Drupal/simpletest/TestBase.phpundefined
@@ -13,7 +13,8 @@
-use ReflectionObject;
+use ReflectionProperty;
+
 use Exception;

Our standard is now to not `use` top level classes, just do new \ReflectionProperty inline. Its out of scope to fix these others, so this whole hunk should just go away (its not our fault ReflectionObject is not used)

+++ b/core/modules/simpletest/lib/Drupal/simpletest/TestBase.phpundefined
@@ -1270,4 +1271,37 @@ public static function generatePermutations($parameters) {
+   *   The name of the property to return.
+   */
+  protected function getProtectedProperty($instance, $property) {
+    $reflection = new ReflectionProperty($instance, $property);

Missing @return

+++ b/core/modules/views/lib/Drupal/views/Tests/ViewExecutableTest.phpundefined
@@ -291,23 +292,13 @@ public function testDestroy() {
+    unset($defaults['storage']);

This could use a comment

+++ b/core/modules/views/lib/Drupal/views/ViewExecutable.phpundefined
@@ -9,6 +9,7 @@
+use ReflectionClass;

Same here, just remove this and do \ReflectionClass inline.

+++ b/core/modules/views/lib/Drupal/views/ViewExecutable.phpundefined
@@ -281,6 +282,16 @@ class ViewExecutable {
+  public $area;

Do we want this to be NULL? Otherwise, public $area = array();

+++ b/core/modules/views/lib/Drupal/views/ViewExecutable.phpundefined
@@ -1721,15 +1732,12 @@ public function createDuplicate() {
-        unset($handlers);

Do we really not need this now?

+++ b/core/modules/views/lib/Drupal/views/ViewExecutable.phpundefined
@@ -1737,24 +1745,12 @@ public function destroy() {
-    $keys = array('current_display', 'display_handler', 'displayHandlers', 'field', 'argument', 'filter', 'sort', 'relationship', 'header', 'footer', 'empty', 'query', 'result', 'inited', 'style_plugin', 'plugin_name', 'exposed_data', 'exposed_input', 'many_to_one_tables');
-    foreach ($keys as $key) {
-      unset($this->$key);
-    }
-
-    // These keys are checked by the next init, so instead of unsetting them,
-    // just set the default values.
-    $keys = array('items_per_page', 'offset', 'current_page');
-    foreach ($keys as $key) {
-      if (isset($this->$key)) {
-        $this->$key = NULL;
-      }
+    $reflection = new ReflectionClass($this);
+    $defaults = $reflection->getDefaultProperties();
+    unset($defaults['storage']);
+    foreach ($defaults as $property => $default) {
+      $this->{$property} = $default;
     }
-
-    $this->built = $this->executed = FALSE;
-    $this->build_info = array();
-    $this->attachment_before = '';
-    $this->attachment_after = '';

YES. Thank you. This is great.

damiankloip’s picture

FileSize
3.61 KB
6.11 KB

Nice, thanks for the review!

Do we want this to be NULL? Otherwise, public $area = array();

Well at the moment all the other ones are NULL, So I vote keeping them the same for now. If we want to change these defaults, we should change all the plugin properties at once.

Do we really not need this now?

I think we can actually change this a bit more (see new patch), where we just destroy each handler, then the property will be taken care of by the reflection code below too.

damiankloip’s picture

FileSize
6.1 KB

Without the rogue line in the use statements in TestBase.

damiankloip’s picture

FileSize
5.88 KB

AHhhh, sorry. ReflectionMethod was there before too.

damiankloip’s picture

Title: Add methods for testing protected class methods and properties in DrupalUnitTestBase. » Add methods for testing protected class methods and properties in TestBase.
tim.plunkett’s picture

Status: Needs review » Reviewed & tested by the community

This is a thing of beauty. Ship it!

damiankloip’s picture

Awesome!

dawehner’s picture

+++ b/core/modules/views/lib/Drupal/views/ViewExecutable.phpundefined
@@ -281,6 +281,16 @@ class ViewExecutable {
+   * Stores the area handlers which are initialized on this view.
...
+   * An array containing Drupal\views\Plugin\views\area\AreaPluginBase
+   * objects.
...
+   * @var array
...
+  public $area;

It doesn't make sense, as we either store the header/footer/empty text, but not nothing?

+++ b/core/modules/views/lib/Drupal/views/ViewExecutable.phpundefined
@@ -1721,15 +1731,11 @@ public function createDuplicate() {
-        unset($handlers);

Thought we should still unset $this->{$type} totally.

+++ b/core/modules/views/lib/Drupal/views/ViewExecutable.phpundefined
@@ -1737,24 +1743,12 @@ public function destroy() {
+    $reflection = new \ReflectionClass($this);
+    $defaults = $reflection->getDefaultProperties();
+    unset($defaults['storage']);
+    foreach ($defaults as $property => $default) {
+      $this->{$property} = $default;

I can see the point in using it here, but I'm wondering whether reflection is good on actual runtime, as it might slows it down? I'm not sure how much, but that should be at least considered.

damiankloip’s picture

It doesn't make sense, as we either store the header/footer/empty text, but not nothing?

We went with this for now, to stay consistent with all the other plugin properties we have. If we want to change the defaults, maybe we should do it all in one go?

Thought we should still unset $this->{$type} totally.

Destroying all the handlers for each type and resetting the property seems enough? Not sure now though.

I'm not sure we will see much overhead here, especially as we are not iterating over ReflectionProperty methods or anything for each property. Maybe, there is? Should we test this out?

damiankloip’s picture

Status: Reviewed & tested by the community » Needs review
FileSize
2.26 KB
6.98 KB

OK, me and Daniel spoke about this on IRC, let's remove the $area property on ViewExecutable and add some assertions that use the invokeProtectedMethod() method.

dawehner’s picture

Status: Needs review » Reviewed & tested by the community

Thanks for cleaning it up.

The only thing which is missing now, is a review by crell or similar level.

+++ b/core/modules/views/lib/Drupal/views/ViewExecutable.phpundefined
@@ -1728,24 +1724,12 @@ public function destroy() {
+    $reflection = new \ReflectionClass($this);
+    $defaults = $reflection->getDefaultProperties();
+    unset($defaults['storage']);
+    foreach ($defaults as $property => $default) {
+      $this->{$property} = $default;

This really is a nice refactoring and really adds maintainability of the code.

chx’s picture

Assigned: Unassigned » Crell
Status: Reviewed & tested by the community » Needs review
chx’s picture

Mind you, I love this but that's a given. We need Crell to bless this... trick.

Berdir’s picture

This looks good to me but I think it shouldn't be overused. There are some cases where it can be useful, but generally, tests should only test the public API of a class as the implementation is it's own business.

But, as in this case with ViewsDataCache, it's quite important that the inner implementation is correct as bugs could lead to rebuilds of the views data cache on every page load. The interesting thing is that it actually was broken, see #1941164: hook_views_data() rebuilt on every page view ? and this test would not have caught that bug as it makes the same "mistake" as I did and calls destruct() more or less directly.

The test coverage that I added to the path alias whitelist, which was broken too, simply does a direct cache()->get() call in a web test, to be sure that it works in a real environment, see PathAliasTest:testPathCache(). If we test the inner workings of a class, we assume that we know what it does anyway.

In another example, in the tests that I've written for state caching, I'm injecting a memory cache backend which is essentially a mock and allows to me check that the internal implementation does work as expected as I can verify when it's called and how and also influence what it returns.

So, all I'm trying to say is that I'm wondering if we should add a sentence or two to the docblock and explain why/when you should use this and when not.

damiankloip’s picture

FileSize
7.23 KB

Thanks berdir, Let's add a comment to that affect too.

dawehner’s picture

Is there a reason why you don't added this comment to the property one as well?

Crell’s picture

I cannot speak to the changes in Views itself; those seem reasonable, I think. However, the introspective testing going on here makes me do a spit take. Most of Berdir's trepidation in #26 I share, but would go a step farther. The docblock on it doesn't go far enough in stating that "if you need this, it means your code is still not clean enough." (Which I believe to be the case.) Using reflection for this sort of test is, I'd say, better than having public methods or properties solely for testing purposes, but not by much. :-)

+++ b/core/modules/views/lib/Drupal/views/Tests/ViewsDataTest.php
@@ -47,12 +47,21 @@ public function testViewsFetchData() {
+    // Test that this table info now exists in the cache.
+    $this->viewsDataCache->destruct();
+    $cache = $this->invokeProtectedMethod($this->viewsDataCache, 'cacheGet', array("views_data:$table_name"));
+    $this->assertIdentical($cache->data, $data, 'Table data has been retrieved from the cache.');

This is why you inject dependent objects. You can mock them.

Is there no way we can avoid this? Because this is a hack, no question. If we have to go this route the documentation should be much clearer about "if you need to use this, you're already doing it wrong". I'd still like to see a better explanation of why we can't refactor the code to not need this quirk in the first place.

dawehner’s picture

At least here it doesn't seem to be an issue to inject the object and test directly on the injected object.

damiankloip’s picture

Yes, fully unit testing everything is great, and we can mock the cache back end. Then we surely aren't actually testing the implementation and container configuration that we are shipping with? We aren't delivering a set of components like symfony, so we need to test the actual real implementations as well.

Crell’s picture

Sure, functional tests that test integrated functionality are fine. But if you're asking "was the cache updated" by introspecting an object, then you're not doing a unit test OR a functional test. You're doing a hacked-up test.

damiankloip’s picture

No, we're not asking if the cache was updated, I'm asking 'Does my cacheGet method actually return this data from the cache' from that we can determine that the views data is not rebuilt all the time. That seems harder to test functionally. We can test this my mocking the cache object and seeing what keys we have from set and get calls, this still wouldn't test the integrated functionality though.

btw, do we have such a mock cache backend at the moment? If not, I'm happy to make one of those :)

dawehner’s picture

Yes there is a mock implementation: Drupal\Core\Cache\MemoryBackend

damiankloip’s picture

So maybe we could check the count of when hook_views_data is invoked to do this? a la #1942660: Add a method to allow modules to clear the \Drupal\views\ViewsDataCache->storage during run-time (once this gets in). So then it makes sense to just move that test into #1944674: Improve performance of ViewsDataCache instead.

But then, if we remove the invokeProtectedMethod helper, how do you propose we test the views destroy() test, as it does make alot of sense to test that all of the properties have been reset. I'm not aware of another way to do this.

dawehner’s picture

FileSize
7.24 KB

Let's improve the tests in one of the other issues, and just get this small piece in.

My personal oppinion is that proper mocked tests is the way to really know whether the code works, though
we also need tests for configuration. One possibility would be to call the same test code, once for a manual created instance, and once for the one in the container.

damiankloip’s picture

Crell, thoughts on #33 to #36?

Crell’s picture

interdiff, please?

I don't really understand what dawehner is suggesting in #36. With a proper mocking framework (which simpletest lacks, but PHPUnit has!), you can setup a mock object on-the fly, configure it with "I expect this method to get called once", then pass it in to the object being tested. That way you can verify that the cache was in fact called.

dawehner’s picture

@crell Sorry this was just a rerole, no actual change at all.

damiankloip’s picture

Well then that is now not to do with this issue, that can be incorporated into the work I've started in #1944674: Improve performance of ViewsDataCache - we still need to test this with the configuration that is shipped too, so this does make alot more work but that's life I guess :)

So now we aren't allowed this at all?! I would still at least like the getProtectedProperty method, so we can test the ViewExecutable::destroy method properly. Should I just move that into the actual test class that's using it instead?

Crell’s picture

I can't say that it's "not allowed". I don't have that kind of karma. :-) I am saying, though, that "if you can't test it without reflecting, you're probably doing it wrong in the first place". I don't know enough about this specific code to say exactly what "right" is, but generally "inject it, and then mock it" is the better approach.

damiankloip’s picture

but generally "inject it, and then mock it" is the better approach

Yep, I understand that is the better approach, but it can't be made to suit all scenarios, surely. Do you see what I meant in #40 with the destroy() method? So, destroy() essentially resets the ViewExecutable class, and all the properties on it. I don't understand how anything could be injected or mocked that would help with this? And I can't see another way to test that all of the class properties have been reset to what they started as?

if you can't test it without reflecting, you're probably doing it wrong in the first place

I'm not sure reflection is 'wrong' in this case, this seems like a great use case for it. If you can let me know a better way to test that I'm happy to concede :)

damiankloip’s picture

Title: Add methods for testing protected class methods and properties in TestBase. » Use reflection for ViewExectuable::destroy()
Component: simpletest.module » views.module
Assigned: Crell » Unassigned
FileSize
3.15 KB
4.42 KB

Ok, let's just change this issue to be views specific based on the code changes to ViewExecutable::destroy(). I have remove the protected method method completely, and move the getProtectedProperty method into ViewExecutableTest, as that's where I need it for now.

dawehner’s picture

Status: Needs review » Reviewed & tested by the community

That's a great cleanup so for, though for sure, we have to continue working to improve the ViewExecutable class in the future.

chx’s picture

Status: Reviewed & tested by the community » Needs review
Issue tags: +needs profiling
dawehner’s picture

Did some testing as well: https://gist.github.com/dawehner/5343845

Before:

0.000350 stddev: 8.5e-05

After:

0.000397 0.0001020

Considering that you don't have that many calls to destroy() this additional cost seems okay

damiankloip’s picture

That seems ok to me, as we need this more dynamic way to 'reset' a view. Currently destory() unsets everything so reusing a destroyed view is not the same as a new instance, when it should be.

chx?

chx’s picture

Status: Needs review » Reviewed & tested by the community

That's not as horrific slow as I was afraid of.

damiankloip’s picture

Issue tags: -needs profiling
FileSize
654 bytes
4.51 KB

just adding a small doc, as per @alexpott's request.

alexpott’s picture

Title: Use reflection for ViewExectuable::destroy() » Change notification: Use reflection for ViewExectuable::destroy()
Project: Drupal core » Views (for Drupal 7)
Version: 8.x-dev » 8.x-3.x-dev
Component: views.module » Code
Status: Reviewed & tested by the community » Active
Issue tags: +Needs change record

Committed 8b386d9 and pushed to 8.x. Thanks!

damiankloip’s picture

Project: Views (for Drupal 7) » Drupal core
Version: 8.x-3.x-dev » 8.x-dev
Component: Code » views.module
Status: Active » Fixed
Issue tags: -Needs change record

I don't think we need a notice here, the usage is unchanged.

damiankloip’s picture

Title: Change notification: Use reflection for ViewExectuable::destroy() » Use reflection for ViewExecutable::destroy()

Automatically closed -- issue fixed for 2 weeks with no activity.

Anonymous’s picture

Issue summary: View changes

Updated issue summary.