PHP Shell CSS
Switch branches/tags
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
app
tests
.gitignore
.travis.yml
README.md
composer.json
modman
travis.sh

README.md

Xtest

Simple Magento Testing Framework.

Build Status

http://xtest-mage.com/


General

xtest is a test-suite that integrates PHPUnit into Magento. It supports basic unit testing as well as selenium testing.

xtest is designed to create integrations tests for projects. Instead of creating all entities we are using a preconfigured database.

Install

To Install xtest link all files to your magento installation. We are providing a modman file to do this automatically.

Unit-Test

Unit tests run on the current database. All changes runs in transactions so nothing may change your database. (Except of MyISAM Tables which are not supporting database rollbacks)

Get started

All samples are taken from https://github.com/code-x/xTest.Demo. We also provide a demo video https://www.youtube.com/watch?v=rPyhS_neY6k

To start testing create a test class in your custom module your currently working on. You just have to create a directory called Test. Then create a file HomepageControllerTest.php with the following content:

File: app/code/local/Codex/Demo/Test/Controller/HomepageControllerTest.php

<?php

class Codex_Demo_Test_Controller_HomepageControllerTest extends Codex_Xtest_Xtest_Unit_Frontend
{

    /**
     *
     */
    public function testHomePageContainsNewProducts()
    {
        $this->dispatch('/');

        // Checks Layout Wrapper exists
        $this->assertLayoutBlockExists('cms.wrapper');

        // Checks page contains some content
        $this->assertContains('New Products', $this->getResponseBody());

    }

}

Run tests using following commands:

cd htdocs/tests
php phpunit.phar ../app/code/local/Codex/Demo/

Congratulations! You've just completed your first unit test using xtest.

You do not have to call a test directly, without an certain filename in the shell all files in the directory are passed and the result printed on the screen. To call certain test-cases you simply have to add the filename:

cd htdocs/tests
php phpunit.phar ../app/code/local/Codex/Demo/Test/Controller/HomepageControllerTest.php

Parameter

--store_code - store code thats bootet on frontend-tests --external -- run tests thats have an @external annotation also --disable_double - do not use double mocks

Selenium based

--browser - Sets Browser thats used in selenium tests (must be available) --breakpoints - define repsonsive breakpoints eg. "1024x800,1280x1024" --debug -- do not close browser window on exception

Configuration

To set-up fixture and selenium configuration (e.g. product sku, e-mailadress, etc) see in app/code/community/Codex/Xtest/etc/xtest.xml You could change all values by creating an own xtest.xml in your module and override our values with yours.

For magento sample data you could use this one:

<config>
    <default>
        <xtest>
            <selenium>
                <screenshot>
                    <breakpoints>450x1024,1280x1024</breakpoints>
                </screenshot>
            </selenium>

            <fixtures>

                <customer>
                    <email>devnull@code-x.de</email>
                    <firstname>Test Vorname</firstname>
                    <lastname>Test Nachname</lastname>

                    <billing_address>
                        <firstname>Xtest Firstname</firstname>
                        <lastname>Xtest Lastname</lastname>
                        <street>Xtest Street</street>
                        <city>Xtest City</city>
                        <postcode>33100</postcode>
                        <telephone>Xtest Phone</telephone>
                        <country_id>DE</country_id>
                        <region_id>88</region_id>
                    </billing_address>

                    <shipping_address>
                        <firstname>Xtest Firstname</firstname>
                        <lastname>Xtest Lastname</lastname>
                        <street>Xtest Street</street>
                        <city>Xtest City</city>
                        <postcode>33100</postcode>
                        <telephone>Xtest Phone</telephone>
                        <country_id>DE</country_id>
                        <region_id>88</region_id>
                    </shipping_address>

                </customer>

                <order>

                    <customer_id>0</customer_id>
                    <customer_data>
                        <email>devnull@code-x.de</email>
                        <firstname>Test Firstname</firstname>
                        <lastname>Test Lastname</lastname>
                    </customer_data>

                    <payment_method>
                        <method>cashondelivery</method>
                        <!-- some other options-->
                    </payment_method>

                    <shipping_method>
                        <method>ups_XPD</method>
                    </shipping_method>

                    <billing_address>
                        <firstname>Xtest Firstname</firstname>
                        <lastname>Xtest Lastname</lastname>
                        <street>Xtest Street</street>
                        <city>Xtest City</city>
                        <postcode>33100</postcode>
                        <telephone>Xtest Phone</telephone>
                        <country_id>DE</country_id>
                        <region_id>88</region_id>
                    </billing_address>

                    <shipping_address>
                        <firstname>Xtest Firstname</firstname>
                        <lastname>Xtest Lastname</lastname>
                        <street>Xtest Street</street>
                        <city>Xtest City</city>
                        <postcode>33100</postcode>
                        <telephone>Xtest Phone</telephone>
                        <country_id>DE</country_id>
                        <region_id>88</region_id>
                    </shipping_address>

                    <items>

                        <item>
                            <qty>1</qty>
                            <sku>abl004</sku>
                        </item>

                    </items>

                </order>
            </fixtures>

            <selenium>

                <checkout>

                    <customer>
                        <email>selenium@code-x.de</email>
                        <firstname>Firstname</firstname>
                        <lastname>Lasntname</lastname>
                    </customer>

                    <addtocart>

                        <product_1>
                            <sku>abl004</sku>
                            <qty>1</qty>
                        </product_1>

                    </addtocart>

                    <billing_address>

                        <firstname>test firstname</firstname>
                        <lastname>test lastname</lastname>
                        <company>company</company>
                        <telephone>123456</telephone>
                        <street1>Teststreet 32</street1>
                        <city>Testcity</city>
                        <postcode>33100</postcode>
                        <use_for_shipping_no>1</use_for_shipping_no>

                    </billing_address>

                    <shipping_address>
                        <!-- todo -->
                    </shipping_address>

                    <shipping_method>
                        <method>ups_XPD</method>
                    </shipping_method>

                    <payment_method>
                        <method>cashondelivery</method>
                    </payment_method>

                </checkout>

            </selenium>

        </xtest>
    </default>
</config>

Basic Test Classes

We are provding two different Unit-Test classes. Please extend Codex_Xtest_Xtest_Unit_Frontend to create frontend tests and Codex_Xtest_Xtest_Unit_Admin to create tests concerning the Magento backend.

Codex_Xtest_Xtest_Unit_Abstract

  • dispatchUrl( $httpUrl, $postData = null ): Dispatch a url

  • dispatch($route, $params = array(), $postData = null): Dispatch a magento url

  • dispatch($route, $params = array(), $postData = null): Dispatch a magento url

Codex_Xtest_Xtest_Unit_Frontend

  • assertPaymentMethodIsAvailable: Checks if a payment method is available
  • populuateQuote: Poplulates Magento Quote to all magento methods
  • setCustomerAsLoggedIn: Sets customer as logged in

Codex_Xtest_Xtest_Unit_Admin

Sets first Admin-User as logged in automatically.

Mocking

Mocking models or helpers is a elementary feature to create tests. Normally you should test a explicit function and mock all other depending stuff to have reliable results.

Model Mocking

In this example we have a Model catalog/product which has a method isSaleable. This method depends on isAvailable that returns true when product is available, false when not.

So, let us start mocking.

<?php

class Codex_Demo_Test_Model_ProductTest extends Codex_Xtest_Xtest_Unit_Frontend
{

    public function demoProvider()
    {
        return array(
            array( true, true ),
            array( false, false )
        );
    }

    /**
     * As catalog/product Model
     * - when product is not available
     * - then it should not be saleable
     *
     * @dataProvider demoProvider
     **/
    public function testDemoMock($productIsAvailable, $expectedSaleable)
    {
        $mock = $this->getModelMock('catalog/product', array('isAvailable') );
        $mock->expects($this->any())
            ->method('isAvailable')
            ->willReturn( $productIsAvailable );
        $this->addModelMock( 'catalog/product', $mock );

        /** @var Mage_Catalog_Model_Product $product */
        $product = Mage::getModel('catalog/product');
        $this->assertEquals( $product->isSalable(), $expectedSaleable );
    }

}

