C H A P T E R  14

image

Regression Tests

The big challenge has begun. In the previous chapter we have been asked to add new features to an old PHP legacy application in a very complex company context. The application has not been maintained for a long time, since the person who developed it no longer works at the company. Management wants some new features and has absolutely no budget to rewrite it from scratch. The development team is paralyzed by this kind of situation. How could we tackle this critical situation?

Ugly But Valuable

Within many companies, there is old software that, despite its age, continues to return great value for the business processes. They are usually ignored by the entire IT department, the members of which are frightened by the thought of having to change. Users don't want new features anymore, because they know that won't have them. If this software could start to grow again, adapting to new business processes, it would be even more valuable for the company.

Throughout this book we have learned about many little refactoring techniques that allow us to maintain the value of software while we grow or maintain it, making it easier to accommodate future implementations. In this case, these small techniques cannot be applied because our code is not object-oriented, and it is not tested. So it has the worst smell that software can have.

For this kind of situation, we can't talk about “refactoring” but about “big refactoring.” This term is intended to indicate major refactoring activities that take a lot of time at the beginning, but once done, they will allow us to have better software which to apply the classic refactoring to.

In the next chapters we will see how to apply some of the big refactoring techniques to transform our legacy web application, introduced in the previous chapter, into an object-oriented application that best fits the requirements of implementation and design.

Specifically we will introduce the following techniques of “big refactoring.”

  • Putting chaos in a cage
  • Transforming procedural code into object-oriented code
  • Introducing ORM instead of SQL
  • Separating business logic from presentation

To demonstrate these techniques we will use the following steps:

  • Write the regression tests for the application, so as not to lose value when refactoring.
  • Transform procedural code into object code through the “Facade Pattern.”
  • Replace the SQL logic by introducing an Object Relational Mapper (ORM).
  • Separate the whole application logic from presentation logic through the “MVC” and “Template View” patterns.
  • Add new presentation logic to display the web application on mobile devices.

Keeping Value vs. Wasting Value

The concept of software value is complex to explain, because it can change between different companies or between different products. For example, a web portal can have great value, because the company needs it to communicate to the outside, and e-commerce is important because a certain company sells its products in markets that normally it could not reach. An intranet is important because it improves the internal company processes and knowledge, and a social software is important because it connects people and makes the company money through advertising or aggregated data selling.

The only thing all these examples have in common is that if a software has value, but cannot grow with the company or follow the market and customer requirements, that software loses value. Even the company itself loses value because it wastes money recreating the same value from scratch.

When you are afraid to modify software, and you don't have the courage to embrace the changes, first you have to understand why. Usually, the reasons are

  • The code is old.
  • The code is not tested.
  • The code is very messy and was not designed clearly.
  • The code is very large.

Usually these reasons are presented all together, which we must learn to deal with. A software that cannot be changed is a dead but walking software.

First we need to be able to change it without the risk of introducing bugs. The best tool we have at our disposal to measure the changes are automated tests. Our first goal is to put our software in a cage of regression functional tests. Through these tests we can avoid introducing bugs, since the tests will notify a failure before it is put into production.

Now we'll see a refactoring technique in the same format viewed in the previous chapters but with an extended example, where we'll learn how to test procedural code with regression functional tests.

Putting the Chaos in a Cage

Problem: “I have an untested procedural application.”

Solution: “Cage the application with regression functional tests.”

Motivation

When we are asked to change or add features to an untested application, the first thing we must do is confine them in regression tests.

Regression tests ensure that we can modify the application without losing the original value, since any change affecting the standard behavior will be notified directly by the tests. In this way we will be free to apply major refactoring in order to make an old application young again.

Where the application is procedural, we can use functional tests to test the macro functionality and unit tests to test the functions. If an application is already written with an object-oriented paradigm we can use unit tests.

Another advantage of writing the regression tests is to know the application domain inside. If there isn't technical documentation of the application, by writing tests, we will go inside the business logic mechanism and at the same time begin to create documentation for the application, because tests can be part of documentation.

With very large applications, we should write tests with an expert user of the software who can help us to identify all of the features, including hidden gems. The work will be a long process but it's necessary to not lose the value the software already has.

Mechanics

  1. Separate the user application actions. For example, in web management common actions are related to actions on the same entity (CRUD). In the case of an intranet or internet portals, common actions are related to the navigation in a single information section (navigating a tree of content, research, etc.) and the actions possible there.
  2. Prepare the fixtures to be used in testing, in order to have independent tests.
  3. For each set of common actions, create a new suite of test cases.
  4. For every action, add a new test case, with test navigation, assertion, and verification, especially taking care to test the critical actions that can fail.
  5. Add the test case to the suite.
  6. Run the test suite to make sure everything is correct.
  7. Continue until you have tested all user actions.

