Programmer Tests: Catalog Class


Programmer Tests: Catalog Class

In the previous section, we made the gateways transaction-capable, and the transaction boundary was controlled by the test fixtures. The Catalog class has a dual purpose. On one hand, interface methods of the Catalog class need to manage the transactional boundaries of the application when that class is used by the rest of the application. On the other hand, the Catalog class interface methods need to participate in the transactions managed by programmer tests when the class is being tested . This behavior is similar to what would be achieved by using the transactional serviced components in COM+ that have a Required transaction attribute.

During the execution of the application, the COM+ Enterprise Services run time would intercept all the interface calls to the serviced components, check their transactional attribute, and begin the new transaction on the component s behalf if there is no transaction in progress. Because we are not using the automatic transaction management provided by Enterprise Services (for various reasons), we have to develop a similar mechanism for the application.

Let s start with the CatalogFixture class. This class needs to inherit from DatabaseFixture instead of ConnectionFixture , rename the SetUp method to Insert , and remove the TearDown method. The changes are highlighted here:

 [TestFixture] public class CatalogFixture :  DatabaseFixture  {    // member variables are the same  public override void Insert()   {  // same as previous  }  // tests are the same } 

When we compile and run these tests, they all fail with the same error we got when we ran the ReviewUpdateFixture . We have to make the Catalog class transaction-enabled to fix these failures. We will begin with the FindByRecordingId method. Here is the existing version of this method:

 public static RecordingDataSet.Recording FindByRecordingId(RecordingDataSet recordingDataSet, long recordingId) {    SqlConnection connection = null;    RecordingDataSet.Recording recording = null;    try    {       connection = new SqlConnection(ConfigurationSettings.AppSettings.Get("Catalog.Connection"));          connection.Open();          RecordingGateway recordingGateway =                 new RecordingGateway(connection);          recording = recordingGateway.FindById(recordingId,                 recordingDataSet);       if(recording != null)       {          long artistId = recording.ArtistId;          ArtistGateway artistGateway = new ArtistGateway(connection);          RecordingDataSet.Artist artist =              artistGateway.FindById(artistId, recordingDataSet);          long labelId = recording.LabelId;          LabelGateway labelGateway = new LabelGateway(connection);          RecordingDataSet.Label label =              labelGateway.FindById(labelId, recordingDataSet);          GenreGateway genreGateway = new GenreGateway(connection);          TrackGateway trackGateway = new TrackGateway(connection);          foreach(RecordingDataSet.Track track in           trackGateway.FindByRecordingId(recordingId,           recordingDataSet))          {             artistId = track.ArtistId;             long genreId = track.GenreId;             artist = artistGateway.FindById(artistId,                 recordingDataSet);             RecordingDataSet.Genre genre =                genreGateway.FindById(genreId, recordingDataSet);          }          ReviewGateway reviewGateway = new ReviewGateway(connection);           ReviewerGateway reviewerGateway = new              ReviewerGateway(connection);          foreach(RecordingDataSet.Review review in                                 reviewGateway.FindByRecordingId(recordingId,                 recordingDataSet))          {             long reviewerId = review.ReviewerId;             RecordingDataSet.Reviewer reviewer =                 reviewerGateway.FindById(reviewerId,                    recordingDataSet);          }       }    }    finally    {       if(connection != null)          connection.Close();    }    return recording; } 

Here is some pseudo-code that describes how we want the method to behave:

 if(transaction is in progress)    {       execute application code    }    else    {       connect to the database       start a transaction       execute application code       if(successful)          commit the transaction       else          rollback the transaction       close the connection to the database    } 

This pseudo-code indicates a pretty large change to the existing code, and we will have to repeat it for every single method of the Catalog class! (Also note that the application logic code block will have to be called from two places, too. This looks pretty nasty!) Instead of blindly doing this, let s look for a more general solution.

Refactoring the Catalog class

What we need is a mechanism to handle transactional aspects of the method invocation separately from the execution of the application code.

There are several approaches we could do to achieve this. For example, we could rely on code generation to create an implementation class, say CatalogSkeleton , which would inherit from the Catalog class and would override all the methods to include the transaction management code. We want to make the methods on the Catalog class nonstatic and virtual to support this option. We could write a proxy class, say CatalogProxy , which would have the same methods as the Catalog class, and would use reflection-based method delegation and add transaction management code. All these approaches sound overly complicated, and one of the consequences of complexity is that they would be equally hard to understand. Let s look for a more explicit solution that will also give us some flexibility in testing. Let s focus first on the code that manages the transaction boundary. We will encapsulate the code that manages the transactional boundary in a class named CommandExecutor . Let s start with the tests that capture the requirements for the CommandExecutor class. We need to verify that the application code is executed. Here is the first test:

 [TestFixture] public class CommandExecutorFixture : ConnectionFixture {    private CommandExecutor commandExecutor =        new CommandExecutor();    private class ExecuteCommand : Command    {       internal int executeCount = 0;         public void Execute()       {          executeCount++;       }    }    [Test]    public void RunOnce()    {       ExecuteCommand command = new ExecuteCommand();       commandExecutor.Execute(command);       Assert.AreEqual(1, command.executeCount);    } } 

In this test, we introduce a couple of new classes. The first class is the CommandExecutor , which will eventually be responsible for running our application code within the context of a transaction. We also introduce an interface called Command , which is shown in the following code:

 namespace DataAccess {    public interface Command    {       void Execute();    } } 

We use Command (see Design Patterns , by Erich Gamma et al, Addison- Wesley, 1995) to encapsulate the application-specific code. The CommandExecutor can then execute the command within the context of a transaction. The Command interface also allows us to better test the CommandExecutor . If you look at the CommandExecutorFixture , you see that we wrote a class named ExecuteCommand , which allows us to verify that the Execute method was called when it is run inside of the CommandExecutor . When we run the test, it passes , so let s move on to the more interesting parts such as the transaction boundary.

The next test will verify that when the CommandExecutor is called, it will start a transaction. Here is the test:

 private class TransactionCheckCommand : Command    {       internal SqlTransaction transaction;       public void Execute()       {          transaction = TransactionManager.Transaction();       }    }        [Test]    public void StartTransaction()    {       TransactionCheckCommand command =           new TransactionCheckCommand();       commandExecutor.Execute(command);       Assert.IsNotNull(command.transaction);       Assert.IsNull(TransactionManager.Transaction());    } 

This test uses another test command class named TransactionCheckCommand . All it does is record the transaction when the Execute method is called. When we run this test, it fails because the command.transaction is equal to null . This is not surprising because we have not put any database code (or transaction management code, for that matter) in the Execute method. Let s do that now.

 public void Execute(Command command) {    bool isTransactionInProgress =        (TransactionManager.Transaction() != null);              if(!isTransactionInProgress)    {       SqlConnection connection = new SqlConnection(ConfigurationSettings.AppSettings.Get("Catalog.Connection"));       connection.Open();       TransactionManager.Begin(connection);       command.Execute();       TransactionManager.Commit();       connection.Close();    } } 

In this code, we open a connection to the database, start a transaction using the TransactionManager , execute the command in the context of the transaction, commit the transaction, and close the connection. When we run the tests in CommandExecutorFixture they all pass. Let s move on to the other scenario, in which the CommandExecutor needs to participate in a transaction that is already in progress. Here is the test:

 [Test] public void ParticipateInTransaction() {    SqlTransaction myTransaction = TransactionManager.Begin(Connection);    TransactionCheckCommand command = new TransactionCheckCommand();    commandExecutor.Execute(command);    Assert.AreSame(myTransaction, command.transaction);    TransactionManager.Rollback(); } 

In this test, we start the transaction in the test and verify that the CommandExecutor uses the transaction. When we run the test, it fails. Let s fix the CommandExecutor to participate in transactions (the changes are in boldface code):

 public void Execute(Command command) {    bool isTransactionInProgress =        (TransactionManager.Transaction() != null);  if(isTransactionInProgress)   {   command.Execute();   }  else    {       SqlConnection connection = new SqlConnection(ConfigurationSettings.AppSettings.Get("Catalog.Connection"));       connection.Open();       TransactionManager.Begin(connection);       command.Execute();       TransactionManager.Commit();       connection.Close();    } } 

All we had to do in the participation case is call the command.Execute method. When we run the tests, they all pass.

The last test that we will show for the CommandExecutor is what should happen if the Execute method throws an exception. In this case, the CommandExecutor should catch the exception, call the TransactionManager.Rollback to return the database to its previous state, and then rethrow the same exception so the calling program knows which exception was thrown. Here is the test in the CommandExecutorFixture :

 private class ExceptionThrowingCommand : Command    {       public void Execute()       {          throw new InvalidOperationException();       }    }           [Test]    [ExpectedException(typeof(InvalidOperationException))]    public void ThrowCommand()    {       ExceptionThrowingCommand command = new ExceptionThrowingCommand();       commandExecutor.Execute(command);    } 

Let s look at the implementation of the CommandExecutor.Execute method that s needed to support this test. The changes are boldface:

 public void Execute(Command command) {    bool isTransactionInProgress =        (TransactionManager.Transaction() != null);              if(isTransactionInProgress)    {       command.Execute();    }    else    {       SqlConnection connection = new SqlConnection(ConfigurationSettings.AppSettings.Get("Catalog.Connection"));       connection.Open();       TransactionManager.Begin(connection);  try   {  command.Execute();          TransactionManager.Commit();  }   catch(Exception exception)   {   TransactionManager.Rollback();   throw exception;   }   finally   {   connection.Close();   }  } } 

We had to do a little work on the code to catch the exception, call Transaction.Rollback , and then rethrow the exception. We also had to ensure that we closed the connection to the database in either case. When we run the tests, they all pass as expected. There are a number of tests that need to be added to deal with the database- related exceptions, but we leave them as an exercise for the reader.

Now that we have implemented the generic CommandExecutor for the transactional execution of Catalog class interface operations, we are ready to migrate the old code to the new execution strategy. Every interface method of the Catalog class will get converted following the same process described as follows :

  • Create a private class in the Catalog class for the operation being performed (for example, FindByRecordingIdCommand ).

  • For each parameter of the original interface method, define an instance variable on the Command class and add it to the signature of the constructor.

  • Define a property that will hold the return value of the original interface method.

  • Define an Execute method that will have the same code as the original interface method.

  • Replace the implementation of the original interface method with the construction and invocation of the corresponding command and use the CommandExecutor .

Here is the updated Catalog class for the FindByRecordingId method:

 public class Catalog    {       private static CommandExecutor commandExecutor =           new CommandExecutor();       private class FindByRecordingIdCommand : Command       {          internal long recordingId;          internal RecordingDataSet recordingDataSet;          internal RecordingDataSet.Recording recording;          public FindByRecordingIdCommand(RecordingDataSet              recordingDataSet, long recordingId)          {             this.recordingDataSet = recordingDataSet;             this.recordingId = recordingId;          }          public void Execute()          {             SqlConnection connection =              TransactionManager.Transaction().Connection;             RecordingGateway recordingGateway =                 new RecordingGateway(connection);             recording =                 recordingGateway.FindById(recordingId,                    recordingDataSet);                             if(recording == null) return;             long artistId = recording.ArtistId;             ArtistGateway artistGateway = new                 ArtistGateway(connection);             RecordingDataSet.Artist artist =                 artistGateway.FindById(artistId,                    recordingDataSet);             long labelId = recording.LabelId;             LabelGateway labelGateway = new                 LabelGateway(connection);             RecordingDataSet.Label label =                 labelGateway.FindById(labelId,                    recordingDataSet);             GenreGateway genreGateway = new                 GenreGateway(connection);                             TrackGateway trackGateway = new                 TrackGateway(connection);             foreach(RecordingDataSet.Track track in                 trackGateway.FindByRecordingId(recordingId,                    recordingDataSet))             {                artistId = track.ArtistId;                long genreId = track.GenreId;                artist = artistGateway.FindById(artistId,                    recordingDataSet);                RecordingDataSet.Genre genre =                    genreGateway.FindById(genreId,                       recordingDataSet);             }             ReviewGateway reviewGateway = new              ReviewGateway(connection);             ReviewerGateway reviewerGateway = new                 ReviewerGateway(connection);             foreach(RecordingDataSet.Review review in                 reviewGateway.FindByRecordingId(recordingId,                    recordingDataSet))             {                long reviewerId = review.ReviewerId;                RecordingDataSet.Reviewer reviewer =                    reviewerGateway.FindById(reviewerId,                       recordingDataSet);             }          }          public RecordingDataSet.Recording Result          {             get              {                return recording;             }          }       }       public static RecordingDataSet.Recording FindByRecordingId(RecordingDataSet recordingDataSet, long recordingId)       {          FindByRecordingIdCommand command =              new FindByRecordingIdCommand(recordingDataSet,              recordingId);          commandExecutor.Execute(command);          return command.Result;       }    } 

After this work was completed, we ran the tests in the CatalogFixture and they passed. We then turned our attention to the AddReview and DeleteReview methods and followed exactly the same process. When we completed it, we ran all the tests and they passed. We removed the [Ignore] attribute from the ReviewUpdateFixture , ran those tests, and they passed as well. The last thing we did was run the customer tests, and they also passed. Having all the tests pass indicates that even though we made significant changes to the underlying implementation of the database access code, the application still works the same way as it did prior to these changes.




Test-Driven Development in Microsoft .NET
Test-Driven Development in Microsoft .NET (Microsoft Professional)
ISBN: 0735619484
EAN: 2147483647
Year: 2004
Pages: 85

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net