Data Transformation


The first step is to build a stub to isolate the Web service implementation from the data access code. We need a stub class because we want to hide how the RecordingDataSet is retrieved. The stub will return a RecordingDataSet that is built in- memory as opposed to one retrieved from the database. Here is the first test:

 public class CatalogServiceStubFixture {    private RecordingDataSet.Recording recording;    private RecordingDataSet.Recording actual;     private CatalogServiceStub service;     [SetUp]    public void SetUp()    {       recording = CreateRecording();       service = new CatalogServiceStub(recording);       actual = service.FindByRecordingId(recording.Id);    }    private RecordingDataSet.Recording CreateRecording()    {       RecordingDataSet dataSet = new RecordingDataSet();       RecordingDataSet.Recording recording =           dataSet.Recordings.NewRecording();       recording.Id = 1;       /* more code to fill in the rest of the recording */       return recording;    }    [Test]    public void CheckId()    {       Assert.AreEqual(recording.Id, actual.Id);    } } 

Given this test, here is the resulting CatalogServiceStub implementation:

 public class CatalogServiceStub    {       private RecordingDataSet.Recording recording;        public CatalogServiceStub(RecordingDataSet.Recording recording)       {          this.recording = recording;       }       public RecordingDataSet.Recording FindByRecordingId(long id)       {          return recording;       }    } 

When we compile and run the test, it succeeds. Let s move on to the next test, which simulates an error condition.

 [Test]    public void InvalidId()    {       RecordingDataSet.Recording nullRecording =           service.FindByRecordingId(2);        Assert.IsNull(nullRecording, "should be null");    } 

When we compile and run this test, it fails because the stub has no error- checking code contained in it. Does it make sense to put error-checking code in stub classes? Yes and no. Yes because it helps us explore what the real interface to a class might be with methods that return errors. No because you can get trapped into simulating very complex behavior for no apparent reason. We will simulate the error condition in the stub and we will see if the implementation is too complicated. Here s the code that makes the InvalidId test succeed:

 public RecordingDataSet.Recording FindByRecordingId(long id)    {       if(id != recording.Id) return null;       return recording;    } 

This code is certainly not too complicated, so it is probably all right to have this level of error-checking in this class. The tests pass, so let s turn our attention to the interoperability requirement.

Data Transfer Object

What should the DTO look like? We have a requirement to be platform-independent. We also have the need to hide the actual database schema from the clients . Lastly, we have to add some calculated fields, totalRunTime and averageReview , that are not present in the database. Figure 6-1 depicts the RecordingDto :

click to expand
Figure 6-1: Data transfer object

In Figure 6-1, there are only three complex types defined in the schema: RecordingDto , TrackDto , and ReviewDto . When you look at the database schema, you see seven tables (Recording, Track, Review, Reviewer, Artist, Genre , and Label). We don t want to expose our client to the full complexity of the data model. (In a more realistic application, we could have hundreds of tables in the database schema, and many of them could be involved in a web of complex constrained relationships.) The reduction in complexity on the client side simplifies the mapping and the amount of data we need to move across the wire; in addition, the extra level of mapping allows us to customize the presentation of the data for particular client needs that also leads to data amount reduction. If you look again at Figure 6-1 and compare it to the data model for the typed DataSet , you notice the following differences:

  • The RecordingDto has a flattened representation of label and artist information because we do not need to expose the data normalization artifacts on the client. ( ReviewDto and TrackDto have similar simplifications for Reviewer, Artist, and Genre information.)

  • There are two additional fields on RecordingDto : totalRunTime and averageRating (these fields are calculated).

  • The type of releaseDate element on the RecordingDto is a string, not DateTime as it is on the RecordingDataSet; the client does not need the flexibility that the DateTime class provides because the client merely displays the data.

To specify this DTO in a platform-independent way, we write it using XML Schema. The Web Services Description Language (WSDL) specifies the use of XML Schema for maximum interoperability and platform neutrality. Here is Figure 6-1 expressed as an XML Schema:

