Programmer Tests


The most appropriate place to write the first programmer test is in the DataAccess namespace. Because the test is similar to the other tests in ReviewUpdateFixture , we should put it in that test fixture. What should the test look like? Well, first it should insert a review into the database using the Catalog.AddReview function. After it is successfully added, the test should try to insert the review again. What should happen when this function is called the second time? Catalog.AddReview should throw an exception indicating that there is an existing review, and the exception object should contain the id of the existing review. Let s write a test that looks for an exception when the second review is added.

 [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);       }    } 

We did not use NUnit s ExpectedException syntax here because we have test method-specific cleanup to do after the exception is thrown. We could have split this fixture in two and then used the ExpectedException , but it seems like more trouble than it is worth. When we compile the test, it fails because we have not defined the ExistingReviewException class. Let s do that now:

 namespace DataAccess {    public class ExistingReviewException : ApplicationException    {       private long id;        public ExistingReviewException(long existingId)       {          id = existingId;        }       public long ExistingId       {          get                 { return id; }       }    } } 

The code now compiles. When we run the test, it fails because we have not changed the Catalog.AddReview function to throw the exception. Let s make that modification:

 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); }  foreach(RecordingDataSet.Review existingReview in   recording.GetReviews())  { if(existingReview.Reviewer.Name.Equals(name)) throw new ExistingReviewException(existingReview.Id); } 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; } 

The boldface code shows the changes needed to throw an exception of type ExistingReviewException when there is an existing review by the same reviewer. Running the test indicates success. Now we need to run the customer tests as well and see what happens. When we run the customer test described at the beginning of the chapter, we get a SoapException , and the test does not pass. We need to make a change to the ReviewAdapter to make the test succeed. Here is the change:

 public void AddReview(string nothing)    {       try       {          review = Gateway.AddReview(name, content, rating,                             Recording.id);       }       catch(SoapException exception)       {          review = null;       }    } 

This compiles; we first run the programmer tests, which all pass. Next we run the customer tests, and we get the result on the following page.

fit.ActionFixture

start

CustomerTests.ReviewAdapter

 

enter

FindByRecordingId

100001

enter

SetRating

4

enter

SetContent

Test Fit Content

enter

SetReviewerName

Example Reviewer

enter

AddReview

 

check

ReviewAdded

False

check

ExistingReviewId

100002 expected

   

0 actual

We still have the implementation for ExistingReviewId in the ReviewAdapter class, which returns 0. There is no way to implement this method as of yet because the SoapException does not explicitly contain the ExistingId field. How do we get the SoapException to contain the ExistingId information? Let s work on making this happen.

Propagating the Exception

There are a number of things we have to do to propagate the ExistingReviewException information to the client. We have to figure out how to augment the general Simple Object Access Protocol (SOAP) failure with the specific information needed when we try to add an additional review. One thing that jumps out immediately is that we do not want the client to be able to see the stack trace of our server code. This is a security issue because it gives people information about how our code is structured, and this could be exploited. So first, let s make sure that the clients cannot see the stack trace. This is accomplished by making a change to the Web.config file in the ServiceInterface directory. Here is the change:

 <customErrors mode="RemoteOnly" /> 

After this change is made, the stack trace is no longer available to users who are not running on the local Web server.

Implementing a SOAP Fault

The next step is to propagate the ExistingReviewException information to the client using a SOAP fault. Previously, we were getting a SoapException without any detail. To make sure that we are getting the information we need, let s write a test that makes calls to the CatalogGateway that attempts to add the review twice, just as we did in the Catalog . Here is the test:

 [TestFixture] public class MultipleReviewFixture : RecordingFixture {    private static readonly string reviewerName =        "DatabaseUpdateReviewFixture";    private static readonly string reviewContent =        "Fake Review Content";    private static readonly int    rating = 3;         private CatalogGateway gateway;    private ServiceGateway.ReviewDto review;    [Test]    public void AddSecondReview()    {       gateway = new CatalogGateway();              review = gateway.AddReview(reviewerName, reviewContent,                       rating, Recording.Id);              try       {          gateway.AddReview(reviewerName, reviewContent,                    rating, Recording.Id);          Assert.Fail("AddReview should have thrown an exception");       }       catch(SoapException exception)       {          Assert.AreEqual(SoapException.ClientFaultCode,              exception.Code);       }       finally       {          ReviewGateway reviewGateway = new ReviewGateway(Connection);          reviewGateway.Delete(RecordingDataSet, review.id);       }    } } 

MultipleReviewFixture inherits from RecordingFixture , which inserts a Recording into the database. The AddSecondReview test attempts to add the same review to the recording twice and verifies that it fails. At first, we are expecting it to return a SoapException with the Code property set to ClientFaultCode , which indicates that the reason the call did not succeed was due to a client-side error. When we compile and run this test, it fails because the Code property defaults to ServerFaultCode . Let s switch over to the server code and make this change. We need to modify the AddReview method in the ServiceInterface to create the SoapException to be sent over the wire. Here is the first implementation:

 [WebMethod]    public ReviewDto AddReview(string reviewerName, string content,        int rating, long recordingId)    {       ReviewDto review = null;       try       {          review = service.AddReview(reviewerName, content,              rating, recordingId);       }       catch(ExistingReviewException existingReview)       {          throw new SoapException(existingReview.Message,             SoapException.ClientFaultCode);       }       return review;    } 

After this is compiled and run, the test passes . All we have done, however, is to specify that a client problem has occurred. Let s move on and get more specific.

Turning back to the MultipleReviewFixture , we need to pass some additional information across the wire to indicate what is wrong. Because of the need for multiple tests with the same SetUp , we need to refactor the test code to eliminate the duplication. Here is the refactored test code that is equivalent to the previous code:

 [TestFixture] public class MultipleReviewFixture : RecordingFixture {    private static readonly string reviewerName =           "DatabaseUpdateReviewFixture";    private static readonly string reviewContent = "Fake Review Content";    private static readonly int    rating = 3;     private CatalogGateway gateway;    private ServiceGateway.ReviewDto review;    private SoapException soapException;    [SetUp]    public new void SetUp()    {       base.SetUp();       gateway = new CatalogGateway();       review = gateway.AddReview(reviewerName, reviewContent,                 rating, Recording.Id);       try       {          gateway.AddReview(reviewerName, reviewContent,                       rating, Recording.Id);       }       catch(SoapException exception)       {          soapException = exception;       }    }    [TearDown]    public new void TearDown()    {       ReviewGateway reviewGateway = new ReviewGateway(Connection);       reviewGateway.Delete(RecordingDataSet, review.id);       base.TearDown();    }    [Test]    public void ExceptionThrown()    {       Assert.IsNotNull(soapException);    }    [Test]    public void ClientFaultCode()    {       Assert.AreEqual(SoapException.ClientFaultCode,           soapException.Code);    } } 

When we compile and run this code, both tests pass, and we can move on to looking for the fault code. We need to use the Detail property of the SoapException to indicate why the call to AddReview failed. This is typically done using an XML document that contains the explicit fault code. The XML document is defined as follows :

 <detail>    <fault-code>ExistingReview</fault-code> </detail> 

We have decided that the detail fault code will be ExistingReview to indicate that a review by that reviewer already exists. Here is the test:

 [Test]    public void DetailFaultCode()    {       Assert.AreEqual("ExistingReview",           XPathQuery(soapException.Detail, "//fault-code"));    }    private string XPathQuery(XmlNode node, string expression)    {       XPathNavigator navigator = node.CreateNavigator();                 string selectExpr = expression;       navigator.MoveToRoot();       XPathExpression expr = navigator.Compile(selectExpr);                 XPathNodeIterator index = navigator.Select(expr);       index.MoveNext();       return index.Current.Value.Trim();     } 

When we run this test, it fails because there is no detail being added to the SoapException . Let s move over to the server and make it work. We need to make the following highlighted changes to the AddReview method to make this test pass:

 [WebMethod]    public ReviewDto AddReview(string reviewerName, string content,           int rating, long recordingId)    {       ReviewDto review = null;       try       {          review = service.AddReview(reviewerName, content,                 rating, recordingId);       }       catch(ExistingReviewException existingReview)       {  XmlDocument document = new XmlDocument();   XmlElement detail = document.CreateElement("detail");   document.AppendChild(detail);   XmlElement code = document.CreateElement("fault-code");   detail.AppendChild(code);   XmlText faultCode =   document.CreateTextNode("ExistingReview");   code.AppendChild(faultCode);   throw new SoapException(existingReview.Message,   SoapException.ClientFaultCode,   Context.Request.Url.ToString(),   document.DocumentElement);  }       return review;    } 

When this code compiles, the tests pass. Let s continue on. The next step is to pass the id of the existing review to the client. We will be modifying the XML document to include this information as well. The new XML document is defined as follows:

 <detail>    <fault-code>ExistingReview</fault-code>    <existing-id>1001</existing-id> </detail> 

Given this new requirement, let s add a test to look for the existing-id field:

 [Test]    public void ExistingReviewId()    {       Assert.AreEqual(review.id,          Int64.Parse(XPathQuery(soapException.Detail,                    "//existing-id")));    } 

Using the standard practice of compiling and running the tests, this test does not pass. We need to fix the implementation on the server and add the existing-id field to the XML document. Here is the updated AddReview method with the changes highlighted:

 [WebMethod]    public ReviewDto AddReview(string reviewerName, string content,           int rating, long recordingId)    {       ReviewDto review = null;       try       {          review = service.AddReview(reviewerName, content,                 rating, recordingId);       }       catch(ExistingReviewException existingReview)       {          XmlDocument document = new XmlDocument();          XmlElement detail = document.CreateElement("detail");          document.AppendChild(detail);          XmlElement code = document.CreateElement("fault-code");          detail.AppendChild(code);          XmlText faultCode =                       document.CreateTextNode("ExistingReview");          code.AppendChild(faultCode);  XmlElement existingId =   document.CreateElement("existing-id");   detail.AppendChild(existingId);   XmlText id =   document.CreateTextNode(   existingReview.ExistingId.ToString());   existingId.AppendChild(id);  throw new SoapException(existingReview.Message,              SoapException.ClientFaultCode,             Context.Request.Url.ToString(),             document.DocumentElement);       }       return review;    } 

After this code is compiled, all the programmer tests pass as expected. When we run the customer tests, we are still left with a single failing customer test. Because we did the work to send the data to the client, we can implement the ReviewAdapter.ExistingReviewId method. Here is the updated ReviewAdapter :

 public class ReviewAdapter : CatalogAdapter    {       private string name;        private string content;       private int    rating;        private ReviewDto review;        private long existingReviewId;              public void AddReview(string nothing)       {          try          {             review = Gateway.AddReview(name, content, rating,                       Recording.id);          }          catch(SoapException soapException)          {             review = null;             existingReviewId = Int64.Parse(XPathQuery(soapException.Detail,                                   "//existing-id"));          }       }       public void DeleteReview(string reviewerName)       {          Gateway.DeleteReview(review.id);       }       public long ExistingReviewId()       { return existingReviewId; }       public bool ReviewAdded()       { return (review != null); }       public void SetReviewerName(string name)       { this.name = name; }       public void SetRating(int rating)       { this.rating = rating; }       public void SetContent(string content)       { this.content = content; }       private string XPathQuery(XmlNode node, string expression)       {          XPathNavigator navigator = node.CreateNavigator();                    string selectExpr = expression;          navigator.MoveToRoot();          XPathExpression expr = navigator.Compile(selectExpr);                    XPathNodeIterator index =              navigator.Select(expr);          index.MoveNext();          return index.Current.Value.Trim();        }    } 

When we compile and run all the tests (customer and programmer), they pass. We are done with implementing the functionality. However, there is some cleanup we need to do before we can move on. In the previous chapter, we made an argument that the customer tests are the best way to test the system through the Web service. In this chapter, we again wrote programmer tests that used the CatalogGateway when we were implementing the functionality. The tests in MultipleReviewFixture verified the failure condition and that the server built a SoapException with the detail information related to the failure. However, the scope of these tests is too large. They require the entire system to run and are really duplicates of the customer tests, so we will remove the MultipleReviewFixture . If we remove the MultipleReviewFixture , we will lose the test coverage related to mapping an ExistingReviewException into a SoapException . We can test this mapping without having to transport it across the Web service, however. Let s do that now.

There is code in the CatalogServiceInterface.AddReview method that builds the SoapException with the information related to the ExistingReviewerException. We can separate this code into a new class called ExistingReviewMapper , which maps the information from an ExistingReviewException into a SoapException with the proper detail filled in. Let s write the ExistingReviewMapperFixture :

 [TestFixture] public class ExistingReviewMapperFixture {    private static readonly long id = 12;    private SoapException soapException;     [SetUp]    public void SetUp()    {       ExistingReviewException exception = new                 ExistingReviewException(id);       soapException = ExistingReviewMapper.Map(exception);    }    [Test]    public void ClientFaultCode()    {       Assert.AreEqual(SoapException.ClientFaultCode,           soapException.Code);    }    [Test]    public void FaultCode()    {       string faultCode = XPathQuery(soapException.Detail,                 "//fault-code");       Assert.AreEqual(ExistingReviewMapper.existingReviewFault,                          faultCode);    }    [Test]    public void ExistingIdField()    {       string existingId = XPathQuery(soapException.Detail,                 "//existing-id");       Assert.AreEqual(id, Int64.Parse(existingId));    }    private static string XPathQuery(XmlNode node, string expression)    {       // same code as shown in previous sections     } } 

The resulting implementation of ExistingReviewMapper looks like this:

 namespace ServiceInterface {    public class ExistingReviewMapper    {       private static readonly string operation = "AddReview";       public static readonly string rootElement = "detail";       public static readonly string faultCodeElement = "fault-code";       public static readonly string existingIdElement = "existing-id";       public static readonly string invalidFieldElement =                 "invalid-field";       public static readonly string existingReviewFault =                 "ExistingReview";       private static XmlDocument Make(string faultCode,           string fieldName, string fieldValue)       {          XmlDocument document = new XmlDocument();          XmlElement detail = document.CreateElement(rootElement);          document.AppendChild(detail);          XmlElement code = document.CreateElement(faultCodeElement);          detail.AppendChild(code);          XmlText codeNode =                       document.CreateTextNode(faultCode.ToString());          code.AppendChild(codeNode);          XmlElement field = document.CreateElement(fieldName);          detail.AppendChild(field);          XmlText fieldNode = document.CreateTextNode(fieldValue);          field.AppendChild(fieldNode);          return document;       }       public static SoapException Map(ExistingReviewException exception)       {          XmlDocument document =              Make(existingReviewFault, existingIdElement,              exception.ExistingId.ToString());          return new SoapException(exception.Message,              SoapException.ClientFaultCode,             operation, document.DocumentElement);       }    } } 

The code compiles, and the tests pass. Let s replace the code in the CatalogServiceInterface.AddReview method to use the ExistingReviewMapper class:

 [WebMethod]       public ReviewDto AddReview(string reviewerName, string content,           int rating, long recordingId)       {          ReviewDto review = null;          try          {             review = service.AddReview(reviewerName, content,                 rating, recordingId);          }          catch(ExistingReviewException existingReview)          {             throw ExistingReviewMapper.Map(existingReview);          }          return review;       } 

We compile and run the programmer tests, and they all pass. We also run the customer tests, and they pass as well. By separating the functionality that maps the ExistingReviewException into a SoapException out, we can now use programmer tests to determine whether the mapping is correct and customer tests to verify that the system works end-to-end as expected.




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