Vishful thinking…

Helper classes for mock testing WebClient

Posted in Uncategorized by viswaug on May 17, 2010

The WebClient class in .NET and Silverlight is used for fetching (GET) resources from the web or sometimes locally. This scenario inherently does not lend itself very well to unit testing since it means the component being written is dependent on an external resource. Unit testing seeks to test the component by itself and eliminate the request to the external resource. Mock testing comes to our rescue in such scenarios by letting us mock the touch-points where the component interfaces with the external resource. But, the use of WebClient to make the request to the external resource makes mocking tricky, since WebClient does not implement interfaces or virtual methods to expose its functionality. Most of the widely used mocking frameworks can only mock members of an interface or virtual methods. To overcome this short-comming, I have created and used the following helpers classes to help mock web requests made using WebClient. It consists of a ‘WrappedWebClient’ class that wraps the methods supported by WebClient and implements interfaces defined alongside to help with the mocking of those methods.

The sample below contains a service class that fetches stories from the Digg web service using a search term.

  1. public interface IServiceRequest<T, K>
  2. {
  3.     void Run( T input );
  4.  
  5.     event EventHandler<ServiceRequestEventArgs<K>> SelectionCompleted;
  6.  
  7.     object State
  8.     {
  9.         get;
  10.         set;
  11.     }
  12. }

  1. public class DiggSearchService : IServiceRequest<string, IEnumerable<DiggStory>>
  2. {
  3.     string template = http://services.digg.com/search/stories?query={0}&count={1}&appkey=http://www.vishcio.us”;
  4.  
  5.     public IWebDownloader WebDownloader
  6.     {
  7.         get;
  8.         set;
  9.     }
  10.  
  11.     public int StoryCount
  12.     {
  13.         get;
  14.         set;
  15.     }
  16.  
  17.     public DiggSearchService()
  18.     {
  19.         WebDownloader = new WrappedWebClient();
  20.         StoryCount = 10;
  21.     }
  22.  
  23.     #region IServiceRequest<string,IEnumerable<DiggStory>> Members
  24.  
  25.     public void Run( string input )
  26.     {
  27.         WebDownloader.DownloadStringCompleted += new EventHandler<WrappedDownloadStringCompletedEventArgs>( WebDownloader_DownloadStringCompleted );
  28.  
  29.         Uri address = new Uri(string.Format(template, input,StoryCount));
  30.         WebDownloader.DownloadStringAsync( address, State );
  31.     }
  32.  
  33.     void WebDownloader_DownloadStringCompleted( object sender, WrappedDownloadStringCompletedEventArgs e )
  34.     {
  35.         if( e.Error != null )
  36.         {
  37.             RaiseSelectionCompletedEvent( null, e.Error );
  38.             return;
  39.         }
  40.  
  41.         XDocument xmlStories = XDocument.Parse( e.Result );
  42.  
  43.         IEnumerable<DiggStory> stories = from story in xmlStories.Descendants( “story” )
  44.                       select new DiggStory
  45.                       {
  46.                           Id = ( int ) story.Attribute( “id” ),
  47.                           Title = ( ( string ) story.Element( “title” ) ).Trim(),
  48.                           Description = ( ( string ) story.Element( “description” ) ).Trim(),
  49.                           HrefLink = new Uri( ( string ) story.Attribute( “link” ) ),
  50.                           NumDiggs = ( int ) story.Attribute( “diggs” ),
  51.                           UserName = ( string ) story.Element( “user” ).Attribute( “name” ).Value,
  52.                       };
  53.         RaiseSelectionCompletedEvent( stories, null );
  54.     }
  55.  
  56.     public event EventHandler<ServiceRequestEventArgs<IEnumerable<DiggStory>>> SelectionCompleted;
  57.  
  58.     public object State
  59.     {
  60.         get;
  61.         set;
  62.     }
  63.  
  64.     #endregion
  65.  
  66.     private void RaiseSelectionCompletedEvent( IEnumerable<DiggStory> data, Exception ex )
  67.     {
  68.         if( SelectionCompleted != null )
  69.             SelectionCompleted( this, new ServiceRequestEventArgs<IEnumerable<DiggStory>>( data, ex ) );
  70.     }
  71. }

The service class above uses the WrappedWebClient instead of the WebClient class itself. This enables the service class to be unit tested by mocking out the web request to the digg service. The WrappedWebClient implements the following interfaces

  • IWebDownloader
  • IWebUploader
  • IWebReader
  • IWebWriter

This enables us to mock the various types of operations that can be performed by the WebClient. The Digg service class above exposes an IWebDownloader property which makes it clear that the service class uses the ‘DownloadStringAsync’ method on the WebClient to access the service. To test the service class above, a sample response for the request can be downloaded beforehand and saved as a XML file. This file can then be compiled as a ‘Resource’ into the test assembly. During the test, the XML file can be retrieved from the test assembly and can be used as the return value from the mock object for the IWebDownloader interface. The Silverlight unit test below illustrates how the service class can be unit tested using the helper classes and the Silverlight mocking framework ‘Moq’.

  1. [TestClass]
  2. public class DiggSearchServiceTest : SilverlightTest
  3. {
  4.     [Asynchronous]
  5.     [TestMethod]
  6.     public void DiggSearchServiceSuccessTest()
  7.     {
  8.         DiggSearchService target = new DiggSearchService();
  9.  
  10.         StringBuilder sb = new StringBuilder();
  11.         StringWriter sw = new StringWriter( sb );
  12.         XDocument.Load( “/WrappedWebClient;component/Tests/stories.xml” ).Save( sw );
  13.  
  14.         var mock = new Mock<IWebDownloader>();
  15.         mock.Setup( foo => foo.DownloadStringAsync( null ) ).Verifiable();
  16.         target.WebDownloader = mock.Object;
  17.  
  18.         bool isLoaded = false;
  19.         IEnumerable<DiggStory> result = null;
  20.         target.SelectionCompleted += ( sender, e ) =>
  21.         {
  22.             isLoaded = true;
  23.             //Get the results from the service
  24.             result = e.EventData;
  25.         };
  26.         target.Run( “LSU” );
  27.  
  28.         EnqueueDelay( 1000 );
  29.         //Mock the service request results
  30.         EnqueueCallback( () => mock.Raise
  31.             (
  32.                 bar => bar.DownloadStringCompleted += null,
  33.                 new WrappedDownloadStringCompletedEventArgs( sb.ToString(), null, false, null )
  34.             ) );
  35.  
  36.         EnqueueConditional( () => isLoaded );
  37.         EnqueueCallback( () => Assert.IsNotNull( result ) );
  38.         EnqueueCallback( () => Assert.IsTrue( result.Count<DiggStory>() == 10 ) );
  39.         EnqueueTestComplete();
  40.     }
  41. }

Suggestions welcome…

Download the source code with the helper classes here

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: