Programmer Tests


Let s outline the approach we want to take with the integration of transactions into the programmer tests. The existing tests for database access follow a similar pattern:

  • SetUp creates the persistent data in the database

  • Test manipulates the persistent data

  • TearDown restores the database to the initial state

However, there are several tests that are not as simple. This is due in part to the need to delete the data that is inserted into the database as part of the test. Here is an example from the ReviewUpdateFixture :

 [Test]    public void AddTwoReviewsWithExistingReviewer()    {       int rating = 1;       string content = "Review content";       ReviewerGateway reviewerGateway =           new ReviewerGateway(Connection);       long reviewerId =           reviewerGateway.Insert(recordingDataSet, reviewerName);       RecordingDataSet.Reviewer reviewer =           reviewerGateway.FindById(reviewerId, recordingDataSet);       RecordingDataSet.Review reviewOne =           Catalog.AddReview(recordingDataSet, reviewerName,           content, rating, Recording.Id);       try       {          RecordingDataSet.Review reviewTwo =              Catalog.AddReview(recordingDataSet,              reviewerName, content, rating, Recording.Id);          Assert.Fail("Expected an Exception");       }       catch(ExistingReviewException exception)       {          Assert.AreEqual(reviewOne.Id, exception.ExistingId);       }             finally       {          RecordingDataSet dbDataSet = new RecordingDataSet();          RecordingDataSet.Recording dbRecording =              Catalog.FindByRecordingId(dbDataSet,Recording.Id);          RecordingDataSet.Review[] reviews =  dbRecording.GetReviews();          ReviewGateway reviewGateway = new ReviewGateway(Connection);          foreach(RecordingDataSet.Review existingReview in reviews)          {             reviewGateway.Delete(dbDataSet, existingReview.Id);           }          reviewerGateway.Delete(recordingDataSet, reviewerId);       }    } 

As you can see, the cleanup code is placed in the finally block of the test instead of the TearDown method. This is typical for tests that make slight adjustments to the test data. We cannot rely on the TearDown method to explicitly remove the slightly modified test data.

We could get creative with overriding SetUp / TearDown methods in the test classes that share common test data but require slight customizations to it, but this approach is error-prone because if we forget to call the base class TearDown method, we will leave the persistent garbage in the database. We also could create separate fixtures for each test that requires a customized SetUp / TearDown method. Both of these approaches are somewhat similar, and in the end all require us to implement code to reset the database back to its initial state. Because we want a solution that lets us focus on what we are testing as opposed to worrying about writing code that resets the database, let s try a different approach.

Transaction Manager

The first part of the solution is to create a class named TransactionManager . This class will be responsible for the following:

  • Begin a transaction; we do not intend to support nested transactions, and any attempt to start a second transaction associated with the thread of the caller while there is an existing transaction in progress should fail.

  • Roll back the transaction.

  • Commit the transaction.

  • Retrieve the current transaction.

  • Maintain an association between the calling thread and the transaction.

We did implement the TransactionManager one programmer test at a time, but for the sake of brevity, the following is the complete list of programmer tests for the TransactionManager :

 [TestFixture]    public class TransactionManagerFixture : ConnectionFixture    {       private SqlTransaction transaction;       [SetUp]       public void BeginTransaction()       {          transaction = TransactionManager.Begin(Connection);       }       [TearDown]       public void CommitTransaction()       {          if(TransactionManager.Transaction() != null)          {             TransactionManager.Commit();          }       }       [Test]       public void BeginNewTransaction()       {          Assert.IsNotNull(transaction);       }       [Test]       public void GetCurrentTransaction()       {          Assert.AreSame(transaction,              TransactionManager.Transaction());       }       [Test]       public void GetCurrentTransactionNoTransactionInProgress()       {          TransactionManager.Commit();          Assert.IsNull(TransactionManager.Transaction());       }       [Test]       public void Commit()       {          TransactionManager.Commit();          Assert.IsNull(TransactionManager.Transaction());       }       [Test]       public void Rollback()       {          TransactionManager.Rollback();          Assert.IsNull(TransactionManager.Transaction());       }       [Test, ExpectedException(typeof(ApplicationException))]       public void CommitNullTransaction()       {          TransactionManager.Commit();          TransactionManager.Commit();       }       [Test,ExpectedException(typeof(ApplicationException))]       public void BeginNewTransactionTwice()       {          TransactionManager.Begin(Connection);          }    } 