Recording.xsd

 <?xml version="1.0" encoding="utf-8" ?> <xs:schema xmlns:tns="http://nunit.org/webservices" elementFormDefault="qualified"  targetNamespace="http:// nunit.org/ webservices" xmlns:xs="http://www.w3.org/2001/XMLSchema"> <xs:element name="Recording" type="tns:RecordingDto" /> <xs:complexType name="RecordingDto">    <xs:sequence>       <xs:element minOccurs="1" maxOccurs="1" name="id" type="xs:long" />       <xs:element minOccurs="1" maxOccurs="1" name="title" type="xs:string" />       <xs:element minOccurs="1" maxOccurs="1" name="artistName"         type="xs:string" />       <xs:element minOccurs="1" maxOccurs="1" name="releaseDate"         type="xs:string" />       <xs:element minOccurs="1" maxOccurs="1" name="labelName"          type="xs:string" />       <xs:element minOccurs="0" maxOccurs="unbounded" name="tracks"          type="tns:TrackDto" />       <xs:element minOccurs="0" maxOccurs="unbounded" name="reviews"          type="tns:ReviewDto" />       <xs:element minOccurs="0" maxOccurs="1" name="totalRunTime"          type="xs:int" />       <xs:element minOccurs="0" maxOccurs="1" name="averageRating"          type="xs:int" />    </xs:sequence> </xs:complexType> <xs:complexType name="TrackDto">    <xs:sequence>       <xs:element minOccurs="1" maxOccurs="1" name="id" type="xs:long" />       <xs:element minOccurs="1" maxOccurs="1" name="title" type="xs:string" />       <xs:element minOccurs="1" maxOccurs="1" name="artistName"          type="xs:string" />       <xs:element minOccurs="1" maxOccurs="1" name="duration" type="xs:int" />       <xs:element minOccurs="1" maxOccurs="1" name="genreName"          type="xs:string" />    </xs:sequence> </xs:complexType> <xs:complexType name="ReviewDto">    <xs:sequence>       <xs:element minOccurs="1" maxOccurs="1" name="id" type="xs:long" />       <xs:element minOccurs="1" maxOccurs="1" name="reviewerName"          type="xs:string" />       <xs:element minOccurs="1" maxOccurs="1" name="rating" type="xs:int" />       <xs:element minOccurs="1" maxOccurs="1" name="reviewContent"          type="xs:string" />    </xs:sequence> </xs:complexType> </xs:schema> 

An additional benefit of using XML Schema is that we can generate C# code from the XML Schema for use inside the program. We use the xsd.exe tool to generate the code. Now that we have a DTO, we can modify the CatalogServiceStub to return the DTO instead of a RecordingDataSet . The changes to the CatalogServiceStubFixture are boldface in the following code:

 private RecordingDataSet.Recording recording;  private RecordingDto dto;  private CatalogServiceStub service;     [SetUp]    public void SetUp()    {       recording = CreateRecording();       service = new CatalogServiceStub(recording);  dto = service.FindByRecordingId(recording.Id);  }    [Test]    public void CheckId()    {       Assert.AreEqual(recording.Id,  dto.id  );    }    [Test]    public void InvalidId()    {  RecordingDto nullDto = service.FindByRecordingId(2);  Assert.IsNull(  nullDto  , "should be null");    } 

When we compile the modified tests, they fail because we did not change CatalogServiceStub . Let s change that now.

 public class CatalogServiceStub    {       private RecordingDataSet.Recording recording;        public CatalogServiceStub(RecordingDataSet.Recording recording)       {          this.recording = recording;       }       public  RecordingDto  FindByRecordingId(long id)       {          if(id != recording.Id) return null;  RecordingDto dto = new RecordingDto();   dto.id = recording.Id;   return dto;  }    } 

Prior to this test, we returned a RecordingDataSet.Recording . Now that we have to return a RecordingDto , we need to map the fields from the RecordingDataSet.Recording into the RecordingDto in the FindByRecordingId method. This code is not in the right place because it seems as if it will be needed somewhere else. For now, let s leave it here, but we will definitely come back at some point and fix it. The code compiles and the tests pass, so let s move on to the rest of the fields.

