During a practice coding session last night, I was writing some tests and ran into a small dilemma. I wanted to execute a method call in a loop. Wow, what a tough problem, right? :-) Anyways, my problem was that putting the method call in a for
loop sort of obscured the intention of the test. Unrolling the loop was clearer, but was a pain to write.
/* Unrolled loop. Pretty obvious what calls we are expecting. */ using (mockery.Record()) { renderer.WriteCurrentPage(1); renderer.WritePageLink(2); renderer.WritePageLink(3); renderer.WritePageLink(4); renderer.WritePageLink(5); } ... /*Less typing, but I'm expecting, um, 4 calls, oh I see, rendering pages 2 to 5 */ using ( mockery.Record()) { renderer.WriteCurrentPage(1); for (int page=2; page<=5; page++) { renderer.WritePageLink(page); } }
Parsing the second sample is admittedly pretty straight forward, but I found myself liking the first style as the scenarios being tested became more complicated. What I wanted was something a bit like the Python range() function. It works like this:
>>> print range(1,10) [1, 2, 3, 4, 5, 6, 7, 8, 9] >>> range(10, 1, -1) [10, 9, 8, 7, 6, 5, 4, 3, 2] >>> print sum(range(1,10)) 45 >>> print (map(lambda i: i * 10, range(1,10))) [10, 20, 30, 40, 50, 60, 70, 80, 90]
So I thought I’d code up something similar in C# for a bit of fun (depending, of course, on your definition of “fun”). Just to be clear, I wanted a different implementation to Python’s. In Python the range stops before the second parameter, so range(1, 3)
gives [1, 2]
. In my case I want [1, 2, 3]
.
A few provisos. I like provisos, because when someone points out my stupidity I can always say “It’s ok! I had provisos!”. First, this has probably been done before. Second, this is probably a bad idea, breaking normal loop semantics and so on. Third, this is probably a bad idea for reasons I haven’t thought of yet. But, in the name of fun (definition pending), I will proceed. You can download the code I ended up with if you want to have a play around with it.
.NET 3.5 version
This section rewritten on 14 February 2008 after reading Steve Harman’s post on System.Linq.Enumerable
. Jay Wren has a good post on this too.
Turns out that .NET 3.5 already has a range implementation that completely meets my basic requirements:
IEnumerable<int> oneToTen = Enumerable.Range(1, 10);
I did search the relevant documentation when I initially wrote this post, but didn’t find this .NET 3.5 addition until Steve Harman’s post. However, the implementation covered in the rest of this post does a bit more than the built in functionality: it lets you specify different step values, does "smart" default step sensing, uses a fairly fluent interface, and is also a reasonable TDD exercise.
This implementation can also be used as a base to generate non-integer ranges, like ranges of DateTime. Jon Skeet also wrote a Range implementation for his book (which I didn’t know when I first wrote this post), that he extended in just this way.
Test driving the Range class
Let’s write a few tests that show how we would like our implementation to work. (When I did this I did the tests and corresponding implementations one at a time, TDD-style. I also did them in a different order to that presented here. I’m combining stuff for the sake of brevity and coherency.)
[Test] public void Should_be_able_to_get_enumerator() { List<int> rangeValues = new List<int>(); foreach (int i in Range.From(1).To(2)) { rangeValues.Add(i); } Assert.That(rangeValues, Is.EqualTo(new int[] { 1, 2 })); } [Test] public void Should_be_able_to_get_range_from_1_to_10() { Assert.That(Range.From(1).To(10).ToArray(), Is.EqualTo(new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 })); }
The implementation to pass them looked a bit like this:
public class Range : IEnumerable<int> { private readonly int start; private int stop; public Range(int start) { this.start = stop = start; } public static Range From(int startRange) { return new Range(startRange); } public Range To(int endRange) { stop = endRange; return this; } public IEnumerator<int> GetEnumerator() { int current = start; while (current <= stop) { yield return current; current++; } } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } }
Right, before plunging into my goal of mapping method calls across this range, I started thinking about negative ranges. In Python you can use range(10, 1, -1)
to get [10, 9, 8, .., 2]
. Maybe we should have that? I don’t want to have to specify a step of -1 though. It should figure that out for itself.
[Test] public void Should_be_able_to_get_range_from_10_to_1() { Assert.That(Range.From(10).To(1).ToArray(), Is.EqualTo(new int[] { 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 })); }
And while we’re at it, let’s give users the option to specify a step if they really want to. They might want every second number in a range.
[Test] public void Should_be_able_to_set_a_step_value() { Assert.That(Range.From(1).To(10).WithStep(2).ToArray(), Is.EqualTo(new int[] { 1, 3, 5, 7, 9 })); }
We don’t want them getting OutOfMemoryException
S thrown while specifying steps though, so we’ll need to make it tough to specify an infinite range.
[Test] public void Should_handle_step_values_that_immediately_go_out_of_range() { Assert.That(Range.From(1).To(10).WithStep(-1).ToArray(), Is.EqualTo(new int[]{1})); Assert.That(Range.From(10).To(1).WithStep(2).ToArray(), Is.EqualTo(new int[] { 10 })); Assert.That(Range.From(-10).To(1).WithStep(-1).ToArray(), Is.EqualTo(new int[] { -10 })); }
So carefully, test at a time, we get an implementation for GetEnumerator<int>()
that looks a bit like this:
public class Range : IEnumerable<int> { ... private int step; public Range WithStep(int step) { this.step = step; return this; } ... public IEnumerator<int> GetEnumerator() { int max = Math.Max(start, stop); int min = Math.Min(start, stop); if (step == default(int)) { step = (start == min) ? 1 : -1; } int current = start; while (current >= min && current <= max) { yield return current; current += step; } } ... }
So far so good. Now let’s make it easy to perform an operation over a Range
. We’ll add a Do()
method on Range
.
[Test] public void Should_be_able_to_do_sum_with_range() { int sum = 0; Range.From(1).To(5).Do(i => sum += i); Assert.That(sum, Is.EqualTo(1 + 2 + 3 + 4 + 5)); }
And to pass that:
//Range.cs public void Do(Action<int> f) { foreach (int i in this) { f(i); } }
As an aside, it’s also nice to know that our current implementation plays nicely with LINQ. The following test passes without modification:
[Test] public void Should_be_able_to_use_linq_with_range() { String oneToThree = ""; foreach (int num in Range.From(1).To(5).Where(i => i <= 3) ) { oneToThree = oneToThree + num + " "; } Assert.That(oneToThree, Is.EqualTo("1 2 3 ")); }
Original goal within range…
Bad pun, sorry. We can now set our sights on rewriting the original test from the start of this post using the Range
class.
[Test] public void Test_original_mocking_scenario() { MockRepository mockery = new MockRepository(); IPagerRenderer renderer = mockery.CreateMock<IPagerRenderer>(); using (mockery.Record()) { //This is what we expect Range.Do to call, //so we obviously won't use the Range class here. renderer.WriteCurrentPage(1); renderer.WritePageLink(2); renderer.WritePageLink(3); renderer.WritePageLink(4); renderer.WritePageLink(5); } using (mockery.Playback()) { renderer.WriteCurrentPage(1); Range.From(2).To(5).Do(i => renderer.WritePageLink(i)); } } public interface IPagerRenderer { void WriteCurrentPage(int i); void WritePageLink(int i); }
The emphasised section of the code has the same effect as the code snippets at the start of this post. You can compare the mockery.Record()
to the mockery.Playback()
sections to see the difference. To me this reads nicely: for each number in the Range
From
2 To
5 Do
the renderer.WritePageLink
operation. I’m not about to going using this everywhere or anything, but I’m considering using it when it helps to communicate the intention of a test. All in all, ‘twas a bit of harmless fun.
Grabbing the code
You can get a CS file containing the tests and Range code used for this post from here [links direct to CS file download]. It requires .NET 3.5, NUnit 2.4+ and Rhino Mocks (I used v3.3) to get it to work without modification. Should go without saying, but don’t assume it works or is suitable for real life – it’s for solely for the purpose of mucking around :-)