Normally you do not test your mocking results. Rather you have to mock depending models to test your own model. Here we have create a simple mocking example.

Helper Mocking

You could mock helper in same way

$mock = $this->getHelperMock('codex_demo', array('getDemoMethod') );
$mock->expects( $this->any() )
    ->method( 'getDemoMethod' )
    ->willReturn( 'some value' );
$this->addHelperMock('codex_demo', $mock);

Please consider you create a mock using $this->getHelperMock() and publish your mock using $this->addHelperMock().

Permanent Mocking: Double

Sometimes you have a class which communicates to an external service, or something your class is doing really crazy stuff so you are not able to test other modules. In this case you could create a permanent mock.

<?php

class Codex_Demo_Model_Weather extends Varien_Object
{

    protected function _apiCall($city, $country)
    {
        return file_get_contents('http://api.openweathermap.org/data/2.5/weather?q='.$city.','.$country);
    }

    /**
     * Return weather for city eg. "broken clouds"
     *
     * @param $city
     * @param $country_id
     * @return mixed
     */
    public function getWeather( $city, $country_id )
    {
        $data = json_decode( $this->_apiCall( $city, $country_id ), true );
        return $data['weather'][0]['description'];
    }

}

If you create a second class

class Codex_Demo_Test_Double_Model_Weather extends Codex_Demo_Model_Weather
{
    protected function _apiCall($city, $country)
    {
        return '{ "coord": { "lon": 8.75, "lat": 51.72 }, "sys": { "type": 3, "id": 177301, "message": 0.0283, "country": "DE", "sunrise": 1425190178, "sunset": 1425229494 }, "weather": [ { "id": 803, "main": "Clouds", "description": "broken clouds", "icon": "04d" } ], "base": "cmc stations", "main": { "temp": 283.41, "humidity": 65, "pressure": 1001, "temp_min": 283.15, "temp_max": 283.55 }, "wind": { "speed": 1, "gust": 3, "deg": 180 }, "rain": { "3h": 0 }, "clouds": { "all": 64 }, "dt": 1425221237, "id": 2855745, "name": "Paderborn", "cod": 200 }';
    }

}

xtest is going to use Codex_Demo_Test_Double_Model_Weather instead of Codex_Demo_Model_Weather. This is helpfully because your tests not depending on a external service.

Block Mocking

Blocks should get all data from model(s) so you really don't want to mock them.

Fixtures

We don't like yaml. So we using magento classes to generate test data. Normally we are using preconfigured databases so we do not have to create all our product-data before testing.

Order / Quote

This will create a basic test order.

/** @var $orderFixture Codex_Xtest_Xtest_Fixture_Order */
$orderFixture = Xtest::getXtest('xtest/fixture_order');
$testOrder = $orderFixture->getTest();

Customer

This will create a test customer.

/** @var $customerFixture Codex_Xtest_Xtest_Fixture_Customer */
$customerFixture = Xtest::getXtest('xtest/fixture_customer');
$testCustomer = $customerFixture->getTest()

Mailing

All mails during your work with Xtest are not beeing send; the are all queued and can be viewed for total control. You can access the mail queue this way:

/** @var $mailqueue Codex_Xtest_Xtest_Helper_Mailqueue */
$mailqueue = Xtest::getXtest('xtest/helper_mailqueue');

print_r( $mailqueue->getQueue() );

In addition Xtest provids some usefull asserts:

  • $this->assertMailTemplateIdSent( $yourTemplateId );
  • $this->assertMailsSent( $yourMailsSentCount )

Examples

Create Order, send order email and check if mail is sent

<?php

class Codex_Demo_Test_Integration_OrderTest extends Codex_Xtest_Xtest_Unit_Frontend
{