Checking the Title Field

The next test is to verify the title field. Here is the CheckTitle test in the CatalogServiceStubFixture :

 [Test] public void CheckTitle() {    Assert.AreEqual(recording.Title, dto.title); } 

The test for title fails because we have not mapped the title field on RecordingDto yet. We need to make the following change to the CatalogServiceStub :

 public RecordingDto FindByRecordingId(long id) {    if(id != recording.Id) return null;    RecordingDto dto = new RecordingDto();    dto.id = recording.Id;    dto.title = recording.Title;    return dto; } 

The test passes , and we can see the pattern for mapping the rest of the fields on the RecordingDto . As we mentioned previously, this code should not be in the stub because it will be needed by the service that talks to the database. So we need a place for this code that maps the RecordingDataSet into a RecordingDto . Martin Fowler refers to this concept as an Assembler. [2]

Building an Assembler

Both the Assembler and the CatalogServiceStubFixture have a need for an in- memory representation of the RecordingDataSet.Recording . The code to build that is contained in the CatalogServiceStubFixture , so we need to extract that code from there and put it in its own class so it can be shared. Let s create a new class called InMemoryRecordingBuilder , whose responsibility is to create in-memory recordings. Then we need to modify the CatalogServiceStubFixture to use the InMemoryRecordingBuilder . We run all the tests again to make sure that the refactoring did not break anything. The tests pass, so we can move on. The first test in the RecordingAssemblerFixture will be to verify that we can map the id field from the RecordingDataSet.Recording to the RecordingDto :

 [TestFixture] public class RecordingAssemblerFixture {    private RecordingDataSet.Recording recording;    private RecordingDto dto;    [SetUp]    public void SetUp()    {       recording = InMemoryRecordingBuilder.Make();       dto = RecordingAssembler.WriteDto(recording);    }    [Test]    public void Id()    {       Assert.AreEqual(recording.Id, dto.id);    } } 

This test code specifies that we need a class called RecordingAssembler to do the mapping from RecordingDataSet.Recording to the RecordingDto , and here is the implementation that satisfies the test:

 public class RecordingAssembler {    public static RecordingDto           WriteDto(RecordingDataSet.Recording recording)    {       RecordingDto dto = new RecordingDto();       dto.id = recording.Id;       return dto;    }  } 

Mapping of the title , artistName, and labelName are similar to the id field, so are not shown here in detail. The mapping of the releaseDate field brings up an interesting issue: the Assembler seems to not only map the data but also customize it for the specific presentation. For example, we chose to present the DateTime information on the client in the format mm/dd/yyyy, and the Assembler does this transformation. It seems that the culture-specific DateTime formatting is the client s responsibility. We will leave it this way for now and check with the customer to see whether it is correct.

Mapping the Relationships in the Assembler

Now we have mapped all the fields of the RecordingDto , and we are ready to move on to the mapping of the related tracks and reviews. The first test will be to count the number of tracks that are associated with the Recording. Here s the test code:

 [Test]    public void TrackCount()    {       Assert.AreEqual(recording.GetTracks().Length,   dto.tracks.Length);    } 

The test fails because we have not added the code for mapping tracks to the RecordingAssembler . We need to add the WriteTracks method to do the mapping:

 private static void WriteTracks(RecordingDto recordingDto,                      RecordingDataSet.Recording recording) {    recordingDto.tracks = new TrackDto[recording.GetTracks().Length];              int index = 0;    foreach(RecordingDataSet.Track track in recording.GetTracks())    {       recordingDto.tracks[index++] = new TrackDto();    } } 

