Chapter 8: Driving Development with Customer Tests


In this chapter, we add the ability to add and delete a review from an existing recording. These capabilities are really two features, but due to the similarities in the implementation, we have grouped them together. Also, instead of implementing the functionality and then checking in with the customer to verify whether or not it s correct (as we did previously), we ll first work with the customer to define a set of customer tests and then implement the functionality to make the tests pass. This approach should eliminate the issues that we had implementing the previous feature when we, as the programmers, made some assumptions that turned out to be incorrect. Having customer tests prior to starting development is an ideal situation. Although we have not seen this situation very often, that does not make it any less desirable. In reality, you should push for the customer tests as soon as possible to alleviate the reconciliation of the differing viewpoints that we demonstrated in Chapter 7, Customer Tests: Completing the First Feature. The longer you develop without customer tests, the greater the chance that the implementation will not match the customer s expectations.

The FIT Script

We need to write a test that verifies the correct behavior when adding a Review to a Recording . The script first verifies the existing state of the recording, then adds a review, and then deletes it. Here is the FIT script, Add a review to an existing recording.

Add a review to an existing recording

This test, knowing the existing state, will add a review to a recording. After it s added, the test will retrieve the recording and verify that the review is correctly added. The last step is to remove the review from the recording, returning the database to its original state.

Before:

fit.ActionFixture

start

CustomerTests.CatalogAdapter

 

enter

FindByRecordingId

100001

check

Found

True

check

AverageRating

2

CustomerTests.ReviewDisplay

ReviewerName()

Content()

Rating()

Sample Reviewer

Inspiration was low

1

Example Reviewer

Could be better

3

Test Reviewer

I thought it was great

4

Add the review:

fit.ActionFixture

start

CustomerTests.ReviewAdapter

 

enter

FindByRecordingId

100001

enter

SetRating

4

enter

SetContent

Test Content

enter

SetReviewerName

FIT Reviewer

enter

AddReview

 

check

ReviewAdded

True

enter

FindByRecordingId

100001

check

AverageRating

3

Verify the contents of the review:

CustomerTests.ReviewDisplay

ReviewerName()

Content()

Rating()

Sample Reviewer

Inspiration was low

1

Example Reviewer

Could be better

3

Test Reviewer

I thought it was great

4

FIT Reviewer

Test content

4

Remove the review:

To make sure that the test is repeatable, we need to remove the newly added review:

fit.ActionFixture

enter

DeleteReview

Reviewer

To get this customer test to work, we need to create a new adapter that can add and delete a review. We call this class the ReviewAdapter , which is shown here:

 public class ReviewAdapter : CatalogAdapter    {       private string name;        private string content;       private int rating;        private ReviewDto review;        public bool ReviewAdded()       {          return (review != null);       }              public void DeleteReview(string reviewerName)       {       }       public void AddReview(string nothing)       {       }       public void SetReviewerName(string name)       {          this.name = name;        }       public void SetRating(int rating)       {          this.rating = rating;        }       public void SetContent(string content)       {          this.content = content;       }    } 

Instead of duplicating much of the CatalogAdapter code, we choose to have AddReviewAdapter inherit from CatalogAdapter to share the implementation. After the adapter compiles, it is time to run the script. Here are the failures.

Add the review:

fit.ActionFixture

start

CustomerTests.AddReviewAdapter

 

enter

FindByRecordingId

100001

enter

SetRating

4

enter

SetContent

Test Content

enter

SetReviewerName

FIT Reviewer

enter

AddReview

 

check

ReviewAdded

True expected

   

False actual

enter

FindByRecordingId

100001

check

AverageRating

3 expected

   

2 actual

Verify the contents of the review:

CustomerTests.ReviewDisplay

ReviewerName()

Content()

Rating()

Sample Reviewer

Inspiration was low

1

Example Reviewer

Could be better

3

Test Reviewer

I thought it was great

4