Examples

For our examples, we will use the web application “Contacts Book,” seen in the previous chapter. Following the mechanism, the first thing we must do is to separate the application into common user actions. The application is a web application that performs web actions on the contact entity. We can add, edit, read, and delete a contact record in a database.

First prepare the relevant data that will load when we start each test, so that the tests will be independent. To do this, we start doing a dump only of the data of the application and not of the structure. Once we do the dump, remove the useless records, and keep only a significant number of records per entity. Remember that, before the data is loaded, we always must empty the table, otherwise the data is added to the bottom.

--- fixtures/contacts.sql ---
TRUNCATE TABLE contacts;

LOCK TABLES contacts WRITE;
INSERT INTO contacts (firstname, lastname, phone, mobile) VALUES ('Jacopo', 'Romei', image
'0543123543', '34012345'),
INSERT INTO contacts (firstname, lastname, phone, mobile) VALUES ('Francesco', 'Trucchia', image
'12345', '234 12345'),
UNLOCK TABLES;

PHPUnit could support the ability to upload data in XML or CSV, but when using the extension for Selenium, we can't use this feature, because both are extensions and in PHPUnit we can use only one extension at a time. We will load the fixtures in SQL directly running a command shell.

We create the folder tests in the root of application, and put the fixtures and functional folder inside the tests folder. In the fixture folder we will put fixture files and in the functional folder we'll put all the functional tests. As mentioned previously, we make a dump of the database and save it in the fixtures folder with the name contacts.sql.

We create also a new folder called contact inside the functional folder, where we put all the test cases related by common actions we can make with the contact entity.

The result should be the following:

...
tests
|- fixtures
| |- contacts.sql
|- functional
| |- contacts
...

At this point, through the PHPUnit Selenium extension, we begin to write our functional tests for each action we can perform with the contact entity. The actions we can perform are:

  • Add a new contact
  • Edit a contact
  • Display a list of contacts sorted by last name
  • Remove contact
  • Validate inserting or editing a contact

For each of these actions we will write a new test case, with its regression tests inside.

Add a New Record

This test is on adding a new contact record.

<?php
require_once 'PHPUnit/Extensions/SeleniumTestCase.php';

class Contact_AddTest extends PHPUnit_Extensions_SeleniumTestCase
{

  function setUp()
  {
    shell_exec('mysql -u root contacts < '.dirname(__FILE__).'/../../fixtures/contacts.sql'),

    $this->setBrowser("*firefox");
    $this->setBrowserUrl("http://localhost/");
  }

  function testAdd()
  {
    $this->open("/refactoring/index.php");
    $this->click("link=New contact");
    $this->waitForPageToLoad("30000");

    $this->assertEquals("Contacts Book", $this->getTitle());
    $this->assertEquals("Contacts Book", $this->getText("//div[@id='header']/h1"));

    $this->assertEquals("First Name*", $this->getText("//div[@id='content']/form/label[1]"));
    $this->assertEquals("Last Name*", $this->getText("//div[@id='content']/form/label[2]"));
    $this->assertEquals("Phone*", $this->getText("//div[@id='content']/form/label[3]"));
    $this->assertEquals("Mobile", $this->getText("//div[@id='content']/form/label[4]"));

    $this->assertTrue($this->isElementPresent("firstname"));
    $this->assertTrue($this->isElementPresent("lastname"));
    $this->assertTrue($this->isElementPresent("phone"));
    $this->assertTrue($this->isElementPresent("mobile"));
    $this->assertTrue($this->isElementPresent("link=Cancel"));

    $this->assertEquals("(* Mandatory fields)", $this->getText("//div[@id='content']/em"));
    $this->assertEquals("All © Francesco Trucchia & Jacopo Romei - Pro PHP Refactoring", image
$this->getText("footer"));

    $this->type("firstname", "Girolamo");
    $this->type("lastname", "Pompei");
    $this->type("phone", "098245678");
    $this->type("mobile", "3402343879");
    $this->click("//input[@value='Save']");
    $this->waitForPageToLoad("30000");

    $this->assertEquals("Pompei", $this->getTable("//div[@id='content']/table.1.0"));
    $this->assertEquals("Girolamo", $this->getTable("//div[@id='content']/table.1.1"));
    $this->assertEquals("098245678", $this->getTable("//div[@id='content']/table.1.2"));
    $this->assertEquals("3402343879", $this->getTable("//div[@id='content']/table.1.3"));
    $this->assertEquals("[X]", $this->getTable("//div[@id='content']/table.1.4"));

    $this->assertTrue($this->getXpathCount('//table/tbody/tr') == 4);

  }
}
?>