We will also modify the WriteDto method of the RecordingAssembler to call this method. Now the tracks are mapped on the RecordingDto , but the TrackDto s that we get on the recording are empty. We want to separate these two test fixtures: RecordingDto mapping is tested by RecordingAssemblerFixture , and TrackDto mapping is tested by TrackAssemblerFixture . To support the isolated testing of TrackDto mapping, we will introduce a new method on the RecordingAssembler : WriteTrack . This method takes a RecordingDataSet.Track and maps it to a TrackDto . Here is the code for the TrackAssemblerFixture with tests for all track fields:

 [TestFixture] public class TrackAssemblerFixture {    private RecordingDataSet.Artist artist;    private RecordingDataSet.Genre genre;    private RecordingDataSet.Track track;    private TrackDto trackDto;    [SetUp]    public void SetUp()    {       RecordingDataSet recordingDataSet = new RecordingDataSet();       artist = recordingDataSet.Artists.NewArtist();       artist.Id = 1;       artist.Name = "Artist";       recordingDataSet.Artists.AddArtist(artist);       genre = recordingDataSet.Genres.NewGenre();       genre.Id = 1;       genre.Name = "Genre";       recordingDataSet.Genres.AddGenre(genre);       track = recordingDataSet.Tracks.NewTrack();       track.Id = 1;       track.Title = "Track Title";       track.Duration = 100;       track.Genre = genre;       track.Artist = artist;       recordingDataSet.Tracks.AddTrack(track);       trackDto = RecordingAssembler.WriteTrack(track);    }    [Test]    public void Id()    {       Assert.AreEqual(track.Id, trackDto.id);    }        [Test]    public void Title()    {       Assert.AreEqual(track.Title, trackDto.title);    }    [Test]    public void Duration()    {       Assert.AreEqual(track.Duration, trackDto.duration);    }        [Test]    public void GenreName()    {       Assert.AreEqual(genre.Name, trackDto.genreName);    }        [Test]    public void ArtistName()    {       Assert.AreEqual(artist.Name, trackDto.artistName);    } } 

Having added the tracks to the RecordingDto , we can write a test for the calculated field: totalRunTime . This field is the sum of the durations of all the tracks on the recording. The calculation of this field and the mapping are done on the RecordingAssembler by the WriteTotalRunTime method:

 private static void WriteTotalRuntime(RecordingDto dto,  RecordingDataSet.Recording recording) {    int runTime = 0;     foreach(RecordingDataSet.Track track in recording.GetTracks())    {       runTime += track.Duration;    }       dto.totalRunTime = runTime;  } 

This method does not seem to fit with the rest of the mapping code (it feels out of place here), but we have not yet found a better place for it. Let s keep it here for awhile, but note it ”both the releaseDate transformation and the totalRunTime calculations suggest that we need some other place for these intelligent mappings.

Mapping of relationships to Reviews is very similar to what we have done with the tracks, and we leave it as an exercise for the reader. The only interesting difference was the additional test we had to write to verify that if the recording does not have any reviews, the average rating is zero:

 [Test]    public void AverageRatingZero()    {       RecordingDataSet dataSet = new RecordingDataSet();       RecordingDataSet.Recording recording =           dataSet.Recordings.NewRecording();       recording.Id = 1;       recording.Title = "Title";       recording.ReleaseDate = DateTime.Today;       RecordingDataSet.Label label = dataSet.Labels.NewLabel();       label.Id = 1;       label.Name = "Label";       dataSet.Labels.AddLabel(label);       RecordingDataSet.Artist artist = dataSet.Artists.NewArtist();       artist.Id = 1;       artist.Name = "Artist";       dataSet.Artists.AddArtist(artist);       recording.Label = label;       recording.Artist = artist;       dataSet.Recordings.AddRecording(recording);       RecordingDto dto = RecordingAssembler.WriteDto(recording);       Assert.AreEqual(0, dto.averageRating);    } 

After we finish with this test, all the code compiles and the tests pass, so we can now go back to the CatalogServiceStub and modify it to use the RecordingAssembler :

 public RecordingDto FindByRecordingId(long id)    {       if(id != recording.Id) return null;       return RecordingAssembler.WriteDto(recording);    } 

The code compiles, and we rerun all the tests. They all pass, so we can safely move on to the code that uses the database instead of the stub.

[2] Martin Fowler, PEAA, page 405.




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