The corresponding implementation of the TransactionManager is shown here:

 using System; using System.Data.SqlClient; using System.Collections; using System.Threading; namespace DataAccess {    public class TransactionManager    {       private static Hashtable transactions = new Hashtable();              public static SqlTransaction Begin(SqlConnection connection)       {          SqlTransaction transaction = Transaction();                    if(transaction == null)          {             transaction = connection.BeginTransaction();             transactions[Thread.CurrentThread] = transaction;          }          else          {             throw new                 ApplicationException("Transaction in progress");          }                    return transaction;       }       public static SqlTransaction Transaction()       {          Thread currentThread = Thread.CurrentThread;          SqlTransaction transaction =              (SqlTransaction)transactions[currentThread];          return transaction;       }       public static void Commit()       {          SqlTransaction transaction = Transaction();                    if(transaction == null)          {             throw new              ApplicationException("No transaction in progress");          }          transaction.Commit();          End();       }       public static void Rollback()       {          SqlTransaction transaction = Transaction();                    if(transaction == null)          {             throw new              ApplicationException("No transaction in progress");          }                    transaction.Rollback();          End();       }       private static void End()       {          transactions.Remove(Thread.CurrentThread);       }    } } 

The TransactionManager uses a Hashtable to maintain the one-to-one mapping between the calling thread and its associated transaction. The rest of the functionality is done by the SqlTransaction class.

Integrating TransactionManager with the Tests and Application Code

The execution of each test using transactions follows the same pattern:

  • The SetUp method starts a new transaction and inserts the required persistent entities into the database.

  • It runs the test.

  • The TearDown method rolls back the transaction.

Instead of writing this over and over, we will capture this pattern in a class named DatabaseFixture that will allow us to consistently enforce the pattern:

 [TestFixture] public abstract class DatabaseFixture : ConnectionFixture {    [SetUp]    public void StartTransaction()    {       TransactionManager.Begin(Connection);       Insert();    }    public abstract void Insert();    [TearDown]    public void Rollback()    {       TransactionManager.Rollback();    } } 
Note  

We defined an abstract method named Insert for derived classes to insert entities into the database that are needed by the test fixture. We do not need the corresponding Delete functionality in the TearDown method because we are using the rollback mechanism that is provided by the database.

Here is a version of the ArtistFixture class modified to work with the newly created DatabaseFixture :

 [TestFixture] public class ArtistFixture : DatabaseFixture {    private static readonly string artistName = "Artist";    private ArtistGateway gateway;     private RecordingDataSet recordingDataSet;    private long artistId;     public override void Insert()    {       recordingDataSet = new RecordingDataSet();       gateway = new ArtistGateway(Connection);       artistId = gateway.Insert(recordingDataSet,artistName);    }    [Test]    public void RetrieveArtistFromDatabase()    {       RecordingDataSet loadedFromDB = new RecordingDataSet();       RecordingDataSet.Artist loadedArtist =           gateway.FindById(artistId, loadedFromDB);       Assert.AreEqual(artistId,loadedArtist.Id);       Assert.AreEqual(artistName, loadedArtist.Name);       }    [Test]    public void DeleteArtistFromDatabase()    {       RecordingDataSet emptyDataSet = new RecordingDataSet();       long deletedArtistId =           gateway.Insert(emptyDataSet,"Deleted Artist");       gateway.Delete(emptyDataSet,deletedArtistId);       RecordingDataSet.Artist deleletedArtist =           gateway.FindById(deletedArtistId, emptyDataSet);       Assert.IsNull(deleletedArtist);    }    [Test]    public void UpdateArtistInDatabase()    {       RecordingDataSet.Artist artist = recordingDataSet.Artists[0];       artist.Name = "Modified Name";       gateway.Update(recordingDataSet);           RecordingDataSet updatedDataSet = new RecordingDataSet();       RecordingDataSet.Artist updatedArtist = gateway.FindById(artistId,           updatedDataSet);       Assert.AreEqual("Modified Name", updatedArtist.Name);    } } 

If you compare this code to the previous version of the ArtistFixture class (see Chapter 5), you will notice the following differences:

  • The ArtistFixture inherits from DatabaseFixture instead of ConnectionFixture to be able to use the transaction capability.

  • Instead of using the [SetUp] attribute to indicate which method is to be called prior to test execution, we implement the Insert method to insert test-specific data into the database.

  • There is no TearDown method because it is handled by the DatabaseFixture , which calls Rollback on the transaction to reset the database.

  • All the test methods no longer delete persistent entities from the database.

