views:

152

answers:

2

Hi there,

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?

+2  A: 

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
"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
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
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
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
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
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
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
+1  A: 

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
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
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
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