FIT Reviewer missing

Test Content

4

We have three failures. Although we did expect this test to fail, the purpose of running the test was to give us an indication of whether or not the customer s expectations have been met. Obviously, there are many more tests needed to completely verify this functionality, but we use this one for the purposes of demonstration. Now let s go about the business of implementing the add and delete review functionality.

Implementing Add/Delete Review

Now that we have a customer test in place, we need to change the focus back to the actual implementation of the feature. To do this, we use programmer tests. After we have a failing customer test, we need to write a set of corresponding programmer tests that drive the development of the feature. But where should we start? We could start with the layer of our application closest to the failing customer tests ”the service interface, or we could start with the layer closest to the database ”data access.

Over the last few chapters, we have gradually built up the application architecture. Figure 8-1 demonstrates what our application structure looks like.

click to expand
Figure 8-1: Application structure

The process of this architectural evolution was gradual and driven by the next small increment of the functionality that we needed to implement. Now we are attempting to add another increment of the functionality; however, the application s architecture appears to be sufficient to support the addition of this new functionality without making architectural changes ”we simply can go to the appropriate points in the application and add the new functionality. We start with the data access layer to support the addition and deletion of reviews.

Let s define a test list for the add and delete review functionality:

Test List

  • Add a review by an existing reviewer to a new recording, load the recording from the database, and verify that the added review is present.

  • Add a review by a new reviewer to a new recording, load the recording from the database, and verify that the added review is present.

  • Add a review by an existing reviewer to a new review, delete the added review, load the recording from the database, and verify that the added/deleted review is not present.

Given this test list, let s get to work on the implementation.

Changing the Catalog Class

We start with the Catalog class because it is the interface to our data access layer, and we need to make changes to this interface to support the additional functionality: AddReview and DeleteReview .

Our test list shows that we need to create a new recording for all the tests. We already have a test fixture that creates a new recording in the database ” RecordingFixture ” and we want to use it. Because the tests we are writing are all related to updating review information on a recording, we will create a new fixture for these new tests: ReviewUpdateFixture . This new fixture will extend the RecordingFixture to reuse its recording creation/deletion capabilities. Here are the tests:

 using System; using DataAccess; using DataModel; using NUnit.Framework; [TestFixture] public class ReviewUpdateFixture : RecordingFixture {    private static readonly string reviewerName = "ReviewUpdateFixture";         private RecordingDataSet recordingDataSet = new RecordingDataSet();    private RecordingDataSet.Recording loadedRecording;        [SetUp]    public new void SetUp()    {       base.SetUp();       RecordingDataSet loadedDataSet = new RecordingDataSet();       loadedRecording = Catalog.FindByRecordingId(loadedDataSet,           Recording.Id);    }    [TearDown]    public new void TearDown()    {       base.TearDown();    }    [Test]    public void AddReviewWithExistingReviewer()    {       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 review =           Catalog.AddReview(recordingDataSet, reviewerName,              content, rating, Recording.Id);       Assert.IsNotNull(review);       RecordingDataSet loadedFromDBDataSet = new RecordingDataSet();       RecordingDataSet.Recording loadedFromDBRecording =           Catalog.FindByRecordingId(loadedFromDBDataSet,              Recording.Id);       Assert.AreEqual(1, loadedFromDBRecording.GetReviews().Length);       RecordingDataSet.Review loadedFromDBReview =           loadedFromDBRecording.GetReviews()[0];       ReviewGateway reviewGateway = new ReviewGateway(Connection);       reviewGateway.Delete(loadedFromDBDataSet, loadedFromDBReview.Id);        reviewerGateway.Delete(recordingDataSet, reviewerId);    }    [Test]    public void AddReviewWithoutExistingReviewer()    {       int rating = 1;       string content = "Review content";       RecordingDataSet.Review review =           Catalog.AddReview(recordingDataSet, reviewerName, content,              rating, Recording.Id);       Assert.IsNotNull(review);       RecordingDataSet loadedFromDBDataSet = new RecordingDataSet();       RecordingDataSet.Recording loadedFromDBRecording =           Catalog.FindByRecordingId(loadedFromDBDataSet,              Recording.Id);       Assert.AreEqual(1, loadedFromDBRecording.GetReviews().Length);       RecordingDataSet.Review loadedFromDBReview =           loadedFromDBRecording.GetReviews()[0];       ReviewGateway reviewGateway = new ReviewGateway(Connection);       reviewGateway.Delete(loadedFromDBDataSet, loadedFromDBReview.Id);        ReviewerGateway ReviewerGateway =           new ReviewerGateway(Connection);       long reviewerId = review.ReviewerId;       ReviewerGateway.Delete(recordingDataSet, reviewerId);    }    [Test]    public void DeleteReview()    {       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 review =           Catalog.AddReview(recordingDataSet, reviewerName,              content, rating, Recording.Id);       Catalog.DeleteReview(review.Id);              RecordingDataSet loadedFromDB = new RecordingDataSet();       RecordingDataSet.Recording loadedFromDBRecording =           Catalog.FindByRecordingId(loadedFromDB, Recording.Id);       Assert.AreEqual(0, loadedFromDBRecording.GetReviews().Length);    } } 

In these tests, we verify two aspects of adding a review to a recording. One scenario is that the Reviewer has already been defined and we simply add the review. In this scenario, we need to know whether a reviewer with a given name already exists, and to support this we need to add a new method to the ReviewerGateway : FindByName .

In the other scenario, we have to also add the Reviewer to the database prior to adding the review. The test to delete a review adds the review and then verifies that it has been removed after the call to DeleteReview . When we try to compile this fixture, it fails because we have not added the AddReview and DeleteReview methods to the Catalog class. Let s do that now:

 public static RecordingDataSet.Review AddReview(RecordingDataSet dataSet,    string name, string content, int rating, long recordingId) {    SqlConnection connection = null;    RecordingDataSet.Review review = null;    try    {       connection = new SqlConnection(ConfigurationSettings.AppSettings.Get("Catalog.Connection"));       connection.Open();       RecordingDataSet.Recording recording =           FindByRecordingId(dataSet, recordingId);       ReviewerGateway reviewerGateway =           new ReviewerGateway(connection);       RecordingDataSet.Reviewer reviewer =           reviewerGateway.FindByName(name, dataSet);       if(reviewer == null)       {          long reviewerId = reviewerGateway.Insert(dataSet,name);          reviewer = reviewerGateway.FindById(reviewerId,dataSet);       }       ReviewGateway reviewGateway = new ReviewGateway(connection);       long reviewId = reviewGateway.Insert(dataSet, rating, content);       review = reviewGateway.FindById(reviewId, dataSet);       review.ReviewerId = reviewer.Id;       review.Recording = recording;       reviewGateway.Update(dataSet);    }    finally    {       if(connection != null)          connection.Close();    }    return review;  } public static void DeleteReview(long reviewId) {    SqlConnection connection = null;    try    {       connection = new SqlConnection(ConfigurationSettings.AppSettings.Get("Catalog.Connection"));       connection.Open();       RecordingDataSet recordingDataSet =           new RecordingDataSet();       ReviewGateway reviewGateway = new ReviewGateway(connection);       reviewGateway.Delete(recordingDataSet, reviewId);    }    finally    {       if(connection != null)          connection.Close();    }    return; } 

This code compiles and runs successfully. Let s work on the CatalogService .

Changing the CatalogService Class

The next place we need to change is the CatalogService in the ServiceInterface . The current version of the CatalogService class is shown here:

 using System; using System.Collections; using DataModel; namespace ServiceInterface {    public abstract class CatalogService    {       public RecordingDto FindByRecordingId(long id)       {          RecordingDataSet.Recording recording = FindById(id);          if(recording == null) return null;          return RecordingAssembler.WriteDto(recording);       }       protected abstract           RecordingDataSet.Recording FindById(long recordingId);    } } 

We also have two subclasses of the CatalogService class: DatabaseCatalogService and StubCatalogService . DatabaseCatalogService implements the abstract FindById method by making a call to the database through the Catalog class; StubCatalogService works with an in-memory representation of a recording and does not make database calls. We used the stub implementation earlier as we were evolving the application architecture; the stub s purpose is to isolate the ServiceInterface code from the database. We have extracted most of the in- memory recording manipulation code that stub uses into another class ” InMemoryRecordingBuilder ” and this builder is used for testing RecordingAssembler .

At this point, we need to add new functionality to the CatalogService class; this, in turn , means that we will need to maintain two implementations of this functionality: DatabaseCatalogService and StubCatalogService . However, adding and deleting a review from the in-memory representation is not a worthy investment. What we really want to test is whether we can add or delete a review with a recording in the database. If we did this with the in-memory representation, we would also have to write the code that added and deleted the review from the RecordingDataSet . Let s leave the current class structure, but let s implement this new functionality only in the DatabaseCatalogService class. We will modify the code that we used in the CatalogService class and provide a default implementation for data access. Here are the changes:

 using System; using System.Collections; using DataModel; namespace ServiceInterface {    public abstract class CatalogService    {       public RecordingDto FindByRecordingId(long id)       {          RecordingDataSet.Recording recording = FindById(id);          if(recording == null) return null;          return RecordingAssembler.WriteDto(recording);       }  public ReviewDto AddReview(string reviewerName, string content,   int rating, long recordingId)   {   RecordingDataSet.Review review =   AddReviewToRecording(reviewerName,   content, rating, recordingId);   return RecordingAssembler.WriteReview(review);   }   public void DeleteReview(long reviewId)   {   DeleteReviewFromRecording(reviewId);   }  protected abstract           RecordingDataSet.Recording FindById(long recordingId);  protected virtual RecordingDataSet.Review AddReviewToRecording(   string reviewerName, string content, int rating, long   recordingId)   {   return null;   }   protected virtual void DeleteReviewFromRecording(long reviewId)   {}  } } 

The added code is in boldface. As you can see, instead of using an abstract method to defer the implementation of data access to the subclasses, the CatalogService class defines a pair of protected virtual methods ( AddReviewToRecording and DeleteReviewFromRecording ) with empty implementations. These default implementations of data access allow us to not modify the StubCatalogService . On the other hand, this version of the CatalogService seems somewhat awkward . We solve this problem in Chapter 11, Service Layer Refactoring. Let s move on to implementing the DatabaseCatalogService .

Updating DatabaseCatalogService

We need to refine the test list for this step. Here is the list of the programmer tests that we need to implement:

  • Add a review to a new recording and verify that the ReviewDto returned has the correct content.

  • Add a review to a new recording, retrieve the recording by id from the service, and verify that the retrieved RecordingDto has the review just added.

  • Add a review to a new recording, delete the review just added, retrieve the recording from the service, and verify that the retrieved recording does not have the deleted review.

Once again, these tests share similar SetUp/TearDown steps, and we can reuse the RecordingFixture to perform the required database access code. We choose to separate the tests for updating review information from the existing DatabaseCatalogService tests and place them into a new fixture: DatabaseUpdateReviewFixture . Here is the code for it:

 [TestFixture] public class DatabaseUpdateReviewFixture : RecordingFixture {    private static readonly string reviewerName =        "DatabaseUpdateReviewFixture";    private static readonly string reviewContent = "Fake Review Content";    private static readonly int    rating = 3;     private CatalogService service;     [SetUp]    public new void SetUp()    {       base.SetUp();       service = new DatabaseCatalogService();    }    [TearDown]    public new void TearDown()    {       base.TearDown();    }    [Test]    public void AddReviewContent()    {       ReviewDto dto = service.AddReview(reviewerName, reviewContent,           rating, Recording.Id);       Assert.AreEqual(reviewerName, dto.reviewerName);       Assert.AreEqual(reviewContent, dto.reviewContent);       Assert.AreEqual(rating, dto.rating);    }    [Test]    public void ReviewAddedToRecording()    {       int beforeCount = Recording.GetReviews().Length;       ReviewDto dto = service.AddReview(reviewerName,           reviewContent, rating,           Recording.Id);       RecordingDto recordingDto =           service.FindByRecordingId(Recording.Id);       Assert.AreEqual(beforeCount+1, recordingDto.reviews.Length);    }    [Test]    public void ReviewDeletedFromRecording()    {       int beforeCount = Recording.GetReviews().Length;       ReviewDto dto = service.AddReview(reviewerName, reviewContent,           rating, Recording.Id);       service.DeleteReview(dto.id);       RecordingDto recordingDto =           service.FindByRecordingId(Recording.Id);       Assert.AreEqual(beforeCount, recordingDto.reviews.Length);    } } 

These tests fail because we have not updated the DatabaseCatalogService class yet. Here are the two methods we have to add to the DatabaseCatalogService to make these tests pass:

 protected override RecordingDataSet.Review AddReviewToRecording(string reviewerName, string content, int rating, long recordingId) {    RecordingDataSet dataSet = new RecordingDataSet();    return Catalog.AddReview(dataSet, reviewerName, content,        rating, recordingId); } protected override void DeleteReviewFromRecording(long reviewId) {    Catalog.DeleteReview(reviewId);    return; } 

Now the tests pass, and we are ready to move on to the Web service.

Updating CatalogServiceInterface

This step is very similar to the step we just finished. Here is a test list:

  • Write the programmer tests for the CatalogServiceInterface (these will go into a new fixture called UpdateCatalogGatewayFixture ).

  • Update the CatalogServiceInterface class by adding two new Web methods: AddReview and DeleteReview .

  • Hook up the Web methods to the DatabaseCatalogService class.

The new programmer tests verify the functionality when accessing the Web service. They are so similar to the tests for DatabaseCatalogService that we show only the one that has the most differences:

 [Test] public void ReviewDeletedFromRecording() {    ServiceGateway.RecordingDto recordingDto =        gateway.FindByRecordingId(Recording.Id);    Assert.IsNull(recordingDto.reviews);    ServiceGateway.ReviewDto dto =        gateway.AddReview(reviewerName, reviewContent, rating,           Recording.Id);    gateway.DeleteReview(dto.id);    recordingDto = gateway.FindByRecordingId(Recording.Id);    Assert.IsNull(recordingDto.reviews); } 

If you compare this test with the similar method in DatabaseUpdateReviewFixture , you see that the differences are in the serialization behavior: When we don t go through the Web service, we get back a RecordingDto with an empty collection of reviews; when we do go through the Web service layer, the resulting RecordingDto is null instead of an empty collection of reviews. These Web service-specific serialization differences are the only new information these tests reveal. Because these tests do not reveal a lot more new information, and because there is a lot of overlap between these programmer tests and the customer tests, we felt that we were going to rely solely on the customer tests for verifying that the application works through the Web service. So we deleted the programmer tests that used the CatalogGateway .

Fixing AddReviewAdapter

Now that CatalogServiceInterface exposes the AddReview and DeleteReview services, we can modify the AddReviewAdapter in the customer tests to use the newly defined Web services.

 public void AddReview(string nothing) {    review = Gateway.AddReview(name,              content, rating, Recording.id); }       public void DeleteReview(string nothing) {    if(review != null)       Gateway.DeleteReview(review.id); } 

After we made the changes, we recompiled the customer tests and ran all the FIT scripts. All the customer and programmer tests pass, which is our indication that we are done implementing this feature.




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