Problem/Motivation
In #2881788: Process-isolate Drupal\Tests\Core\Extension\ModuleHandlerTest we had to do some fancy footwork in order to fix a test of ModuleHandler.
That's because Drupal\Core\Extension\Extension has a dependency on DRUPAL_ROOT, which makes it difficult to write unit tests in some circumstances.
Specifically, when the Extension object is unserialized, it will try to find the Drupal application root by using the DRUPAL_ROOT constant. This is a problem when you're writing a unit test which is process-isolated, because DRUPAL_ROOT is (rightly) not defined for unit tests.
Proposed resolution
The goal of this issue is to change Drupal\Core\Extension\Extension::unserialize() so that it checks for the availability of \Drupal::root() and uses that service if available, falling back on DRUPAL_ROOT if it is not.
There should also be a KernelTestBase test which spoofs the app.root service to prove that the service is used in preference over the constant when unserialized.
Remaining tasks
User interface changes
API changes
Data model changes
| Comment | File | Size | Author |
|---|---|---|---|
| #28 | interdiff-26-28.txt | 1.23 KB | foxtrotcharlie |
| #28 | remove-extension-dependency-on-drupal-root-2881981-28.patch | 2.53 KB | foxtrotcharlie |
| #27 | unittest.png | 93.78 KB | vakulrai |
| #26 | 2881981-26.patch | 2.4 KB | Vidushi Mehta |
| #24 | remove-extension-dependency-on-drupal-root-2881981-24.patch | 2.32 KB | foxtrotcharlie |
Comments
Comment #2
mile23Comment #3
dawehnerWe used to put the root into the serialized extension itself, but this causes issues with moving files, see #2558247: Remove rebuildAll() and module install from UpdatePathTestBase. I guess calling to
\Drupal::root()would fix the issue?Comment #4
foxtrotcharlie commentedThis patch checks for the Drupal root using \Drupal::root() if available or DRUPAL_ROOT if not. Wasn't sure if I need the check or could have just used \Drupal::root() so let me know if unnecessary. I haven't created a test as I've never done one before. Happy to give it a shot if someone points me in the right direction.
Comment #6
mile23So in some contexts \Drupal::root() service won't be available. So you'd check \Drupal::hasService('app.root'), and if it's there, use it, otherwise use the constant.
Comment #7
foxtrotcharlie commentedThanks! New patch checks for the service first, and if there, uses it, and if not uses the constant.
Comment #8
mile23...plus the test? :-)
Comment #9
foxtrotcharlie commentedThanks for your help so far @Mile23. Never written a Kernel test (in fact, any test) so gonna do some reading now and will come back when I have questions :-)
Comment #10
foxtrotcharlie commentedSo these are the steps that I think are required: Create an extension object, serialize it, then use the unserialize method on it to test that the service container app.root is used instead of the DRUPAL_ROOT constant.
I’ve been reading up on PhpUnit and have a broad understanding of setUp() and mocks and am wondering if I should (and if it’s possible) create a mock of an extension object, and a mock of a container and then set up a fake app.root for the container so that the call to \Drupal::root() gets the fake root. And the test then checks that this is used instead of the constant.
I've seen the following approach to mocking a service and creating a dummy container:
And am wondering if I should do something similar. So some questions:
1) Is this the right way to set up the container?
2) How should I set up the extension object (or mock object) that we’re going to use the unserialize method on.
3) Do you know of any existing tests that do something similar, which I could look at?
Thanks :-)
Comment #11
mile23So you'll want to write a Kernel test for the following reasons:
It has a mostly-booted Drupal kernel, which means
DRUPAL_ROOTwill be defined.It already has a container set up, which you can access from the test through
$this->container.This means you can say
$this->container->set('app.root', 'some_test_value').So then you'd make an
Extensionobject (don't mock it), serialize, and unserialize. From there you can check the value of root.Comment #12
foxtrotcharlie commentedThanks for you help @Mile23 - you are awesome :-)
So here's the patch with the test. I ran locally and it passes. Some things I wasn't' sure of but thought I'd just get something up here so you could check the actual patch, questions below:
1) Is there a more appropriate way to create the extension object?
2) Is the way I serialize and unserialize correct?
3) I couldn't access the root property of the extension object as it's protected, so I created a ReflectionProperty. Let me know if there's a better way.
4) I wasn't sure whether the dummy app root should be created in setUp() or just in the test itself. setUp() seemed more appropriate because that's where we're setting up the test ;-) but it could have easily been created as a variable in the test function like so:
5) Do I need a @covers e.g. @covers ::unserialize or something like that in the test docblock comment?
Thanks!
Comment #13
heddnCan you extend an existing Extension test? Maybe ModuleHandlerTest?
If you extend ModuleHandlerTest, then it makes sense to do all this setup in the test itself.
phpunit's readAttribute does the same thing. Search for other examples in core or look at CckFileTest.
Setting to NW for the readAttribute changes. The rest of my comments are more nits than anything.
Comment #14
foxtrotcharlie commentedNew patch which uses phpunit's readAttribute instead of the ReflectionProperty, at @heddn's suggestion. Left the test as a Kernel test after discussion in irc.
Comment #16
jibranLooks fine to me.
Comment #17
larowlanthis should use $this->assertEquals, not self::
PHP support for calling instance methods statically is to be phased out in a future version, lets avoid adding it
Comment #18
larowlanFor some background https://thephp.cc/news/2017/07/dont-call-instance-methods-statically
Comment #19
foxtrotcharlie commentedThanks @larowlan. Changed to use $this->assertEquals instead of self::assertEquals.
Comment #21
mile23Restarting the test for 8.6.x and setting RTBC.
Looks good to me.
Comment #22
alexpottDRUPAL_ROOTis not defined. The container is mockable in unit tests.This is a constant -
'/dummy/app/root'- it's best just to hard code this. I makes for less indirection in tests and therefore quicker understanding of what is actually being tested.All of this could be in the actual test method - no? And then there wouldn't be a need for all the class properties.
Comment #24
foxtrotcharlie commentedThanks @alexpott. I have recreated the patch so that it's a unit test and simplified as suggested. I moved everything into the test and removed the setup(). I hope this is in order?
Comment #25
mile23Thanks. A couple more things...
Need to set @coversDefaultClass, otherwise we're marking this as covering \unserialize().
Add a line like
$this->assertFalse(defined('DRUPAL_ROOT'), 'Constant DRUPAL_ROOT is defined.');since that's the assumption of our test.Use $this->assertEquals().
Comment #26
Vidushi Mehta commentedAdded a patch with the changes as mentioned by #25.
Comment #27
vakulrai commented@Vidushi Mehta i have tested the path. The patch has passed the Php unit tests. Adding unit test screenshots.
Comment #28
foxtrotcharlie commentedThanks Vidushi. I believe that @coversDefaultClass needs to be set on the class, so I have updated and created a new patch. Otherwise, I think your changes address all the issues picked up by @Mile23. I've also created an interdiff so you can see what I changed.
Comment #29
mile23@vakulrai, thanks for the review. No need to attach screenshots, since we have a testbot result in #26.
#28 LGTM, and addresses #25.
Comment #30
mile23Oops, accidentally changed IS.
Comment #31
Vidushi Mehta commented@foxtrotcharlie Thanks for making my mistake correct :)
Comment #33
alexpottHow about going a bit further than this an scheduling the root property for removal and asking things to provide it to the ->load() method. It doesn't really belong here. Serializing DRUAPL_ROOT makes this information non-portable so if it's put in a cache there's a problem. So in Drupal 8 we add an optional argument to ->load() and use it if provided. But let's do this in a separate issue as at least this allows us to make forward steps.
Changing the title since we're not removing the dependency we really only mitigating it and to do that we're using the service locator anti-pattern.
Adding issue credit for reviews too.
Comment #34
alexpottCommitted b097188 and pushed to 8.7.x. Thanks!
Fixed indentation and comment flow on commit.
Fixed file mode on commit.