It looks as if we need to have a page in which the
This implementation is really two separate
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
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
public struct SearchCriteria
{
public long id;
public string artistName;
public string title;
public string labelName;
public int averageRating;
}
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.
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
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
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.
You might
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
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
[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.
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
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
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.