    /**
     * As Customer
     * - when i plaved a order
     * - then I should reveice a new order email
     */
    public function testOrderMail()
    {
        /** @var $orderFixture Codex_Xtest_Xtest_Fixture_Order */
        $orderFixture = Xtest::getXtest('xtest/fixture_order');
        $testOrder = $orderFixture->getTest();

        $testOrder->sendNewOrderEmail();

        $this->assertMailsSent( 1 );
        $this->assertMailTemplateIdSent( 'sales_email_order_template' );
    }

}

Dispatch Route, check if layout is present

<?php

class Codex_Demo_Test_Controller_HomepageControllerTest extends Codex_Xtest_Xtest_Unit_Frontend
{

    /**
     * As Customer
     * - when I open Homepage
     * - I should see "New Products"
     */
    public function testHomePageContainsNewProducts()
    {
        $this->dispatch('/');

        // Checks Layout Wrapper exists
        $this->assertLayoutBlockExists('cms.wrapper');

        // Checks page contains some content
        $this->assertContains('New Products', $this->getResponseBody() );

    }

}

Render-HTML

When the selenium-server is running you have the option to take screenshots of the html. These screenshots are stored in png-format in a directory of your project.

class Codex_Demo_Test_Selenium_HomepageScreenshotTest extends Codex_Xtest_Xtest_Unit_Frontend
{

    public function testRenderHomepage()
    {
        $this->dispatch('/');
        $this->renderHtml('homePage', $this->getResponseBody() );
    }

}

This is quite comfortable to take screenshots from customer/account because you are able to mock or create some data. All data created here would be reverted after your test because you are extending Codex_Xtest_Xtest_Unit_Abstract.

class Codex_Demo_Test_Selenium_CustomerAccountScreenshotTest extends Codex_Xtest_Xtest_Unit_Frontend {
    public function testOrderHistory()
    {
        /** @var $customerFixture Codex_Xtest_Xtest_Fixture_Customer */
        $customerFixture = Xtest::getXtest('xtest/fixture_customer');
        $customer = $customerFixture->getTest();
        $customer->setConfirmation(null);
        $customer->save();

        /** @var $orderFixture Codex_Xtest_Xtest_Fixture_Order */
        $orderFixture = Xtest::getXtest('xtest/fixture_order');

        $quote = $orderFixture->getFixtureQuote()->getTest( $customer );
        $order = $orderFixture->convertQuoteToOrder( $quote );
        $order->setState( current( Mage::getSingleton('sales/order_config')->getVisibleOnFrontStates() ) );
        $order->save();

        $this->setCustomerAsLoggedIn( $customer );

        $this->dispatch('sales/order/history');
        $this->renderHtml( 'account order history', $this->getResponseBody() );

        $this->dispatch('sales/order/view/order_id/'.$order->getId());
        $this->renderHtml( 'account order details', $this->getResponseBody() );
    }
}

You can view the screenshots (and test results) by browsing to http://localhost/YourProject/htdocs/tests/view/

Selenium Tests

All selenium tests a running against your current database. NOTHING can be reverted. You have to clean up data by yourself! (or you do not care about cleaning up) Be sure you are extending Codex_Xtest_Xtest_Selenium_TestCase

Using Selenium

You have to start Selenium first. We provide all required files in htdocs/tests/selenium. Just run start.sh to start it.

cd htdocs/tests/selenium
./start.sh

Page-Objects

We are providing some basic page objects to simplify handling selenium tests. Let us start with some really tricky testing: onepage checkout progress.

<?php

class Codex_Demo_Test_Selenium_CheckoutTest extends Codex_Xtest_Xtest_Selenium_TestCase
{

    protected static $_customerEmail;
    protected static $_customerPassword;

