共用方式為


Encapsulating Enumeration

While writing a unit test for a particular feature, I was faced with an interesting problem. My unit test has different scenarios. These scenarios test that a certain data item is propagated all the way down from the called function.

For eg, I have a method that I need to test - lets call it CFoo:DomSometing().

We pass in a data item to this method - so as part of the changes I change the method to CFoo::DoSomething(int data)

I need to make sure, in my tests, that the 'data' item is properly consumed by the function.

It turns out that for different functions that I call as part of each of my scenario, the way of checking is different. For eg, for one scenario, I have to call the function 'n' times, whereas for another scenario, I have to call the function for a particular duration.

I want to encapsulate all of this, so that I can write one test function that does the following

 

bool RunTest ( RunTestDelegate testMethod, int data, ILoopIterator iterator)
{
    foreach (int i in iterator.GetValues())
    {
         // call the test method
         testMethod (data);
    }
}

 

Here, the RunTestDelegate will be used to encapsulate each method that I need to test, so that I dont have to write one RunTestXXX method per each method I want to test.

The question is: how do you implement the ILoopIterator? Clearly, the first requirement is that it should be returning an IEnumerable<int> because that is what the foreach loop is iterating over.

Also, as discussed above, we need it so that we can iterate over both a range of values, as well as for a time duration.

In order to solve this, I implemented the interface as follows:

interface ILoopIterator
{
    IEnumerable<int> GetValues();
}

class LoopIterator : ILoopIterator
{
    private int count;
    private TimeSpan duration;
    private bool isCounted;
    private LoopIterator (TimeSpan duration)
    {
        this.duration = duration;
        this.count = -1;
        this.isCounted = false;
    }

    private LoopIterator (int iterationCount)
    {
        this.count = iterationCount;
        this.isCounted = true;
        this.duration = TimeSpan.MaxValue;
    }

    public static LoopIterator CreateTimed(TimeSpan duration)
    {
        return new LoopIterator (duration);
    }

    public static LoopIterator CreateCounted(int iterationCount)
    {
        return new LoopIterator (iterationCount);
    }

    public IEnumerable<int> GetValues()
    {
        if (this.isCounted)
        {
            for (int i = 0; i < this.count; i++)
            {
                yield return i;
            }
        }
        else
        {
            int i = 0;
            DateTime end = DateTime.Now + this.duration;
            while (DateTime.Now < end)
            {
                yield return i;
                ++i;
            }
        }
    }
}

This code used the .NET/2.0 C# features - i.e generics & yield statement for implementing enumerators.

The caller will use a different factory method, depending on how he wants the iteration to be performed.

For duration based iteration, use LoopIterator .CreateTimed(TimeSpan duration)

For normal iteration (like a for loop) use LoopIterator .CreateCounted(int count)

The utility of this pattern, is that the client (which is using ILoopIterator ) does not need to change!

Comments