In the setUp() method we load the fixtures in the database, run the web browser, and set the BasePath of the application. In the testAdd() method we verify that a new record is properly inserted.

We run the tests and verify that everything is ok. We start the Selenium RC server if we have not started it before, as seen in Chapter 5.

$ phpunit tests/functional/contact/addTest.php
PHPUnit 3.4.1 by Sebastian Bergmann.

.

Time: 10 seconds

OK (1 test, 19 assertions)
Edit a Record

The second test is related to editing a contact record.

<?php

require_once 'PHPUnit/Extensions/SeleniumTestCase.php';

class Contact_EditTest extends PHPUnit_Extensions_SeleniumTestCase
{

  function setUp()
  {
    shell_exec('mysql -u root contacts < '.dirname(__FILE__).'/../../fixtures/contacts.sql'),

    $this->setBrowser("*firefox");
    $this->setBrowserUrl("http://localhost/");
  }

  function testEdit()
  {
    $this->open("/refactoring/index.php");

    $this->click("link=Trucchia");
    $this->waitForPageToLoad("30000");

    $this->assertEquals("Contacts Book", $this->getTitle());
    $this->assertEquals("Contacts Book", $this->getText("//div[@id='header']/h1"));

    $this->assertEquals("First Name*", $this->getText("//div[@id='content']/form/label[1]"));
    $this->assertEquals("Last Name*", $this->getText("//div[@id='content']/form/label[2]"));
    $this->assertEquals("Phone*", $this->getText("//div[@id='content']/form/label[3]"));
    $this->assertEquals("Mobile", $this->getText("//div[@id='content']/form/label[4]"));

    $this->assertTrue($this->isElementPresent("firstname"));
    $this->assertTrue($this->isElementPresent("lastname"));
    $this->assertTrue($this->isElementPresent("phone"));
    $this->assertTrue($this->isElementPresent("mobile"));
    $this->assertTrue($this->isElementPresent("link=Cancel"));

    $this->assertEquals("Francesco", $this->getValue("firstname"));
    $this->assertEquals("Trucchia", $this->getValue("lastname"));
    $this->assertEquals("12345", $this->getValue("phone"));
    $this->assertEquals("234 12345", $this->getValue("mobile"));

    $this->assertEquals("(* Mandatory fields)", $this->getText("//div[@id='content']/em"));
    $this->assertEquals("All © Francesco Trucchia & Jacopo Romei - Pro PHP Refactoring", image
$this->getText("footer"));

    $this->type("firstname", "Piergiacomo");
    $this->type("phone", "1234");
    $this->click("//input[@value='Save']");
    $this->waitForPageToLoad("30000");

    $this->assertEquals("Trucchia", $this->getTable("//div[@id='content']/table.2.0"));
    $this->assertEquals("Piergiacomo", $this->getTable("//div[@id='content']/table.2.1"));
    $this->assertEquals("1234", $this->getTable("//div[@id='content']/table.2.2"));

  }
}
?>

As in the previous test in the setUp() method, we load the fixtures in the database, start the browser, and set the base path. In the testEdit() method we verify that a record is edited in the correct way.

Run the tests and verify that everything is ok.

$ phpunit tests/functional/contact/EditTest.php
PHPUnit 3.4.1 by Sebastian Bergmann.

.

Time: 9 seconds

OK (1 test, 20 assertions)
Read a List of Records

The third test is on displaying a list of contact records.

<?php

require_once 'PHPUnit/Extensions/SeleniumTestCase.php';

class Contact_ListTest extends PHPUnit_Extensions_SeleniumTestCase
{

  function setUp()
  {
    shell_exec('mysql -u root contacts < '.dirname(__FILE__).'/../../fixtures/contacts.sql'),

    $this->setBrowser("*firefox");
    $this->setBrowserUrl("http://localhost/");
  }

