4

I am writing a small app to teach myself ASP.NET MVC, and one of its features is the ability to search for books at Amazon (or other sites) and add them to a "bookshelf".

So I created an interface called IBookSearch (with a method DoSearch), and an implementation AmazonSearch that looks like this

public class AmazonSearch : IBookSearch
{
   public IEnumerable<Book> DoSearch(string searchTerms)
   {  
      var amazonResults = GetAmazonResults(searchTerms);
      XNamespace ns = "http://webservices.amazon.com/AWSECommerceService/2005-10-05";
      var books= from item in amazonResults.Elements(ns + "Items").Elements(ns + "Item")
                 select new Book
                 {
                      ASIN = GetValue(ns, item, "ASIN"),
                      Title = GetValue(ns, item, "Title"),
                      Author = GetValue(ns, item, "Author"),
                      DetailURL = GetValue(ns, item, "DetailPageURL")
                 };
      return books.ToList();
  }

  private static XElement GetAmazonResults(string searchTerms)
  { 
      const string AWSKey = "MY AWS KEY";
      string encodedTerms = HttpUtility.UrlPathEncode(searchTerms);
      string url = string.Format("<AMAZONSEARCHURL>{0}{1}",AWSKey, encodedTerms);
      return XElement.Load(url);
  }

  private static string GetValue(XNamespace ns, XElement item, string elementName)
  {
     //Get values inside an XElement
  }

}

Ideally I would like to have done this TDD-style, writing first a test and all. But I gotta confess I am having trouble getting my head around it.

I could create a FakeSearch that implements DoSearch() and return some ad-hoc books, but I don't think that brings any value at the moment, does it? Maybe later when I have some code that uses a list of books.

What else could I test first? The only test I can think of would be one that mocks the call to the cloud (at GetAmazonResults) and then checks that DoSearch can execute the Linq2XML select correctly and return the correct list. But it seems to me that this type of test can only be written after I have some code in place so I know what to mock.

Any advice on how you guys and girls would go around doing this test-first style?

rodbv
  • 5,214
  • 4
  • 31
  • 31

3 Answers3

3

It seems that your main issue here is knowing when to write mock code. I see your point: if you haven't written the code yet, how can you mock it?

I think the answer is that you want to start your TDD with very, very simple tests, as Kent Beck does in Test Driven Development. Start by writing a test that calls DoSearch and asserts that what you receive isn't null, and write some code to make that pass. Then write a test that asserts that you're retrieving the proper number of Books for a known search term, and write the code to make that pass. Eventually you'll get to a point where you need to receive actual, valid Book data to pass a test, and at that point, you'll have a portion of DoSearch written, and you can think about mocking it (or portions of it).

MattK
  • 10,195
  • 1
  • 32
  • 41
  • "Then write a test that asserts that you're retrieving the proper number of Books for a known search term, and write the code to make that pass" I'm having trouble visualizing how such test would look like, in a way that I am testing proper code, not testing my test. – rodbv Jan 16 '09 at 19:11
  • Remember that you're developing incrementally, and that you're never doing any more work than is strictly necessary to pass a test. For that test you'll start by returning a completely fake list of books, but later, another test should require you to refactor that to a real query. – MattK Jan 16 '09 at 19:22
  • This is how TDD helps you make sure that you never (or at least, rarely) write unnecessary code. You always write the bare minimum needed to pass a test, with the idea that you'll refactor later while keeping all your tests passing. Hard to get the hang of, but makes elegant code. – MattK Jan 16 '09 at 19:24
  • So first I would create a test like DoSearch_DoesNotReturnNull, and this test would fail because I have absolutely no code yet (or a function that throws NotImplementedException, so it compiles). The second test, what would be its name and how would the code look like, and the code to pass it? – rodbv Jan 16 '09 at 19:53
  • PS: Maybe my last comment sounds a bit lazy, I don't expect anyone to actually write code, just a line explaning how it would look like... – rodbv Jan 16 '09 at 20:03
  • Right. Your first test wouldn't even compile. The code you'd write would be the implementation of DoSearch, which would probably just return an empty collection. Your second test might be something like DoSearch_ReturnsOneResultForKnownQuery, and you'd just modify the code to return 1 fake Book. – MattK Jan 16 '09 at 20:17
  • But as you test all the requirements and your tests get more and more sophisticated, you'll eventually end up writing all of DoSearch, just to make the tests pass. Basically, you test for more and more complex logic as you go. I highly recommend Kent Beck's book for a good description of this. – MattK Jan 16 '09 at 20:18
2

You'll want to write a mock when you're testing code that uses the search, not for testing the search itself.

For the class above, I might test by:

  • searching for a common book and checking that is was found and is valid.
  • searching for a random fixed string "kjfdskajfkldsajklfjafa" and making sure no books were found
  • etc

But.. and here's the big one, I'd never mock out a class I was testing, I'd mock out classes that it used.

Long story short: FakeSearch would be used when testing that the UI was working properly when the Search button was pressed. I could ensure that it was getting invoked, and that the UI was handling the returned books properly.

Hope that helps.

Allain Lalonde
  • 91,574
  • 70
  • 187
  • 238
  • Yes I agree about the mocking, it has to be stuff outside the scope of the class. So in my example above it looks like XElement.Load() would be the call to be mocked, is that right? – rodbv Jan 16 '09 at 19:56
  • Here's what I don't get yet: I would like to write a test that checks that a search for "Isaac Asimov Foundation" returns some books. But I can't think what could I write to make it pass that doesn't actually go to Amazon get some results, which means this big chunk of code to write all at once. – rodbv Jan 16 '09 at 20:12
  • It's a lot of code to test at once since: XElement.Load is static. I think that's a design flaw, and should be fixed by allowing the injection of an XElementLoader (new) that way you can make sure that your code is calling the underlying system correctly. – Allain Lalonde Jan 17 '09 at 16:37
0

In this class the main focus appears to be that it integrates correctly with Amazon's web services. Since that web service is not something you own, you shouldn't mock it, because you don't have intimate knowledge of how it works. "Only mock types you own", "don't mock third-party libraries" etc.

Here are some ways to approach the problem:

Write a test which connects to the real web service over the network, perhaps searching for some very popular book which you can trust will be around for years to come. This gives good assurance that you are using the service correctly, but it's also subject to many false positives - for example sometimes the network might be down or then the data in the remote system changes. Thus you will also need tests which...

Write tests against static data files, which are based on data from the real web service. To get the test data, you could manually do requests to the web service and write the responses to file*. You will need to mock the network connection (either using a stub which does no networking, or by starting up an embedded web server in the tests and connecting to it instead of the real URL). This way you can easily test all kinds of corner cases and error conditions, and the data will always be available and stay the same, regardless of what happens to the real web service. One caveat is that if the API of the real web service changes, these tests will not notice it, so you will also need some tests written against the real web service (as mentioned above).

* For example, once I used cron and a little shell script to download every few minutes data from a web service, which contained ever changing schedule information. Gathering such data for a period of a few weeks was very useful as test data. From that data I hand-crafted static responses which contained all kinds of special cases which I noticed in the real data. It was also useful for setting up a fake web service and a "time machine" which replayed that earlier captured data, so that our system could be used without access to the real web service.

Esko Luontola
  • 73,184
  • 17
  • 117
  • 128