Feed aggregator

Dcycle: Can the exact same module code run on Drupal 7 and 8?

Planet Drupal -

As the maintainer of Realistic Dummy Content, having procrastinated long and hard before releasing a Drupal 8 version, I decided to leave my (admittedly inelegant) logic intact and abstract away the Drupal 7 code, with the goal of plugging in Drupal 7 or 8 code at runtime.

Example original Drupal 7 code // Some logic. $updated_file = file_save($drupal_file); // More logic. Example updated code

Here is a simplified example of how the updated code might look:

// Some logic. $updated_file = Framework::instance()->fileSave($drupal_file); // More logic. abstract class Framework { static function instance() { if (!$this->instance) { if (defined('VERSION')) { $this->instance = new Drupal7(); } else { $this->instance = new Drupal8(); } } return $this->instance; } abstract function fileSave($drupal_file); } class Drupal8 extends Framework { public function fileSave($drupal_file) { $drupal_file->save(); return $drupal_file; } } class Drupal7 extends Framework { public function fileSave($drupal_file) { return file_save($drupal_file); } }

Once I have defined fileSave(), I can simply replace every instance of file_save() in my legacy code with Framework::instance()->fileSave().

In theory, I can then identify all Drupal 7 code my module and abstract it away.

Automated testing

As long as I surgically replace Drupal 7 code such as file_save() with “universal” code such Framework::instance()->fileSave(), without doing anything else, without giving in the impulse of “improving” the code, I can theoretically only test Framework::instance()->fileSave() itself on Drupal 7 and Drupal 8, and as long as both versions are the same, my underlying code should work. My approach to automated tests is: if it works and you’re not changing it, there is no need to test it.

Still, I want to make sure my framework-specific code works as expected. To set up my testing environment, I have used Docker-compose to set up three containers: Drupal 7, Drupal 8; and MySQL. I then have a script which builds the sites, installs my module on each, then run a selftest() function which can test the abstracted function such as fileSave() and make sure they work.

This can then be run on a continuous integration platform such as Circle CI which generates a cool badge:

Extending to Backdrop

Once your module is structured in this way, it is relatively easy to add new related frameworks, and I’m much more comfortable releasing a Drupal 9 update in 2021 (or whenever it’s ready).

I have included experimental Backdrop code in Realistic Dummy Content to prove the point. Backdrop is a fork of Drupal 7.

abstract class Framework { static function instance() { if (!$this->instance) { if (defined('BACKDROP_BOOTSTRAP_SESSION')) { $this->instance = new Backdrop(); } elseif (defined('VERSION')) { $this->instance = new Drupal7(); } else { $this->instance = new Drupal8(); } } return $this->instance; } } // Most of Backdrop's API is identical to D7, so we can only override // what differs, such as fileSave(). class Backdrop extends Drupal7 { public function fileSave($drupal_file) { file_save($drupal_file); // Unlike Drupal 7, Backdrop returns a result code, not the file itself, // in file_save(). We are expecting the file object. return $drupal_file; } } Disadvantages of this approach

Having just released Realisic Dummy Content 7.x-2.0-beta1 and 8.x-2.0-beta1 (which are identical), I can safely say that this approach was a lot more time-consuming than I initially thought.

Drupal 7 class autoloading is incompatible with Drupal 8 autoloading. In Drupal 7, classes cannot (to my knowledge) use namespaces, and must be added to the .info file, like this:

files[] = includes/MyClass.php

Once that is done, you can define MyClass in includes/MyClass.php, then use MyClass anywhere you want in your code.

Drupal 8 uses PSR-4 autoloading with namespaces, so I decided to create my own autoloader to use the same system in Drupal 7, something like:

spl_autoload_register(function ($class_name) { if (defined('VERSION')) { // We are in Drupal 7. $parts = explode('\\', $class_name); // Remove "Drupal" from the beginning of the class name. array_shift($parts); $module = array_shift($parts); $path = 'src/' . implode('/', $parts); if ($module == 'MY_MODULE_NAME') { module_load_include('php', $module, $path); } } });

Hooks have different signatures in Drupal 7 and 8; in my case I was lucky and the only hook I need for Drupal 7 and 8 is hook_entity_presave() which has a similar signature and can be abstracted.

Deeply-nested associative arrays are a staple of Drupal 7, so a lot of legacy code expects this type of data. Shoehorning Drupal 8 to output something like Drupal 7’s field_info_fields(), for example, was a painful experience:

public function fieldInfoFields() { $return = array(); $field_map = \Drupal::entityManager()->getFieldMap(); foreach ($field_map as $entity_type => $fields) { foreach ($fields as $field => $field_info) { $return[$field]['entity_types'][$entity_type] = $entity_type; $return[$field]['field_name'] = $field; $return[$field]['type'] = $field_info['type']; $return[$field]['bundles'][$entity_type] = $field_info['bundles']; } } return $return; }

Finally, making Drupal 8 work like Drupal 7 makes it hard to use Drupal 8’s advanced features such as Plugins. However, once your module is “universal”, adding Drupal 8-specific functionality might be an option.

Using this approach for website upgrades

This approach might remove a lot of the risk associated with complex site upgrades. Let’s say I have a Drupal 7 site with a few custom modules: each module can be made “universal” in this way. If automated tests are added for all subsequent development, migrating the functionality to Drupal 8 might be less painful.

A fun proof of concept, or real value?

I’ve been toying with this approach for some time, and had a good time (yes, that’s my definition of a good time!) implementing it, but it’s not for everyone or every project. If your usecase includes preserving legacy functionality without leveraging Drupal 8’s modern features, while reducing risk, it can have value though. The jury is still out on whether maintaining a single universal branch will really be more efficient than maintaining two separate branches for Realistic Dummy Content, and whether the approach can reduce risk during site upgrades of legacy custom code, which I plan to try on my next upgrade project.

As the maintainer of Realistic Dummy Content, having procrastinated long and hard before releasing a Drupal 8 version, I decided to leave my (admittedly inelegant) logic intact and abstract away the Drupal 7 code, with the goal of plugging in Drupal 7 or 8 code at runtime.

CU Boulder - Webcentral: Adding Simpletests To A Drupal 7 Module

Planet Drupal -

I recently inherited a module that didn't have any testing structure setup. I don't boast 100% test coverage on any of my projects and traditionally have done a poor job adding tests, but I thought I could do better with this project. While in Drupal 8 the focus is more on unit tests, in Drupal 7 Simpletests are the standard. 

The Simpletest module was born out the Simpletest PHP library. When I went to the Simpletest website you see first in Google search results, the latest release is from 2012, and so I thought the project might be dead. However, the project now lives on Github and has development activity as recent as December 2016. 

The Simpletest module was created back in the days of Drupal 6. Drupal didn't have a huge emphasis on testing back then and so part of the initial module included tests that could be run against Drupal core. As development on the Simpletest module evolved, it was moved into Drupal 7 core development and now you can see it as one of the modules included in a stock Drupal 7 install.

It can be confusing to see a core "Testing" module and then a contrib "Simpletest" module coexist, but for Drupal 7, you only need to use the core module. To follow along with this post, you'll need to turn on the Testing module in order to see and run your tests. 

Testing Info Setup

To begin setting up your module to run Simpletests, you first have to create a "your_module.test" file. It is best practice to place this file in a tests folder. You will need to add the location of this file to your module's info file in order for Drupal to find it. 

name = "Google CSE" description = "Use Google Custom Search to search your site and/or any other sites." core = 7.x files[] = tests/google_cse.test

The second part of getting Drupal to recognize your tests is the aforementioned test file. In it, you will create a class that extends the "DrupalWebTestCase" class and provides some basic required functions.

class GoogleCSETestCase extends DrupalWebTestCase { /** * Tell Drupal about the module's tests. * * @return array * An array of information to display on Testing UI page. */ public static function getInfo() { // Note: getInfo() strings are not translated with t(). return array( 'name' => 'Google CSE Tests', 'description' => 'Ensure that the Google Custom Search Engine is integrated with Search API.', 'group' => 'Search', ); }

Once you have created that file and cleared the Drupal cache, then you should see your tests in their group on "admin/config/development/testing". If you don't know what group to put your tests in you can make your own; however, if you leave the group key out, your tests will appear at the top of the testing page in a blank group.

First Test Setup

Now that you can run your tests through the Drupal UI, you need to actually write a test to run. I'm not doing the whole red, green, refactor mantra since I know so little about testing, in general, and adding that level of strictness on top would just get in the way. Plus, since I'll be fixing a bug, the test will be red at first anyway even though the code is already written for the functionality. 

/** * Perform steps to setup Drupal installation for test runs. */ public function setUp() { // Load the dependencies for the Google CSE module. $modules = array( 'google_cse', 'search', 'block', ); parent::setUp($modules);   // Eventually move more setup functions to separate testing module. // module_enable(array(google_cse_testing)); }

Every class extending "DrupalWebTestCase" has to implement a setup function. Typically, you'll pass a few modules back to the parent class that are dependencies for your module. I knew that I would need the Search module enabled as a dependency, but you also have to enable any modules not enabled by the testing profile. I had never installed a site using that profile so it was a bit tricky knowing what I needed to enable until I looked at a vanilla site installed using the testing profile. It is a rather bare bones environment and more modules were turned off than I expected. For instance, you don't even get the Toolbar module enabled and have no content types pre-installed to work with. So, if you're scratching your head while writing a test, do yourself a favor and actually walk through your steps as the test runner would follow them; don't assume the standard profile is the same environment.

If you have anything else you'd like to do in the setup function, make sure you do it after passing modules back to the parent function. In the parent function, the actual installation of Drupal happens so any variable setting you do will get overwritten. Eventually, I will write a module specifically for testing that will handle any additional setup tasks, but for right now, I just want to get a test up and running. 

Testing a Configuration Change

For my first test, I had a display bug related to a specific set of configuration a user might enter. To test a fix for this, I wanted to change the configuration as a user would through the UI and then assert that the HTML output changed as expected. 

/** * Tests all SiteSearch configuration options. */ public function testSiteSearchSettingsConfig() { // Need to have user for this test. $this->setupGoogleCSEUser(); // Need to have Google CSE as default search. $this->setupGoogleCSEAsDefaultSearch(); // Need to setup page with Google CSE search block until redirect issue is fixed. // @todo Figure out why going to /search/google ended up in a redirect loop. $this->setupGoogleCSESearchPage();

I began my test with some setup tasks. These tasks could be performed in the setup function, but I wanted to abstract them and put them in individual tests since I might not need to perform these setup tasks for every test I write. 

You'll see that I made a to-do related to a redirect loop I was seeing. While running Simpletests, you will see feedback from assertions and other functions you use to prove your test is functioning as you expect. I kept seeing a 302 response while trying to browse to a path that always returned a 200 when I tried to go to it manually. Debugging that issue was maddening as I had little information to go on. 


From that test report, I can see the failed assertions, and I also get a nice verbose message to inspect relating to the test failures. You can choose whether or not to include verbose logging in your test runs, but for developing tests I have no reason why you would turn that setting off. 


...and that's what I get from the verbose message. Great. A redirect loop can't really return much more than a blank page, and you'll notice the test report mentioned the number of bytes loaded for each page request, which turned out to be zero bytes for this particular request. I tried to replicate the redirect loop while browsing a site installed with the testing profile, but I couldn't reproduce the redirect loop. I was losing my mind trying to figure this out so I cheated and set up the page I was trying to test a different way. 

The Assertions

My test included simulating how a user would navigate through a site and change configurations. I used two assertion functions to test that routine, a function to get a response, and a function to post to an endpoint. 

// Post to config form to save SiteSearch settings. $edit = array(); $edit['google_cse_sitesearch'] = "example.com/user User Search \n example.com/node Node Search"; $edit['google_cse_sitesearch_form'] = 'radios'; $this->drupalPost('admin/config/search/settings', $edit, t('Save configuration')); // Go to Google CSE search page. $this->drupalGet('node/1'); // Assert that all SiteSearch options are there. $this->assertText('Search the web', "Default SiteSearch radio button found."); $this->assertText('User Search', "First SiteSearch radio button found."); $this->assertText('Node Search', "Second SiteSearch radio button found."); // Post different config options for more checks. $edit = array(); $edit['google_cse_sitesearch_form'] = 'select'; $edit['google_cse_sitesearch_option'] = 'Search Your Site'; $edit['google_cse_sitesearch_default'] = 1; $edit['google_cse_sitesearch'] = "example.com/user \n example.com/node"; $this->drupalPost('admin/config/search/settings', $edit, t('Save configuration')); // Go to Google CSE search page. $this->drupalGet('node/1'); // Assert select options have changed. // Need to use raw option since the select list options are not visible. $this->assertRaw('<option value="">Search Your Site</option>', "Default SiteSearch select option found."); $this->assertRaw('<option value="example.com/user" selected="selected">Search &quot;example.com/user&quot;</option>', "First SiteSearch select option found and selected."); $this->assertRaw('<option value="example.com/node">Search &quot;example.com/node&quot;</option>', "Second SiteSearch select option found.");

As you can see above, I was simulating a change to add radio buttons or a select list to a search form and make sure that change was reflected in the rendering of the search block. To post to a form, you have to tell "drupalPost()" the path to the form, the form input values, and what to look for as far as a submission button. 

The form input keys you will need correspond to the values you will see in $form_state['values']. As for what to post into those keys as values, I ended up looking at what was serialized in the variable table. 

Once I saved the configuration values, I needed to make sure the change was reflected on a page with a search block. It is best to use "assertText()" while looking for changes to a page that should be visible. That function worked fine for radio buttons since the options are all displayed on the page; however, for a select list, I didn't know how to test the options that weren't visible so I used "assertRaw()" to test the hidden select options. 


After a little fiddling around and trial and error with my first Simpletest setup, I got to enjoy an all green test run and submit a patch with greater confidence that it actually did what it was supposed to do. Furthermore, any additional patch added to the module will test whether or not that code change breaks the configuration test I wrote and allow the maintainers to spot problems before they merge in any code. 

Adding Testing To A Contrib Module

But what if the module you're contributing to doesn't have automated testing set up? Well, if you don't maintain the module, then your out of luck. A maintainer has to enable automated testing and choose when those tests are run. Since I just took over maintainership of the module I wrote this test for, I got a crash course into how that process is done.

You'll first need to enable automated testing for each branch contained in your module. Under the "Automated Testing" tab, you'll see that you can add different testing profiles per branch. This makes sense as different versions of Drupal can require different versions of PHP and whatever database you are using, MYSQL vs. PostgreSQL vs. some other database backend.


You can also choose which branch of Drupal you'd like to test against. Don't fret over what to pick here as you can add multiple test runs for each branch. This feature comes in handy with Drupal 8 since you can test your code on the minor release that is in development as well as the stable current minor release.

As for when to run the tests, I always choose "Run on commit and for issues" since that gives me the option of not allowing patches to be merged in without running the test suite on them first. Once you add some testing, there will be a select list next to the patch upload field that gives you options on what test profile you want to test against. You can also choose not to test it against a testing profile, but I'm not sure exactly why you would want to do that if you've actually bothered to write tests and set up automated testing. 

One final note is to make sure that the issue you've uploaded a patch to has the right branch selected in the issue description. If you've tagged it for a release, say 7.x-2.4, rather than a development branch, say 7.x-2.x, then the test bot can get confused as to what branch of your code to checkout via Composer. I got burned by this at first and had to do some investigating to figure that out.

An additional area to check is the release node for the dev branch you are working on. Somehow that node didn't have a "Git branch" listed on my release even though that field is required. Huh? The node was made before I took over the module so I'm not sure how that happened, but if there is no Git branch listed, then the test bot can't check out the branch you want to test.

Hopefully, if you've never used Simpletest before reading this blog post, you now feel empowered and knowledgeable enough to start writing your own tests for custom and contrib modules. Happy testing, y'all!

Developer Blog

OSTraining: How to Use Pathauto in Drupal 8

Planet Drupal -

Many modules have been in flux during the early stages of Drupal 8's development.

Few modules have changed as much as Pathauto, which the vast majority of Drupal sites use to control their URLs.

In this tutorial, I'll show you the current way to use Pathauto with your Drupal 8 site.

Cheeky Monkey Media: Serious Accolades Prove that We Do More Than 'Monkey' Around

Planet Drupal -

Serious Accolades Prove that We Do More Than 'Monkey' Around rohan Mon, 02/27/2017 - 17:34

At Cheeky Monkey, one of our goals is to have fun at work. After all, it's been said that the average worker spends 5 hours and 41 minutes per day at their desk, so why not make the most of it? That being said, we take our work very seriously and it shows. Cheeky Monkey was recently named not only as a top web design company in Canada, but also as one of the best web developers from leading technology research firm Clutch. Yeah, we’re pretty excited about that.

Pages

Subscribe to Cruiskeen Consulting LLC aggregator