CI Tools and Best Practices in the Cloud

Continuous Integration

Subscribe to Continuous Integration: eMailAlertsEmail Alerts newslettersWeekly Newsletters
Get Continuous Integration: homepageHomepage mobileMobile rssRSS facebookFacebook twitterTwitter linkedinLinkedIn


Continuous Integration Authors: Elizabeth White, Yeshim Deniz, Pat Romanski, Liz McMillan, Mehdi Daoudi

Related Topics: Continuous Integration

Continuous Integration: Article

Test-Driven Development, Refactoring, and Continuous Integration

These days, you're nothing if you're not agile

Project success depends heavily on a team's ability to quickly incorporate new requirements and deliver solid results. Although most organizations have an appetite for the benefits that can be realized using extreme programming, many cannot commit to a methodology that minimizes upfront documentation and design and promotes pair programming. Whichever methodology you choose for your next project, refactoring, test-driven development, and continuous integration should be a part of it. When these three key principles of XP are incorporated into your methodology, you are providing a process that will minimize the occurrence of bugs, promote clean code, and position your project for agile growth.

Test-Driven Development
Creating a solid application requires a commitment to testing - user acceptance tests, system tests, and unit tests. All have to provide assurance and peace of mind to various stakeholders involved with the development of the application. Generally, each is performed when the discrete component of the application has been completed. Unit tests provide a promise that the code developed will perform as expected. Test-driven development (TDD) takes the unique approach of having the developer focus on coding the tests before the classes to which they will belong. It's strange, but it works.

Let's work through an example. We will use NUnit, a testing framework that can be used with all .NET languages. With NUnit, the test case is called a test fixture. It is simply a class with certain attributes decorating it. Each test fixture has a number of tests that are simply methods that have been decorated with the Test attribute.

Imagine that your current project requires you to develop a class that represents a telephone. According to TDD, the first step will be to define the test case. Because the class we will be building is Telephone, the test fixture will be named TelephoneTest. It will be responsible for testing anything that could break on the telephone class.

01 [TestFixture]
02 public class TelephoneTest {
03 }

Next, we need the Telephone class. It looks like this:

01 public class Telephone {
02 }

The Telephone class is now ready to provide some functionality. However, because we are following the principle of TDD, we must write the tests first. Our Telephone class will have two states, connected and disconnected. We will have a LiftReceiver method that returns true to indicate the state of connected and false for disconnected.


01 [TestFixture]
02 Public class TelephoneTest {
03   [Test]
04   public void TestReadyState() {
05     Telephone t = new Telephone("yes");
06     bool dtExists = t.LiftReceiver();
07
08     Assert.IsTrue(dtExists, "There is no dialtone available.");
09   }
10   [Test]
11   public void TestDisconnectedState() {
12     Telephone t = new Telephone("no");
13     bool dtExists = t.LiftReceiver();
14
15     Assert.IsFalse(dtExists, "There should be no dialtone.");
16   }
17 }

Lines 8 and 15 are where the actual tests occur. Line 8 says, if dtExists is true, then continue on. If dtExists is false, fail this test, and display the message. Similarly, line 15 says, if dtExists is false, then this test passes, but if dtExists is false, fail the test and display its message.

Now that we have the first two unit tests written, we can code the functionality of the Telephone class. It will look like this:


01 public class Telephone {
02   private bool isConn = false;
03
04   public Telephone(string state) {
05     if (state.Equals("yes")
06       this.isConn = true;
07     else
08       this.isConn = false;
09   }
10
11   public bool LiftReceiver() {
12     return true;
13   }
14 }

When we run the test cases, we quickly find that TestReadyState() passes the assertion, because LiftReceiver() returns true. But in TestDisconnectedState(), the assertion fails because the test was expecting false to be returned. As a developer you should be happy because this shows you two things: First, the test for a connected state passed, so you don't have to worry about it anymore. You have high confidence that it returned exactly what you expected. Second, you should be happy about the failure of the disconnected state test because you found out about it early enough in the development cycle to fix it with little cost. In TDD, if your tests fail, you find out why and fix it. So, let's fix it. We know that LiftReceiver() shouldn't always return true. For now, let's just say that if the telephone is connected, you get a dial tone and if not, you don't. LiftReceiver() should now look like this:

00 ...
11 public bool LiftReceiver() {
12 return isConn;
13 }
14 ...

When we rerun the test cases, everything passes. Excellent!

The basic steps involved in TDD are:

  • Think about the problem domain
  • Code some tests for the problem domain
  • Code the domain
  • Run the tests - if they pass, go to the last step
  • Modify domain, and go back to the previous step
  • Add the test fixtures and domain classes to the source code repository
I have found that the hardest part of TDD is convincing team members that it works and that they need to diligently follow the process. The developers that swear by it say that it helps them quickly formulate their solution to the problem domain and develop test cases that provide better overall coverage. I wholeheartedly agree.

Refactoring
During the evolution of our projects, we should be cognizant of the complexity of code and what it means in terms of continued development and maintenance. If code is thrown together haphazardly, into what has been coined "spaghetti code," you can expect more frequent occurrences of bugs, increased time during development, and a nightmare during maintenance. There is also a strong possibility that whoever is working on the project isn't going to be as happy as they could be, and this will likely result in the contribution of more spaghetti code.

There is a line from my favorite movie "Glengarry Glen Ross" that I have adapted and say to myself while coding: "A.B.R...A - Always. B - Be. R - Refactoring. Always be refactoring!" As defined by Martin Fowler, "Refactoring is the process of changing a software system in such a way that it does not alter the external behavior of the code yet improves its internal structure." I like to call it "cleaning up the code." Let's see if we can improve on the previous examples by "cleaning up" the code. By doing this, we can also see how the unit tests provide assurance that the refactorings are not going to break the project.

Personally, I really dislike seeing blocks of logic in the constructor of Telephone. I would much rather see a single method that encapsulates exactly what is being performed when that constructor is called. A simple refactoring technique commonly referred to as "Extract Method" is used. The basic steps are as follows:

  • Create a new method with the appropriate signature
  • Copy the code from the source and put it into the new method
  • Compile
  • Replace original source code with method call
Our resulting Telephone class now looks like this:


01 public class Telephone {
02   private bool isConn = false;
03
04   public Telephone(string state) {
05     SetconnectionStatus(state);
06   }
07
08   public bool LiftReceiver() {
09     return isConn;
10   }
11
12   private void SetConnectionStatus(string state) {
13     if (state.Equals("yes")
14       this.isConn = true;
15     else
16       this.isConn = false;
17   }
18 }

As you can see, the code is cleaner and self-documenting. The constructor now conveys a sense of what is happening as opposed to how it's doing it. The SetConnectionStatus() has a method name that describes exactly what functionality it provides to the class. The source code certainly reads better. However, can we be sure that the code we just modified won't negatively affect other code that is dependent on it? Absolutely, because we have already written the unit tests and we can simply execute them and determine if the code is negatively affected. When the tests succeed, we can be confident that our refactoring has been a success. If they fail, we find out why and fix the problem. The example can be refactored again. Let's now clean up SetConnectionStatus() by in-lining the if/else statement.

00 ...
12 private void SetConnectionStatus(string state) {
13 return state.equals("yes");
14 }
15 ...

The code is now cleaner, clearer, and easier to maintain. Once we run the tests on it, we can verify that the refactoring was a success.

There are hundreds of ways of refactoring code, and many of the most common have been excellently documented in Martin Fowler's book Refactoring: Improving the Design of Existing Code.

Continuous Integration
So far, we have discussed how unit testing can help us deliver code with a high level of confidence and how refactoring can help improve the design of our code. Continuous integration (CI) provides us with a means of ensuring that applications developed by a team will come together correctly. CI is a process of automating the application builds, often several times a day. By continuously building the application and running the unit tests, a team can have a high level of confidence that the code being developed independently by different team members can work together as a cohesive unit. Automating this process significantly reduces the amount of time a team must spend reconciling issues of integration. Also, by having the builds and tests executed several times a day, integration bugs are identified early in the development process. You likely won't get all of them, but you'll get most of them.

Integration in a team environment generally follows these steps:

  • Developer checks out a copy of the source
  • Developer modifies/adds code and writes unit tests
  • Developer adds modifications to source code repository
  • At scheduled intervals, the CI server checks the source code repository for modifications
  • If modifications do not exist, wait for the next scheduled interval
  • If modifications exist, get a fresh copy of the code, build, and test
  • If errors occurred in the build, notify the whole team that the build is broken
  • The team identifies why the build broke, and fixes the problem, and possibly writes more tests
  • Code is then added to the source code repository
See Figure 1 for a detailed illustration of these steps.

The upfront effort required to implement a CI server on a project is minuscule in comparison to the time that can be wasted trying to integrate team members' code. Another benefit of CI is the accountability it places on each developer. Once an error is identified in the build, everyone on the team knows about it, and there is a good chance it was the last person who added to the source repository who broke the build. No one wants to be "that" guy!

There are several CI tools available, the most popular being CruiseControl.NET (CC.Net). It runs as a service and has the ability to integrate nicely with many of the most frequently used source control tools, such as CVS, Subversion, and Visual SourceSafe. It provides an HTML view of the current build as well as the previous build, so teams have a common means of examining project status over time.

Conclusion
Test-driven development, refactoring, and continuous integration allow developers to have a higher degree of confidence in the code they develop. TDD forces us to think in terms of what we are coding and the exceptions that can occur before we actually write a single line of the domain. Refactoring allows us to continuously improve the design of our code in a way that ensures a clean, clear, and maintainable future for the application. Continuous integration provides a means of ensuring that the code developed as a team will work as expected, and minimizes the time spent consolidating and testing the application. Although your customer might not agree to a full XP methodology, using these three key principles of XP can bring a certain level of agility to your project.

Comments (0)

Share your thoughts on this story.

Add your comment
You must be signed in to add a comment. Sign-in | Register

In accordance with our Comment Policy, we encourage comments that are on topic, relevant and to-the-point. We will remove comments that include profanity, personal attacks, racial slurs, threats of violence, or other inappropriate material that violates our Terms and Conditions, and will block users who make repeated violations. We ask all readers to expect diversity of opinion and to treat one another with dignity and respect.