I remember reading How to level up as a
developer
and thinking that I should try and write a few different types of
programs to challenge myself. I’d been curious about Rack Middleware so
I made it a resolution for 2013 to code up an idea I’d had for a while.
It’s a common scenario that website owners to commission a site, then
run an SEO campaign 6 months after the site has launched when they realise they
have no traffic. This often leads to a request
from the SEO people is to sort out unique and relevant meta tags for
each page, matching the text of the h1 element and so on. If this
wasn’t considered from the start it can be hard to drop in on some
sites, particulary where there static/hard coded pages involved.
…Enter RackSeo
It’s a plain rack middleware which should work with any
Rack based framework or app (Rails, Sinatra, Padrino, Merb etc.).
It processes the final HTML response, sets up the meta tags and
populates them with sensible content based on the text in the page
(using the summarize gem). You
can specify a format for the title tag based on css selectors (eg pull
the h1 text and append the name of the company) and it will work
dynamically on every page withoutchanginganycode.
It’s configurable using a YAML file and can be set for different paths
including wildcards.
You can read all about it over on the Github
readme. I’m all ears
on how to take it forward to being a useful tool in the future.
Whilst I don’t do so much PHP development nowdays, I’ve been watching
the current rennaissance with interest. PHP is taking note of trends
being pushed by the Rails world with “clean and classy” frameworks such
as Laravel and best practice manifestos like PHP the right
way coming to light. This is all good
news for the web because better quality code (whatever language it’s
written in) raises the bar all round.
That said, there still seems to be a lack of good documentation
concerning proper Behaviour Driven Development (BDD) with PHP and it’s a
problem that we came across recently at Kyan. I’m going to try and do a
walkthrough of setting up some simple fetaure testing using a default
install of the Prestashop ecommerce
framework. It’s a fairly sizeable project that doesn’t ship with test
coverage - let’s fix that!
Prestashop has a good set of instructions so I won’t duplicate them all
here. Suffice to say you need to set up a MySQL db and change a few
permissions.
2.) Install Composer
Make sure PHP is in your path. You can test this by firing up terminal
and typing
1
php -v
If you’re using MAMP, put something like
this at the bottom of your .bashrc
1
export PATH="/Applications/MAMP/bin/:$PATH"
Double check the path to the MAMP binaries. It’s different in different
versions of MAMP.
Now cd into your newly created prestashop folder and run the following
commands to install composer;
12
mkdir bin
curl -s https://getcomposer.org/installer | php -- --install-dir=bin
If you’re installing using the default OSX apache and you get an error
about “detect_unicode = Off”, you’ll also need to
run the following;
1234
sudo su
echo"detect_unicode = Off" >> /private/etc/php.ini
apachectl restart
exit
3.) Install Behat and Mink
This part is the reason I’m writing this blog post. There seems to be a
lack of clear information about how to get these up and running from
scratch. Hopefully this will be sorted out as time goes on but I would
suggest people submit their points about what works and doesn’t work for
them.
To get started, make a file called composer.json in the root of the
Prestashop install and put in the following;
This is the equivalent of a Gemfile if you’ve used bundler and Ruby. So
to get everything up and running, run composer like so.
1
phpbin/composer.pharinstall
All being, well you should see the packages installing into a vendor
folder in the project root.
4.) Setting up Behat
1234
> bin/behat --init
+d features - place your *.feature files here
+d features/bootstrap - place bootstrap scripts and static files here
+f features/bootstrap/FeatureContext.php - place your feature related code here
This bootstraps the files needed to run the tests. Let’s go ahead and
write our first test. Usually in test driven development, we write the
tests before the code, but I’m advocating here that we can add tests to
and exisiting site to give us the confidence to refactor and add
features later on. Start editing /features/basket.feature and put in
the following;
123456789101112
Feature: Basket In order to add a product to the basket As a website user I need to add the product to my basket And I should see the product in the basket @javascriptScenario: Add a product to basket Given I am on a product pageWhen I click "Add to cart"And I hover over "#shopping_cart a"Then I should see the product title
This is an example of a Cucumber test (the natural language syntax is
referred to as Gherkin) which is a popular way of doing acceptance
testing in Rails and other frameworks. We proceed by running the test
like so;
1234567891011121314151617
> bin/behat
Feature: Basket
In order to add a product to the basket
As a website user
I need to add the product to my basket
And I should see the product in the basket
@javascript
Scenario: Add a product to basket # features/basket.feature:8 Given I am on a product page
When I click on "Add to cart" And I hover over "#shopping_cart a" Then I should see the product title
1 scenario (1 undefined)4 steps (4 undefined)0m0.039s
You can implement step definitions for undefined steps with these snippets:
/*** @Given /^I am on a product page$/ */public function iAmOnAProductPage(){ throw new PendingException();}/*** @When /^I click "([^"]*)"$/ */public function iClick($arg1){ throw new PendingException();}/** * @Given /^I hover over "([^"]*)"$/ */public function iHoverOver($arg1){ throw new PendingException();}/*** @Then /^I should see the product title$/ */public function iShouldSeeTheProductTitle(){ throw new PendingException();}
This runs the test and tells you what to do next. Copy the snippets into
the features/bootstrap/FeatureContext.php file above the final
curly brace. After saving and running bin/behat again, the output should change to include
1
TODO: write pending definition
5.) Enable Mink
This is where the documentation starts to part ways with the latest
versions of Mink and Behat. After struggling with the official run
through at http://mink.behat.org/ trying to
get Zombie working as the Javascript client, I stumbled across the Mink
example repository in the Behat official Github page here
https://github.com/Behat/MinkExtension-example
The key is swapping out BehatContext in the FeaturesContext file, to
make sure that it reads;
1
class FeatureContext extends Behat\MinkExtension\Context\MinkContext
and then you need to configure Behat and Mink to be looking in the right
places and choosing the correct browser drivers. Make a file called
behat.yml in the root of the project with the following;
goutte seems to be a requirement as far as I can tell, but who
knows…
Now when you run bin/behat again, you should see an error like this;
1
Curl error thrown for http POST to http://localhost:4444/wd/hub/session with params:{"desiredCapabilities":{"browserName":"firefox","version":"8","platform":"ANY","browserVersion":"8","browser":"firefox"},"requiredCapabilities":[]}
Enter Selenium…
6.) Setting up selenium
This should be quite straightforward. First off, make sure you have the
standard version of Firefox installed. Download Selenium RC from the link
at the top of the page and move the .jar file to the vendor/ folder.
Then run the following command to start the Selenium server;
You should see INFO: Launching a standalone server and you’re good to
try the tests again by running bin/behat. This time you’ll see Firefox
start up, flash up with the page content and close again. The first test
passed! Now let’s implement the others.
7.) Finishing the steps
Change the methods in FeatureContext.php to look like this;
1234567891011121314151617
/** * @Given /^I hover over "([^"]*)"$/ */public function iHoverOver($arg1){ $this->getSession()->getPage()->find('css', $arg1)->mouseOver(); $this->getSession()->wait(5000, "$('#cart_block_list').hasClass('expanded')");}/** * @Then /^I should see the product title$/ */public function iShouldSeeTheProductTitle(){ $product_title = $this->getSession()->getPage()->find('css', '.cart_block_product_name'); $product_title == "iPod Nano";}
A few things to note here before we proceed - this isn’t a very robust
test and could be improved. There’s duplication all over the place which
could be refactored (but it’s late and I’ve just got it working…). The
real problem is that it’s assuming the title of the product as the
assertion in the last point. A better approch would be to call/mock the
product from Prestashop in the constructor and use that instead. Maybe
in part 2.
Also, it bugged me that the wait->(...) call which executes JS is
executed in the context of the session. It makes perfect sense in
hindsight as getPage is returning a sort of DOM object but it tripped
me up nonetheless.
Down to business - run bin/behat one last time and the output should
look like this;
1234567891011121314151617
> bin/behat 1 ↵
Feature: Basket
In order to add a product to the basket
As a website user
I need to add the product to my basket
And I should see the product in the basket
@javascript
Scenario: Add a product to basket # features/basket.feature:8 Given I am on a product page # FeatureContext::iAmOnAProductPage() When I click on "Add to cart"# FeatureContext::iClickOn() And I hover over "#shopping_cart a"# FeatureContext::iHoverOver() Then I should see the product title # FeatureContext::iShouldSeeTheProductTitle()1 scenario (1 passed)4 steps (4 passed)0m4.056s
Happy days!
Conclusion
There’s a ton of PHP apps out there that would benefit from regression
tests like this. They’re not that hard to write once you get going and
they give you an invaluable safety net and peace of mind when it comes
to refactoring and adding new features.
My motivation for writing this was that the current state of documentation
seems a bit patchy, despite the Behat and Mink projects being well established.
I suppose this is a call to arms to PHP devs to try this stuff out
and get their feedback into the loop.
I’d love to see more advanced posts dealing with the various drivers
that are available and also to hear what people think of
this one. I’ll finish by saying I’m starting out on this trail so my advice
is only what I’ve managed to cobble together up to now. If there’s
improvements or suggestions in the comments I’ll happily fold them back in to
the main post. Happy testing…
There’s always debate about which ecommerce platform is harder/better/faster/stronger, and from experience of Prestashop and Magento I can say they’re both really good for different types of shops. For Forsyths, I developed a Prestashop solution for their Sheet Music department whilst I was working there and it’s been great so far but…
When picking a solution it really pays to do some forward planning and in this case, Prestashop can’t scale up to the kind of multi-store functionality that this music department store needed. That said, it was a lot easier getting a Prestashop store off the ground (I tried both at the start) and it’s served us well. My only gripe is that Prestashop’s module and templating system isn’t flexible enough to make real changes without editing the core – and that screws up future updates. Not what you need for ecommerce purposes.
UPDATE
Since writing this, it’s all change with Prestashop v1.5
They’ve had a basic overrides system in place for a while now.
Also the new version supports multistore but I haven’t used it, yet…
** Making the switch
Swapping out the products from PS to Mage is pretty straightforward – it’s just a case of a mysql query which maps them into the right fields. As with any Magento import the process is more or less similar;
Add some dummy data into Magento, using all the fields you’re likely to use
Export the data as a csv using System > Import/Export > Profiles
Look at the file in your var/export folder and take a look at the headers
Tip for *nix users working with csv files – in terminal use the following command to save the first two lines to a file;
1
head -n 2 your_csv_file.csv > your_csv_file_head.csv
That’s if your csv file is really big.
** The problem with users
Now the above works fine for most things, but the problem with users lies in the different authentication that both shops use. Magento uses MD5 with a salt on the end, Prestashop uses a ‘Cookie Key’ prefix to the customer password, which is then MD5 encrypted.
Anyone who’s looked into MD5 knows that this is a pig – you can’t reverse an MD5 into plain text, therefore you can’t get the original password strings in order to re-encode them (which is actually a good thing…).
** How to fix it
Clearly its impossible to convert from one MD5 hash to another so we have to try something different. Make the following file;
class Mage_Customer_Model_Customer extends Mage_Core_Model_Abstract{/*** Authenticate customer** @param string $login* @param string $password* @return true* @throws Exception*/public function authenticate($login, $password){$this->loadByEmail($login);if ($this->getConfirmation() && $this->isConfirmationRequired()) {throw Mage::exception('Mage_Core', Mage::helper('customer')->__('This account is not confirmed.'),self::EXCEPTION_EMAIL_NOT_CONFIRMED);}if (!$this->validatePassword($password) && !$this->validatePassword('hKvthisisyourgibberishcookiestringfromprestashopCM'.$password)) {throw Mage::exception('Mage_Core', Mage::helper('customer')->__('Invalid login or password.'),self::EXCEPTION_INVALID_EMAIL_OR_PASSWORD);}Mage::dispatchEvent('customer_customer_authenticated', array('model' => $this,'password' => $password,));return true;}//end class}}
Notice the random string prepended to the password variable? That’s your cookie string which you’ll find in your Prestashop install here;
1
(prestshop root)/config/settings.inc.php
It’s the line beginning
1
define('_COOKIE_KEY_', 'ThisIsTheBitYouWant...'
Believe it or not that’s all you need to do. Import your MD5 hashes from Prestashop straight into Magento and it’ll authenticate them as Prestashop would do. I’ll go through the mysql import in another post.