When we compile and run these tests, they fail because we need to make a small change in the constructor of the ArtistGateway class, which is in boldface in the following code:

 public ArtistGateway(SqlConnection connection)    {       this.connection = connection;       command = new SqlCommand("select id, name from artist where id = @id",          connection);       command.Parameters.Add("@id",SqlDbType.BigInt);  command.Transaction = TransactionManager.Transaction();  adapter = new SqlDataAdapter(command);       builder = new SqlCommandBuilder(adapter);    } 

We needed to associate the SQL command with the current transaction. When this code is compiled and run, the tests fail because we need to make a similar change to the IdGenerator class. After updating IdGenerator , we compile and run the tests again, and this time they all pass. To complete this part of the implementation, we need to make the same changes to the following classes:

  • LabelFixture and LabelGateway

  • GenreFixture and GenreGateway

  • ReviewFixture and ReviewGateway

  • ReviewerFixture and ReviewerGateway

  • TrackFixture and TrackGateway

We completed these one at a time, each time verifying that the tests pass. It is time for us to tackle the RecordingGatewayFixture and RecordingGateway classes. The changes here are different because the RecordingGatewayFixture (and other test fixtures, for that matter) inherit from a class named RecordingFixture , whose responsibility is to insert a recording into the database. We need to modify the RecordingFixture to inherit from DatabaseFixture to take advantage of the transactional capability. However, it is not as simple as just doing that. The test fixtures that inherited from RecordingFixture also customized the recording after it was inserted into the database. This means that we have to put in a method that the RecordingFixture calls to customize the Recording after it has inserted the Recording entity into the database. Figure 10-1 describes the structure of the test fixtures and their respective responsibilities after implementing these changes.

click to expand
Figure 10-1: Test fixture hierarchy

The code changes for RecordingFixture are highlighted in this code:

 [TestFixture] public abstract class RecordingFixture :  DatabaseFixture  {    private RecordingBuilder builder = new RecordingBuilder();    private RecordingDataSet dataSet;    private RecordingDataSet.Recording recording;  public override void Insert()   {  dataSet = builder.Make(Connection);       recording = dataSet.Recordings[0];  CustomizeRecording();   }   public virtual void CustomizeRecording()   {}  public RecordingBuilder Builder    {       get { return builder; }    }    public RecordingDataSet.Recording Recording    {       get { return recording; }    }    public RecordingDataSet RecordingDataSet    {       get { return dataSet; }    } } 

We also had to change the RecordingGatewayFixture due to the changes in the RecordingFixture . The tests remained the same, so only the changes are shown in the following code:

 [TestFixture] public class RecordingGatewayFixture : RecordingFixture {    private RecordingGateway gateway;     public override void CustomizeRecording()    {       gateway = Builder.RecordingGateway;    }    // Tests were the same as shown previously } 

Finally, when we run these tests, they all fail because we did not modify the RecordingGateway to use transactions. The change we need to make to the constructor of RecordingGateway is highlighted as follows:

 public RecordingGateway(SqlConnection connection)    {       this.connection = connection;       command = new SqlCommand("select id, title, releaseDate, artistId, labelId" +        "from recording where id = @id", connection);       command.Parameters.Add("@id",SqlDbType.BigInt);  command.Transaction = TransactionManager.Transaction();  adapter = new SqlDataAdapter(command);       builder = new SqlCommandBuilder(adapter);    } 

Now when we run the RecordingGatewayFixture tests, they all pass. We then updated the following test fixtures that also inherit from RecordingFixture :

  • RecordingArtistFixture

  • RecordingLabelFixture

  • RecordingReviewsFixture

  • ReviewRecordingFixture

  • TrackRecordingFixture

  • DatabaseCatalogServiceFixture

  • DatabaseUpdateReviewFixture

  • ReviewUpdateFixture

The changes were similar to the ones that we made for the RecordingGatewayFixture . When we ran the tests, they all passed except four tests in ReviewUpdateFixture . In looking at the code, we see the big difference is that this code makes calls to the Catalog class, which also needs to be modified to know about transactions. Instead of having the four failing tests every time we run NUnit, we mark the ReviewUpdateFixture with the [Ignore] attribute. After we fix the Catalog class, we will remove the attribute and see whether we have corrected the problem.




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