    public function setUp()
    {
        parent::setUp();

        $customerConfig = self::getSeleniumConfig('checkout/customer');
        self::$_customerEmail = $customerConfig['email'];

        // Delete Testcustomer
        $customerCol = Mage::getModel('customer/customer')->getCollection();
        $customerCol->addFieldToFilter('email', self::$_customerEmail );
        $customerCol->walk('delete');

        // Create a new one
        $customer = Mage::getModel('customer/customer');
        $customer->setData($customerConfig);
        self::$_customerPassword = $customer->generatePassword();
        $customer->setStore( current( Mage::app()->getStores() ) ); // TODO
        $customer->setPassword( self::$_customerPassword );
        $customer->validate();
        $customer->setConfirmation(null);
        $customer->save();

        $customer->load( $customer->getId() );
        $customer->setConfirmation(null);
        $customer->save();

        $_custom_address = array (
            'firstname' => 'Test',
            'lastname' => 'Test',
            'street' => array (
                '0' => 'Sample address part1',
            ),
            'city' => 'Paderborn',
            'region_id' => '',
            'region' => '88',
            'postcode' => '33100',
            'country_id' => 'DE',
            'telephone' => '0000111',
        );
        $customAddress = Mage::getModel('customer/address');
        $customAddress->setData($_custom_address)
            ->setCustomerId($customer->getId())
            ->setIsDefaultBilling('1')
            ->setIsDefaultShipping('1')
            ->setSaveInAddressBook('1');
        $customAddress->save();
    }

    public function testOnepageCheckout()
    {

        $cartConfig = $this->getSeleniumConfig('checkout/addtocart');
        foreach( $cartConfig AS $_productData )
        {

            /** @var $productPageObject Codex_Xtest_Xtest_Pageobject_Frontend_Product */
            $productPageObject = $this->getPageObject('xtest/pageobject_frontend_product');

            $productPageObject->openBySku( $_productData['sku'] );
            $productPageObject->setQty( $_productData['qty'] );

            $productPageObject->pressAddToCart();
            $productPageObject->assertAddToCartMessageAppears();

        }

        /** @var $cartPageObject Codex_Xtest_Xtest_Pageobject_Frontend_Cart */
        $cartPageObject = $this->getPageObject('xtest/pageobject_frontend_cart');
        $cartPageObject->open();

        $cartPageObject->takeResponsiveScreenshots('products in cart');

        $this->assertEquals( count($cartConfig), count( $cartPageObject->getItems() ), 'cart is missing some items' );

        $cartPageObject->clickProceedCheckout();
        $this->assertContains('checkout/onepage/', $this->url() );

        // ---

        /** @var $checkoutPageObject Codex_Xtest_Xtest_Pageobject_Frontend_Checkout */
        $checkoutPageObject = $this->getPageObject('xtest/pageobject_frontend_checkout');

        $checkoutPageObject->takeResponsiveScreenshots('login');
        $checkoutPageObject->login( self::$_customerEmail, self::$_customerPassword );
        $checkoutPageObject->assertStepIsActive('billing');

        // ---

        $checkoutPageObject->setBillingAddress();
        $checkoutPageObject->takeResponsiveScreenshots('billing address');
        $checkoutPageObject->nextStep();

        // ---

        // TODO: Shipping Address

        // ---

        $checkoutPageObject->assertStepIsActive('shipping_method');
        $checkoutPageObject->setShippingMethod();
        $checkoutPageObject->takeResponsiveScreenshots('shipping method');
        $checkoutPageObject->nextStep();

        // ---

        $checkoutPageObject->assertStepIsActive('payment');
        $checkoutPageObject->setPaymentMethod();
        $checkoutPageObject->takeResponsiveScreenshots('payment method');
        $checkoutPageObject->nextStep();

        // ---

        $checkoutPageObject->assertStepIsActive('review');
        $checkoutPageObject->acceptAgreements();
        $checkoutPageObject->takeResponsiveScreenshots('review');
        $checkoutPageObject->nextStep();

        // ---

        $checkoutPageObject->takeResponsiveScreenshots();
        $checkoutPageObject->assertIsSuccessPage();

    }

}

Make sure you have configured your test-data as mentioned in chapter "Configuration".

Run selenium tests

To run your tests using firefox und taking screenshots in a width of 450px and 1280px open a console and type:

cd htdocs/tests
php phpunit.phar ../app/code/local/Codex/Demo/Test/Selenium/CheckoutTest.php --browser firefox --breakpoints 450x800,1280x1024

Tip: If you are debugging tests you could use parameter --debug so the browser window is not closing as fast as during the normal modus.