Saturday, February 16, 2008

Yield Return is Evil :-(Sometimes)

If you don't already know Justice Gray has put the word out that if don't read Mo's Khan Blog you are a loser. I would like to let everybody know that I read Mo's blog. Mo recently wrote up a great post about testing and deferred execution. Even though I was well informed by Mo's blog post I still fell victim to the evils of deferred execution.

I have decided to include the actual code that I was working on. I came to the conclusion that although a contrived example is a great tool for learning a real world example is better suited to exhibit how you can fall prey to deferred execution in the wild.

The Test Failure Message:

IMapper`2.MapFrom(any); Expected #1, Actual #0.
   at Rhino.Mocks.MockRepository.VerifyAll()
   at Rhino.Mocks.PlaybackModeChanger.Dispose()
   at...

The failure message simple states that the expected method MapFrom was never invoked.

The Test:

private MockRepository mockery;

private IXmlToIndustryCodesMapper mockXmlToIndustryCodeMapper;

 

[SetUp]

public void Setup()

{

    mockery = new MockRepository();

    mockXmlToIndustryCodeMapper = mockery.DynamicMock<IXmlToIndustryCodesMapper>();

}

 

public IXmlToWcbAccountMapper CreateSUTWithMock()

{

    return new XmlToWcbAccountMapper(mockXmlToIndustryCodeMapper);

}

[Test]

public void Should_leverage_the_industry_codes_mapper()

{

    StringBuilder xml = new StringBuilder();

 

    WcbAccountElementDefintion wcbAccountElementDefintion = new WcbAccountElementDefintion();

 

    string accountNumber = "123";

 

    xml.Append(wcbAccountElementDefintion.OpenTag);

    xml.Append(wcbAccountElementDefintion.AccountNumberElement.CreateElementFor(accountNumber));

    xml.Append(wcbAccountElementDefintion.CloseTag);

 

 

    using (mockery.Record()) {

        Expect.Call(mockXmlToIndustryCodeMapper.MapFrom(null)).IgnoreArguments().Return(new List<IIndustryCode>());

    }

 

    using (mockery.Playback()) {

        IEnumerable<IWcbAccount> wcbAccounts = CreateSUTWithMock().MapFrom(new XmlElementFactory().From(xml.ToString()));

    }

}

The implementation:

public IEnumerable<IWcbAccount> MapFrom(IXmlElement input)

{

    WcbAccountElementDefintion wcbAccountElementDefintion = new WcbAccountElementDefintion();

 

    foreach (IXmlElement wcbAccountElement in Parse.Xml(input).AllElementsNamed(wcbAccountElementDefintion.Name))

    {

        string accountNumber = Parse.Xml(wcbAccountElement).ForValueOfElement<string>     

                               (wcbAccountElementDefintion.AccountNumberElement.Name);

 

        IEnumerable<IIndustryCode> industryCodes = xmlToIndustryCodeMapper.MapFrom(wcbAccountElement);

 

        yield return new WcbAccount(accountNumber, industryCodes);

    }

}

The Problem:

The reason that this code fails is because of deferred execution. The following line is our culprit.

yield return new WcbAccount(accountNumber, industryCodes);

Simply calling MapFrom on the XmlToWcbAccountMapper will not invoke the MapFrom method.

IEnumerable<IWcbAccount> wcbAccounts = CreateSUTWithMock().MapFrom(new XmlElementFactory().From(xml.ToString()));

 

It's kind of like the wcbAccounts collection has a pointer to the MapFrom method. Once the Enumerator moves to the first item the MapFrom method will be invoked.

 

Since wcbAccounts is waiting to be iterated over the following line will never be called.

 

IEnumerable<IIndustryCode> industryCodes = xmlToIndustryCodeMapper.MapFrom(wcbAccountElement);

 

Actually none of the lines in the MapFrom Method will be invoked

 

The Solution:

 

The solve this problem we need to somehow cause the MapFrom Method to be invoked.

 

The first thing that comes to mind is that we could just loop over the collection of items.

 

foreach (IWcbAccount account in wcbAccounts) {}

 

If you were just interested in invoking the first call to yield you could do the following:

 

CreateSUTWithMock().MapFrom(new XmlElementFactory().From(xml.ToString())).GetEnumerator().MoveNext();

 

By moving to the fist element in the enumerator we are forcing the MapFrom method to be invoked.

 

To make this solution a little cleaner we could just create a new collection and pass the enumerable in as a parameter in the constructor. This would cause the new list to iterate over the collection.

 

new List<IWcbAccount>(wcbAccounts);

 

This is the final line of code that I ended up with:

 

new List<IWcbAccount>(CreateSUTWithMock().MapFrom(new XmlElementFactory().From(xml.ToString())));

 

Even though deferred execution has a few pitfalls it really is a powerful feature of the .Net Framework.

5 comments:

Mr. mO said...

Awesome post Mr. Adam!

You're so right, a real life example works so much better then a contrived one!

Thomas pedro said...

Help is given every year as a piece allow in light of a designation equation for Native American tribes with affirmed IHPs. Tribes that present an IHP are granted piece awards. Qualified Alaska Native towns and Native American tribes choose who will get the square allow. payday loans

marko said...

The scope of choices open to those looking for home advances with terrible credit is very broad, on account of the development of the internet loaning segment. Customary loan specialists are known to be the slightest moderate alternative as they charge higher financing costs, however they additionally have stricter terms and punishment structures as well. Check Cashing

marko said...

In the event that you have a not all that amazing FICO assessment, securing this sort of advance against your house is the most ideal route for you to procure a low intrigue and APR. You utilize the value you get from your home to pay off all you're existing obligations. At that point you are left with just your home loan to manage. cash advance

marko said...

So as to change your home credit you should give your bank's misfortune moderation division with an application for a home advance adjustment and a Hardship letter, which clarifies your conditions. You will likewise need to furnish the bank with evidence that you will have the capacity to pay the new installments. check cashing