Implementing Search


It looks as if we need to have a page in which the user enters search criteria and then pushes the Search button. When the Search button is pushed , we need to copy the criteria from the page and pass it to a method in the CatalogService class, which will search the database, using the criteria, and return an ArrayList of recordings that match the search criteria. When we get the ArrayList back, we will have to bind it to a Repeater to have the results displayed in the tabular form.

This implementation is really two separate tasks : the first task is to implement the search functionality in the CatalogService class, and the second task is to write the Web page that interacts with the user and calls the method in the CatalogService to retrieve the results. Separating the tasks allows us to implement the majority of the implementation (the CatalogService search method) using the same techniques we used in previous chapters.

Implementing the Search Service

The existing CatalogService class does not provide a method to search the recording database. We implemented this functionality using the techniques we demonstrated in the previous chapters, but we do not show the step-by- step implementation here. (The completed functionality with programmer and customer tests is available in the companion website that we mentioned in the Introduction.) The steps were not much different from what we did for other CatalogService methods , such as FindByRecordingId , AddReview , and DeleteReview .

Here is the Search method implementation in the CatalogService class:

 public ArrayList Search(SearchCriteria criteria)    {       ArrayList searchResults = new ArrayList();       ArrayList recordings = Catalog.Search(criteria);       foreach(RecordingDataSet.Recording recording in recordings)       {          searchResults.Add(RecordingAssembler.WriteDto(recording));       }       return searchResults;    } 

SearchCriteria is defined as follows :

 public struct SearchCriteria    {       public long id;       public string artistName;       public string title;       public string labelName;       public int averageRating;    } 

Implementing the Search Page

Now that the CatalogService task is complete, we can focus entirely on the Web application code. Just as we did in Chapter 6, Programmer Tests: Using TDD with ASP.NET Web Services, we will isolate the code we are working on (Web application code) from the underlying implementation ( CatalogService ) by implementing the CatalogServiceStub .

Here is the first test:

 [TestFixture] public class SearchFixture {    [Test]    public void SearchById()    {       SearchCriteria criteria = new SearchCriteria();       criteria.id = 42;        CatalogServiceStub serviceStub = new CatalogServiceStub();       ArrayList results = serviceStub.Search(criteria);       Assert.AreEqual(1, results.Count);       RecordingDto dto = (RecordingDto)results[0];       Assert.AreEqual(criteria.id, dto.id);    } } 

This test specifies the need for a CatalogServiceStub class with a method named Search , which takes as input the SearchCriteria class and returns an ArrayList of RecordingDto objects that match the criteria. The test verifies that the method returns a single RecordingDto and that the RecordingDto.id is equal to the value that we specified in the search criteria. Here is the implementation of CatalogServiceStub that will make the test pass:

 public class CatalogServiceStub {    public ArrayList Search(SearchCriteria criteria)    {       ArrayList results = new ArrayList();       RecordingDto dto = new RecordingDto();       dto.id = criteria.id;       results.Add(dto);       return results;    } } 

This implementation is an example of the technique called Fake It (Til You Make It) described in Test-Driven Development by Kent Beck (Addison- Wesley, 2003). We are faking the call to CatalogService and returning a known value. Let s write another test that expects multiple RecordingDto objects.

 [Test]    public void SearchByArtistName()    {       SearchCriteria criteria = new SearchCriteria();       criteria.artistName = "Fake Artist Name";        CatalogServiceStub serviceStub = new CatalogServiceStub();       ArrayList results = serviceStub.Search(criteria);       Assert.AreEqual(2, results.Count);       foreach(RecordingDto dto in results)          Assert.AreEqual(criteria.artistName, dto.artistName);    } 

This test states that if we specify the artistName criteria, we ll get two RecordingDto objects back, and the RecordingDto.artistName field will be equal to the value we specified in the criteria. Here is the CatalogServiceStub implementation:

 public ArrayList Search(SearchCriteria criteria)    {       ArrayList results = new ArrayList();       if(criteria.id != 0)       {          RecordingDto dto = new RecordingDto();          dto.id = criteria.id;          results.Add(dto);       }       else if(criteria.artistName != null)        {          RecordingDto dto = new RecordingDto();          dto.artistName = criteria.artistName;          results.Add(dto);          results.Add(dto);       }       return results;    } 

The implementation of the CatalogServiceStub is not very sophisticated because it does not have to be. The goal is to simulate a couple of scenarios, and this is sufficient.

Binding the Results to a Repeater Web Control

The task description states that we have to bind the search results to a Repeater Web control. The data source for the Repeater can be anything that implements the IEnumerable interface. Because ArrayList does implement IEnumerable , we don t have any problem with that. However, we can t use the RecordingDto class as the item in the ArrayList because the Repeater Web control has to bind to public property fields on the object, and the RecordingDto has public member variables ”not properties. Because the RecordingDto is generated using xsd.exe, we need to create another object to adapt the RecordingDto to something that can be used by the Repeater Web control. We will call this class the RecordingDisplayAdapter , and its responsibility will be to provide public properties for the fields that will be displayed onscreen.

Here are the tests:

 [TestFixture] public class RecordingDisplayAdapterFixture {    private RecordingDto dto = new RecordingDto();    private RecordingDisplayAdapter adapter;     [SetUp]    public void SetUp()    {       dto.id = 42;       dto.title = "Fake Title";       dto.labelName = "Fake Label Name";       dto.artistName = "Fake Artist Name";       dto.averageRating = 5;        adapter = new RecordingDisplayAdapter(dto);    }    [Test]    public void VerifyTitle()    {       Assert.AreEqual(dto.title, adapter.Title);    }    [Test]    public void VerifyArtistName()    {       Assert.AreEqual(dto.artistName, adapter.ArtistName);    }    [Test]    public void VerifyAverageRating()    {       Assert.AreEqual(dto.averageRating, adapter.AverageRating);    }    [Test]    public void VerifyId()    {       Assert.AreEqual(dto.id, adapter.Id);    }    [Test]    public void VerifyLabelName()    {       Assert.AreEqual(dto.labelName, adapter.LabelName);    } } 

The resulting implementation of RecordingDisplayAdapter is as follows:

 public class RecordingDisplayAdapter {    private RecordingDto dto;    public RecordingDisplayAdapter(RecordingDto dto)    {       this.dto = dto;    }    public string Title    {        get { return dto.title; }     }    public string ArtistName    {       get { return dto.artistName; }     }    public string LabelName    {       get { return dto.labelName; }     }    public int AverageRating    {       get { return dto.averageRating; }     }    public long Id    {       get { return dto.id; }    } } 

RecordingDisplayAdapter is an example of a design pattern called Adapter. [1] It has a single responsibility: to adapt the RecordingDto so it can be bound to the Repeater Web control. Now we need to convert the list of RecordingDto objects that we get back from the CatalogServiceStub into a list of RecordingDisplayAdapter objects. Let s modify the SearchFixture tests to expect a list of RecordingDisplayAdapter objects instead of RecordingDto objects.

Here are the tests with the changes in boldface:

 [TestFixture] public class SearchFixture {    [Test]    public void SearchById()    {       SearchCriteria criteria = new SearchCriteria();       criteria.id = 42;  CatalogServiceGateway gateway =   new CatalogServiceGateway();   ArrayList results = gateway.Search(criteria);  Assert.AreEqual(1, results.Count);  RecordingDisplayAdapter adapter =   (RecordingDisplayAdapter)results[0];   Assert.AreEqual(criteria.id, adapter.Id);  }    [Test]    public void SearchByArtistName()    {       SearchCriteria criteria = new SearchCriteria();       criteria.artistName = "Fake Artist Name";  CatalogServiceGateway gateway =   new CatalogServiceGateway();   ArrayList results = gateway.Search(criteria);  Assert.AreEqual(2, results.Count);  foreach(RecordingDisplayAdapter adapter in results)   Assert.AreEqual(criteria.artistName, adapter.ArtistName);  } } 

Because the CatalogServiceStub returns a list of RecordingDto objects, we have to introduce another class, CatalogServiceGateway , whose responsibility is to call the CatalogServiceStub to retrieve the list of RecordingDto objects and turn it into a list of RecordingDisplayAdapter objects.