  function testList()
  {
    $this->open("/refactoring/index.php");
    $this->assertEquals("Contacts Book", $this->getTitle());

    $this->assertEquals("Contacts Book", $this->getText("//div[@id='header']/h1"));
    $this->assertEquals("New contact", $this->getText("link=New contact"));

    $this->assertEquals("Last Name", $this->getTable("//div[@id='content']/table.0.0"));
    $this->assertEquals("First Name", $this->getTable("//div[@id='content']/table.0.1"));
    $this->assertEquals("Phone", $this->getTable("//div[@id='content']/table.0.2"));
    $this->assertEquals("Mobile", $this->getTable("//div[@id='content']/table.0.3"));

   $this->assertEquals("Romei", $this->getTable("//div[@id='content']/table.1.0"));
    $this->assertEquals("Romei", $this->getText("link=Romei"));

    $this->assertEquals("Jacopo", $this->getTable("//div[@id='content']/table.1.1"));
    $this->assertEquals("0543123543", $this->getTable("//div[@id='content']/table.1.2"));
    $this->assertEquals("0543123543", $this->getText("link=0543123543"));
    $this->assertEquals("34012345", $this->getTable("//div[@id='content']/table.1.3"));
    $this->assertEquals("34012345", $this->getText("link=34012345"));
    $this->assertEquals("[X]", $this->getTable("//div[@id='content']/table.1.4"));
    $this->assertEquals("X", $this->getText("link=X"));

    $this->assertEquals("Trucchia", $this->getTable("//div[@id='content']/table.2.0"));
    $this->assertEquals("Trucchia", $this->getText("link=Trucchia"));
    $this->assertEquals("Francesco", $this->getTable("//div[@id='content']/table.2.1"));
    $this->assertEquals("12345", $this->getTable("//div[@id='content']/table.2.2"));
    $this->assertEquals("12345", $this->getText("link=12345"));
    $this->assertEquals("234 12345", $this->getTable("//div[@id='content']/table.2.3"));
    $this->assertEquals("234 12345", $this->getText("link=234 12345"));
    $this->assertEquals("[X]", $this->getTable("//div[@id='content']/table.2.4"));
  }
}
?>

As in the previous test in the setUp() method, we load the fixtures in the database, start the browser, and set the base path. In the testList() method, we verify that the contact list is displayed correctly.

Run the tests and verify that everything is ok.

$ phpunit tests/functional/contact/ListTest.php
PHPUnit 3.4.1 by Sebastian Bergmann.

.

Time: 9 seconds

OK (1 test, 24 assertions)
Remove a Record

This test is on removing a contact record.

<?php

require_once 'PHPUnit/Extensions/SeleniumTestCase.php';

class Contact_RemoveTest extends PHPUnit_Extensions_SeleniumTestCase
{

  function setUp()
  {
    shell_exec('mysql -u root contacts < '.dirname(__FILE__).'/../../fixtures/contacts.sql'),

    $this->setBrowser("*firefox");
    $this->setBrowserUrl("http://localhost/");
  }

  function testRemove()
  {
    $this->open("/refactoring/index.php");

    $this->assertEquals(3, $this->getXpathCount('//table/tbody/tr'));
    $this->assertEquals("Romei", $this->getTable("//div[@id='content']/table.1.0"));
    $this->assertEquals("Trucchia", $this->getTable("//div[@id='content']/table.2.0"));

    // Delete first record
    $this->click("//div[@id='content']/table/tbody/tr[2]/td[5]/a");
    $this->assertEquals('Are you sure?', $this->getConfirmation());
    $this->chooseOkOnNextConfirmation();

    $this->assertEquals(2, $this->getXpathCount('//table/tbody/tr'));
    $this->assertEquals("Trucchia", $this->getTable("//div[@id='content']/table.1.0"));

  }
}
?>

As in the previous test in the setUp() method, we load the fixtures in the database, start the browser, and set the base path. In the testRemove() method, we verify that a contact record is successfully deleted from our contact list.

Run the tests and verify that everything is ok.

$ phpunit tests/functional/contact/RemoveTest.php
PHPUnit 3.4.1 by Sebastian Bergmann.

.

Time: 8 seconds

OK (1 test, 3 assertions)
Validate a Record

This test is on validating a record when we insert a new one or modify an existing one.

<?php

require_once 'PHPUnit/Extensions/SeleniumTestCase.php';

class Contact_ValidateTest extends PHPUnit_Extensions_SeleniumTestCase
{

  function setUp()
  {
    shell_exec('mysql -u root contacts < '.dirname(__FILE__).'/../../fixtures/contacts.sql'),

    $this->setBrowser("*firefox");
    $this->setBrowserUrl("http://localhost/");
  }

