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.
After completing this lesson, you should be able to:
In this unit, you will learn about the following topics:
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
}
}
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:
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
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:
Refactor slowly. Make changes, create and run tests, then move on to the next change.
Never refactor without tests to back up the refactor.
Watch for constructors doing a lot of work. Use dependency injection to replace that work. This has the added benefit of creating a seam for testing in addition to reducing the coupling of the code.
Focus on test coverage. It's not always possible to test certain code areas without significant alterations of the code base. Always question if refactoring is worth the possibility of completely breaking the rest of the build.
Focus on critical functionality. Ensure that the most important parts work. Writing tests for code built on untested code is no guarantee since we do not even know if the core functions as expected.
In this walkthrough you will perform the following tasks:
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.
Open CircleLayout.as in the net.digitalprimates.components.layout package.
Examine the updateDisplayList()
method in this class.
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.
At the top of the class, create a private circle property.
private var circle:Circle;
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;
}
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();
}
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 ) );
Remove the getLayoutRadius()
method from the class.
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.
In this walkthrough you will perform the following tasks:
distanceFrom()
to the Circle
class.Circle
classes' equals()
method to apply the distanceFrom()
method.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.
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 {
}
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 );
}
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.
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.
Save the Circle.as file
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.
Open the FlexUnit4Training.mxml file.
Run the FlexUnit4Training.mxml file.
Code that tests easily is
Highly cohesive
Loosely coupled
Refactoring code to become testable
Generally, creates better code
Adds necessary seams in the code
Favors simplicity
Testing legacy code should be approached with caution.
Do not refactor without tests in place to backup
Black box classes need to make their properties and methods available for testing.
Minimal inter method/class/property dependencies
Public getter and setter functions