Here is the CatalogServiceGateway class that makes the tests pass:

 public class CatalogServiceGateway    {       public ArrayList Search(SearchCriteria criteria)       {          ArrayList results = new ArrayList();          CatalogServiceStub stub = new CatalogServiceStub();          ArrayList dtos = stub.Search(criteria);          foreach(RecordingDto dto in dtos)          {             RecordingDisplayAdapter adapter =                 new RecordingDisplayAdapter(dto);             results.Add(adapter);          }          return results;       }    } 

When we compile and run the tests, they all pass, so we can move on.

Creating the Page

You might wonder when we will actually write the page. What we have done so far is to implement the functionality that the page uses in a testable way. Now we need to write the page that uses this tested functionality. We use the tools in Visual Studio to lay out the page.

The following is the code-behind page that Visual Studio creates:

 public class SearchPage : System.Web.UI.Page    {       protected System.Web.UI.WebControls.Label idLabel;       protected System.Web.UI.WebControls.Label titleLabel;       protected System.Web.UI.WebControls.Label artistNameLabel;       protected System.Web.UI.WebControls.Label averageRatingLabel;       protected System.Web.UI.WebControls.Label labelNameLabel;       protected System.Web.UI.WebControls.TextBox recordingId;       protected System.Web.UI.WebControls.TextBox title;       protected System.Web.UI.WebControls.TextBox artistName;       protected System.Web.UI.WebControls.TextBox labelName;       protected System.Web.UI.WebControls.RadioButtonList averageRating;       protected System.Web.UI.WebControls.Button searchButton;       protected System.Web.UI.WebControls.Button cancelButton;       protected System.Web.UI.WebControls.Repeater searchResults;           private void Page_Load(object sender, System.EventArgs e)       {          // Put user code to initialize the page here       }       // Web Form Designer generated code       private void SearchButtonClick(object sender, System.EventArgs e)       {       }    } 

We compile this page and then display the page in the browser. The page displays correctly, but when we push the Search button, nothing happens because we have not written it yet. Let s correct that by writing the SearchButtonClick method by just using the recordingId field:

 private void SearchButtonClick(object sender, System.EventArgs e)    {       long idValue = Int64.Parse(recordingId.Text);       SearchCriteria criteria = new SearchCriteria();       criteria.id = idValue;       searchResults.DataSource = gateway.Search(criteria);       searchResults.DataBind();    } 

This method is responsible for translating the text box fields onscreen into the SearchCriteria class and then calling the CatalogServiceGateway to get the results. We hope it feels strange that we did not implement a test before we wrote this code. We had to do it because there is no way to write this test within the context of ASP.NET. However, we can separate this translation code into a helper class and test this helper class outside the context of ASP.NET.

Here are the tests:

 [TestFixture] public class SearchPageHelperFixture {    private static readonly string idText = "42";    private static readonly string titleText = "Fake Title";    private static readonly string artistNameText = "Fake Artist Name";    private static readonly string averageRating = "3";    private static readonly string labelText = "Fake Label Name";    private SearchCriteria criteria;    private SearchPageHelper helper = new SearchPageHelper();     [SetUp]    public void SetUp()    {       criteria = helper.Translate(idText, titleText, artistNameText, averageRating,           labelText);       }    [Test]    public void VerifyId()    {       Assert.AreEqual(Int64.Parse(idText), criteria.id);    }    [Test]    public void VerifyTitle()    {       Assert.AreEqual(titleText, criteria.title);    }    [Test]    public void VerifyLabel()    {       Assert.AreEqual(labelText, criteria.labelName);    }    [Test]    public void VerifyArtistName()    {       Assert.AreEqual(artistNameText, criteria.artistName);    }    [Test]    public void VerifyAverageRating()    {       Assert.AreEqual(Int32.Parse(averageRating),                  criteria.averageRating);    } } 

The SearchPageHelper implementation is as follows:

 public class SearchPageHelper    {       public SearchCriteria Translate(string id, string title, string artistName,           string averageRating, string labelName)       {          SearchCriteria criteria = new SearchCriteria();          criteria.id = Int64.Parse(id);          criteria.title = title;          criteria.labelName = labelName;          criteria.artistName = artistName;          criteria.averageRating = Int32.Parse(averageRating);          return criteria;       }    } 

What we did is split out code that is usually done in the code-behind page into a separate class that does the translation so that we can test it. Let s modify the SearchButtonClick method to use the newly created SearchPageHelper class:

 private void SearchButtonClick(object sender, System.EventArgs e)    {       SearchCriteria criteria = helper.Translate(recordingId.Text, title.Text, artistName.Text,           averageRating.SelectedValue, labelName.Text);       searchResults.DataSource = gateway.Search(criteria);       searchResults.DataBind();    } 

When we open the browser to test the page, it works correctly, but we have to put values in the recordingId field and select one of the average rating radio buttons . If we do not put values in the recordingId field or fail to select one of the average rating radio buttons, the page fails because we cannot parse the text fields into numbers . Let s add some tests in the SearchPageHelperFixture class that captures these requirements:

 [Test]    public void IdFieldNotSpecified()    {       criteria = helper.Translate(null, titleText, artistNameText, averageRating, labelText);          Assert.AreEqual(0, criteria.id);    }    [Test]    public void AverageRatingFieldNotSpecified()    {       criteria = helper.Translate(null, titleText, artistNameText, null, labelText);          Assert.AreEqual(0, criteria.averageRating);    } 

The corresponding change to the SearchPageHelper class is in boldface in the following code:

 public class SearchPageHelper    {       public SearchCriteria Translate(string id, string title, string artistName,           string averageRating, string labelName)       {          SearchCriteria criteria = new SearchCriteria();  try   {   criteria.id = Int64.Parse(id);   }   catch(Exception)   {   criteria.id = 0;   }  criteria.title = title;          criteria.labelName = labelName;          criteria.artistName = artistName;  try   {   criteria.averageRating = Int32.Parse(averageRating);  }          catch(Exception)          {             criteria.averageRating = 0;          }          return criteria;       }    } 

When we look at the code in the SearchButtonClick method, we see that there isn t anything else we can extract from this method to write programmer tests because the rest of the code depends on the ASP.NET environment. What we have done, though, is make the code that is not testable with NUnit as small as possible. We will have to test the rest manually with the browser or use a testing tool that simulates the browser environment.

Enough of This Stub

The code that we implemented uses the CatalogServiceStub . It s about time to see whether the code will work with the real service layer implementation. Looking at the implementation of the CatalogServiceGateway , you will see that it is hard-wired to use the CatalogServiceStub . We need a way to specify the CatalogServiceStub when we execute the programmer tests and another class that calls the real CatalogService when we execute the customer tests.

The current implementation of CatalogServiceGateway is as follows:

 public class CatalogServiceGateway    {       public ArrayList Search(SearchCriteria criteria)       {          ArrayList results = new ArrayList();          CatalogServiceStub stub = new CatalogServiceStub();          ArrayList dtos = stub.Search(criteria);          foreach(RecordingDto dto in dtos)          {             RecordingDisplayAdapter adapter =                 new RecordingDisplayAdapter(dto);             results.Add(adapter);          }          return results;       }    } 

Clearly, we can t have this class instantiate the CatalogServiceStub . So how can we make this switch invisible to the code in the SearchPage.aspx.cs ? The simplest way to do this is to have the CatalogServiceGateway class defer the retrieval of the search results to a derived class. The changes are in boldface:

 public  abstract  class CatalogServiceGateway    {       public ArrayList Search(SearchCriteria criteria)       {          ArrayList results = new ArrayList();  ArrayList dtos = GetDtos(criteria);  foreach(RecordingDto dto in dtos)          {             RecordingDisplayAdapter adapter =                 new RecordingDisplayAdapter(dto);             results.Add(adapter);          }          return results;       }  protected abstract ArrayList GetDtos(SearchCriteria criteria);  } 

Now, derived classes will have to implement the GetDtos method. When we try to compile it, it fails because CatalogServiceGateway is an abstract class and can no longer be instantiated . We need to modify the CatalogServiceStub to inherit from CatalogServiceGateway to implement the GetDtos method for the tests.

Here is the updated version with the changes in boldface:

 public class CatalogServiceStub :  CatalogServiceGateway  {  protected override ArrayList GetDtos(SearchCriteria criteria)  {       ArrayList results = new ArrayList();       if(criteria.id != 0)       {          RecordingDto dto = new RecordingDto();          dto.id = criteria.id;          results.Add(dto);       }       else if(criteria.artistName != null)        {          RecordingDto dto = new RecordingDto();          dto.artistName = criteria.artistName;          results.Add(dto);          results.Add(dto);       }       return results;    } } 

When this change is made, the SearchFixture tests no longer compile because they try to instantiate the CatalogServiceGateway . The changes are in boldface in the following code:

 [TestFixture] public class SearchFixture {    [Test]    public void SearchById()    {       SearchCriteria criteria = new SearchCriteria();       criteria.id = 42;  CatalogServiceStub stub = new CatalogServiceStub();  ArrayList results =  stub.Search(criteria);  Assert.AreEqual(1, results.Count);       RecordingDisplayAdapter adapter =           (RecordingDisplayAdapter)results[0];       Assert.AreEqual(criteria.id, adapter.Id);    }    [Test]    public void SearchByArtistName()    {       SearchCriteria criteria = new SearchCriteria();       criteria.artistName = "Fake Artist Name";  CatalogServiceStub stub = new CatalogServiceStub();  ArrayList results =  stub.Search(criteria);  Assert.AreEqual(2, results.Count);       foreach(RecordingDisplayAdapter adapter in results)          Assert.AreEqual(criteria.artistName, adapter.ArtistName);    } } 

The last change we need to make is to the SearchPage.aspx.cs class; we will change CatalogServiceGateway to CatalogServiceStub so that the page will compile. The program now works as it did before we started making this change. Now that we have the code back to a stable state, we can implement another class that derives from CatalogServiceGateway and makes the real call to the CatalogService .

Here is the CatalogServiceImplementation class:

 public class CatalogServiceImplementation : CatalogServiceGateway    {       private CatalogService service = new CatalogService();       protected override ArrayList GetDtos(SearchCriteria criteria)       {          return service.Search(criteria);       }    } 

After this code compiles, we can change the SearchPage.aspx.cs class to instantiate the CatalogServiceImplementation class, and the Web page will use the CatalogService class in the service layer and search the database. The following is the updated SearchPage.aspx.cs class with the change in boldface:

 public class SearchPage : System.Web.UI.Page    {       protected System.Web.UI.WebControls.Label idLabel;       protected System.Web.UI.WebControls.Label titleLabel;       protected System.Web.UI.WebControls.Label artistNameLabel;       protected System.Web.UI.WebControls.Label averageRatingLabel;       protected System.Web.UI.WebControls.Label labelNameLabel;       protected System.Web.UI.WebControls.TextBox recordingId;       protected System.Web.UI.WebControls.TextBox title;       protected System.Web.UI.WebControls.TextBox artistName;       protected System.Web.UI.WebControls.TextBox labelName;       protected System.Web.UI.WebControls.RadioButtonList averageRating;       protected System.Web.UI.WebControls.Button searchButton;       protected System.Web.UI.WebControls.Button cancelButton;       protected System.Web.UI.WebControls.Repeater searchResults;  private CatalogServiceGateway gateway =   new CatalogServiceImplementation();  private SearchPageHelper helper = new SearchPageHelper();           private void Page_Load(object sender, System.EventArgs e)       {          // Put user code to initialize the page here       }       // Web Form Designer generated code              private void SearchButtonClick(object sender, System.EventArgs e)       {          SearchCriteria criteria = helper.Translate(recordingId.Text, title.Text, artistName.Text,              averageRating.SelectedValue, labelName.Text);          searchResults.DataSource = gateway.Search(criteria);          searchResults.DataBind();       }    } 

When we recompile and bring the page up in the browser, we can search the database using the SearchPage .

[1] E. Gamma, et al. Design Patterns , Addison-Wesley, 1995.




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