  function testValidation()
  {
    $this->open("/refactoring/index.php");
    $this->click("link=New contact");
    $this->waitForPageToLoad("30000");

    $this->click("//input[@value='Save']");
    $this->waitForPageToLoad("30000");
$this->assertEquals("The firstname field is mandatory", image
$this->getText("//div[@id='content']/form/ul/li[1]"));
    $this->assertEquals("The lastname field is mandatory", image
$this->getText("//div[@id='content']/form/ul/li[2]"));
    $this->assertEquals("The phone field is mandatory", image
$this->getText("//div[@id='content']/form/ul/li[3]"));

    $this->assertTrue($this->getXpathCount("//div[@id='content']/form/ul/li") == 3);

    $this->type("firstname", "Francesco");
    $this->click("//input[@value='Save']");
    $this->waitForPageToLoad("30000");

    $this->assertEquals("The lastname field is mandatory", image
$this->getText("//div[@id='content']/form/ul/li[1]"));
    $this->assertEquals("The phone field is mandatory", image
$this->getText("//div[@id='content']/form/ul/li[2]"));

    $this->assertTrue($this->getXpathCount("//div[@id='content']/form/ul/li") == 2);
    $this->assertEquals("Francesco", $this->getValue("firstname"));

    $this->type("lastname", "Trucchia");
    $this->click("//input[@value='Save']");
    $this->waitForPageToLoad("30000");

    $this->assertEquals("The phone field is mandatory", image
$this->getText("//div[@id='content']/form/ul/li[1]"));

    $this->assertTrue($this->getXpathCount("//div[@id='content']/form/ul/li") == 1);

  }
}
?>

As in the previous test in the setUp() method, we load the fixtures in the database, start the browser, and set the base path. In the testValidate() method, we verify that the validation system works correctly when we add and modify a contact record.

Run the tests and verify that everything is ok.

$ phpunit tests/functional/contact/ValidateTest.php
PHPUnit 3.4.1 by Sebastian Bergmann.

.

Time: 9 seconds

OK (1 test, 10 assertions)
Test Refactoring

If we look at the code written so far for our tests, do we notice any familiar bad smells? Well, we would say yes, the setUp() method is the same in all tests, and duplicate code is a bad smell. Remember that the tests, both functional and unit, must be maintained like the rest of our code, so that we have simple tests that are written clearly. To improve the code of our tests written so far, we can apply two refactoring techniques, “Extract Super Class” and “Move Method.”

Through the “Extract Super Class” technique we create a new class, which we will call Contacts_TestCase, which extends PHPUnit_Extensions_SeleniumTestCase, and we move within it the setUp() method. At this point all our tests should extend the class Contacts_TestCase.

require_once 'PHPUnit/Extensions/SeleniumTestCase.php';

class ContactsTestCase extends PHPUnit_Extensions_SeleniumTestCase
{
  function setUp()
  {
    shell_exec('mysql -u root contacts < '.dirname(__FILE__).'/../fixtures/contacts.sql'),

    $this->setBrowser("*firefox");
    $this->setBrowserUrl("http://localhost/");
  }
}

...

class Contact_AddTest extends ContactsTestCase {...}

...

class Contact_EditTest extends ContactsTestCase {...}

...

class Contact_ListTest extends ContactsTestCase {...}

...

class Contact_RemoveTest extends ContactsTestCase{...}

...

class Contact_ValidateTest extends ContactsTestCase {...}

In this way we will not have duplicated code, and it will be easier to maintain our functional tests. Run the tests again and check that everything is still correct.

$ phpunit tests/functional/contact/
PHPUnit 3.4.1 by Sebastian Bergmann.

.....

Time: 45 seconds

OK (5 tests, 76 assertions)
Unify Test Cases in a Suite

We added all the tests for the contact entity. Now we can group them into a suite test, so we can organize them in the best way. We create a suite.php file inside the functional folder with the following class:

<?php
require_once('AddTest.php'),
require_once('EditTest.php'),
require_once('ListTest.php'),
require_once('RemoveTest.php'),
require_once('ValidateTest.php'),

class Contact_Suite extends PHPUnit_Framework_TestSuite
{
  public static function suite()
  {
    $suite = new PHPUnit_Framework_TestSuite('Contact'),
    $suite->addTestSuite('Contact_AddTest'),
    $suite->addTestSuite('Contact_EditTest'),
    $suite->addTestSuite('Contact_ListTest'),
    $suite->addTestSuite('Contact_RemoveTest'),
    $suite->addTestSuite('Contact_ValidateTest'),

    return $suite;
  }
}
?>

Now we can use this suite class to run tests.

Summary

In this chapter we have begun the process of “big refactoring” of an old PHP application in order to add new features without losing the value of the application itself. We learned how to cage our application in functional regression tests so that we can detect bad changes in our refactoring work when they happen.

In the next chapter we'll learn how to improve the quality of procedural code with techniques of refactoring according to patterns.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.173.78