Unit 15 - Creating Testable Code

Download Download Unit Project Files

The ability to thoroughly test a class or component is an indication of good design. In situations where a component has unexposed functionality, it can be very difficult to write tests that ensure the component functions as expected. Refactoring a class for better unit testing is therefore great for quality assurance as well as coding practice.

Objectives:

After completing this lesson, you should be able to:

Topics

In this unit, you will learn about the following topics:

Why highly cohesive code tends to test more easily

Highly cohesive code creates strong test fixtures and short, simple tests. A class with low cohesion will frequently have us begging the question, "does this belong in the fixture?" If there is a section of code that should be in the fixture but doing so will create unnecessary overhead for some of the tests we are likely looking at a case of low cohesion. Always remember that a class with high cohesion tends to create tests with high cohesion.

Consider the following database example:

public class ContactDatabaseConnection {
    public function ContactDatabaseConnection () {
    }

public function connect():void {
    //code to connect
}

public function getContact( itemName:String ):Contact {
    //code to retrieve contact
}

public function formatContact( contact:Contact ):FormattedContact {
    //code to format contact
}

}

Try to imagine the fixture for this interface. The test fixture would need to create a new database connection. It would also need to create a new contact for testing.

private var testContact:Contact;
private var connection:ContactDatabaseConnection;

[Before]
public function setup():void {
    connection = new ContactDatabaseConnection();
    connection.connect();
    testContact = new Contact();

}

However, the formatContact() method does not need a database connection. In fact, what does formatting a contact have to do with a ContactDatabaseConnection? These methods do not have a unified test fixture. In one section we would be testing the connect and retrieval functions. The other requires a contact be ready made. Clearly, formatContact() does not belong.

If you find yourself needing a unique fixture for each test in a class it is highly likely you are dealing with a class that contains low cohesion or only temporal cohesion at best. In this situation, you might want to return to the code and find a way to split the class to increase the cohesion.

Here is an improved version of the ContactDatabaseConnection class that should be much easier to test and maintain:

public class ContactDatabaseConnection {
    public function ContactDatabaseConnection () {
    }

public function connect():void {
    //code to connect
}

public function getContact( itemName:String ):IContact {
    //code to retrieve contact
}

}

And moving the formatContact method into the specific Contact implementation:

public class Contact implements IContact {
    //contact code

public function formatContact():void {
    //format code
}

}

Why loosely coupled code can be tested less painfully

By creating loosely coupled code, we create classes that can more easily be tested in isolation. Tightly coupled code creates a dependency on outside resources that frequently do not belong in the class with which they are coupled.

Consider the following:

public class BoxOfCrayons() {
    private var crayons:Array = new Array();

public function addCrayon( color:String ):void {
    var crayon = new WaxCrayon();
    crayon.color = color;
    crayon.wrapper.color = color;
}

}

Think of the tests you would need to write for the addCrayon() method:

This section of code is tightly coupled for a few reasons. First, BoxOfCrayons is making its own crayons. What if BoxOfCrayons can also contain ShortCrayon or ClayCrayon? Even worse, why is BoxOfCrayons modifying crayon wrappers at all? addCrayon() is now tightly coupled to this exact implementation of WaxCrayon. What if we decide that WaxCrayons do not have wrappers? You would also need to change the implementation of addCrayon().

Tightly coupled code:

Refactoring an un-testable piece of code to something testable

There is no simple answer on the best way to refactor an untestable object. For some, it may be as simple as using a getter to expose a previously hidden property. For others, where objects are tightly coupled, may require a complete rewrite. The testability of a piece of code depends entirely on its available seams. In code without seams, one must rely on any "magic" performed behind the scenes. If what you put in does not yield what you expected you have no way of knowing where everything went wrong.

When refactoring, ask yourself these questions:

If you answer "no" to any of these questions, that might be a good place to start refactoring

Creating tests for legacy code

Legacy code tests tend to be the most difficult to test. Frequently the original code is not written with any testing in mind and may require significant refactoring. Refactoring the code to support functionality can potentially break other sections of code creating a veritable cornucopia of bugs.

When refactoring legacy code, keep the following in mind:

Walkthrough 1: Refactoring an object to become testable

In this walkthrough you will perform the following tasks:

Steps

  1. Import the FlexUnit4Training_wt1.fxp project from the Unit 15/Start folder. Please refer to Unit 2: Walkthrough 1 for instructions on importing a Flash Builder project.

  2. Open CircleLayout.as in the net.digitalprimates.components.layout package.

  3. Examine the updateDisplayList() method in this class.

    override public function updateDisplayList( contentWidth:Number, contentHeight:Number ):void {
    
        ...                         
        super.updateDisplayList( contentWidth, contentHeight );
    
        var circle:Circle = new Circle( new Point( contentWidth/2, contentHeight/2 ),
         getLayoutRadius( contentWidth, contentHeight ) );
    
        for ( i = 0; i < target.numElements; i++ ) {
            element = target.getElementAt(i);
    
            elementLayoutPoint = circle.getPointOnCircle( elementAngleInRadians );
            ...         
            //Used to be move()
            element.setLayoutBoundsPosition( elementLayoutPoint.x-elementWidth,
             elementLayoutPoint.y-elementHeight );
    
        }
    }

    This method creates a circle object of type Circle with dimensions based on the contentWidth and contentHeight arguments. The elements of the layout are positioned to points on the circle.

    Suppose you wanted to test the functionality of this method. It would be impossible to isolate because the method is dependent on the Circle class.

    The circle object could be mocked if the CircleLayout class had its own circle property instead.

  4. At the top of the class, create a private circle property.

    private var circle:Circle;
  5. Generate a public getter and setter for the property. Right-click the property, select Source > Generate Getter/Setter, and click OK.

    public function get circle():Circle {
        return _circle;
    }
    
    public function set circle(value:Circle):void {
        _circle = value;
    }
  6. Add a call to the invalidateDisplayList() method on the target property within the setter of the circle.

    public function set circle(value:Circle):void {
        circle = value;
        target.invalidateDisplayList();
    }
  7. Remove the line that instantiates the circle object within the updateDisplayList() method.

    var circle:Circle = new Circle( new Point( contentWidth/2, contentHeight/2 ), getLayoutRadius( contentWidth, contentHeight ) );
  8. Remove the getLayoutRadius() method from the class.

  9. Perform a null check for the circle within updateDisplayList() method.

    super.updateDisplayList( contentWidth, contentHeight );
    
    if(circle) {
        for ( i = 0; i < target.numElements; i++ ) {
            ...
        }
    }

    The CircleLayout class can now be better tested as it is not dependent on another class.

Walkthrough 2: Refactoring an object to become testable

In this walkthrough you will perform the following tasks:

Steps

  1. Open the Circle.as file from the net.digitalprimates.math package.

    Alternatively, if you didn't complete the previous lesson or your code is not functioning properly, you can import the FlexUnit4Training_wt2.fxp project from the Unit 15/Start folder. Please refer to Unit 2: Walkthrough 1 for instructions on importing a Flash Builder project.


    Add the distanceFrom() method

  2. Just below the equals() method, add a new public method named distanceFrom(). It should return a value of data type Number. It should take a parameter named circle of type Circle.

    public function distanceFrom( circle:Circle ):Number {
    }
  3. The new method should return Point.distance( this.origin, circle.origin ), which calculates the distance between the Circles' origin points.

    public function distanceFrom( circle:Circle ):Number {
        return Point.distance( this.origin, circle.origin );
    }


    Modify the equals() method

    The equals() method in Circle may appear simple enough, but can be difficult to test. This comparison is based on radius and origin. Although the radius comparison is about as simple as it could be, the point comparison is based on the accessing the origin.x and origin.y and asserting their equality separately. If you were to isolate and mock the circle for testing, you would have to mock the Point object as well.

    public function equals( circle:Circle ):Boolean {
        var equal:Boolean = false;
    
        if ( ( circle ) && ( this.radius == circle.radius ) && ( this.origin ) &&
         ( circle.origin ) ) {
            if ( ( this.origin.x == circle.origin.x ) && ( this.origin.y == circle.origin.y ) ) {
                equal = true;
            }
        }
    
        return equal;
    }

    Creating an isolated object for testing will ultimately reduce these interdependencies. Mocks should be able to work without the required input of additional mocks. Creating code of this kind will add seams, loosen coupling, and improve quality over all.

  4. In the equals() method, replace the line of the second if statement, which checks the equality of this and the comparison circle's origin's x and y values, with a line that checks that this.distanceFrom( circle ) is equal to 0.

    if ( ( this.origin.x == circle.origin.x ) &&
    ( this.origin.y == circle.origin.y ) ) {
        equal = true;
    }

    Becomes:

    if ( this.distanceFrom( circle ) == 0 ) {
        equal = true;
    }

    The Circle class's new equals() method should read as follows:

    public function equals( circle:Circle ):Boolean {
        var equal:Boolean = false;
    
        if ( ( circle ) && ( this.radius == circle.radius ) && 
        ( this.origin ) && ( circle.origin ) )  {
            if ( this.distanceFrom( circle ) == 0 ) {
                equal = true;
            }
        }
    
        return equal;
    }

    At this point, if you were to mock a Circle, you would only need to mock and create expectations for a circle object. A mock for the Point class is not needed here.

  5. Save the Circle.as file


    Run the unit tests

    This is a great example of why automated unit testing is so beneficial in the development life cycle. In your modifications of the Circle.as class, it's very possible that you could have broken some functionality in the class and elsewhere. Fortunately, there are a wealth of unit test cases, many of them dealing with the Circle class and even the equals() method.

  6. Open the FlexUnit4Training.mxml file.

  7. Run the FlexUnit4Training.mxml file.

Summary

Navigation