Feeds:
Posts
Comments

Archive for the ‘BDD Kata’ Category

BDD Kata – string calculator

My friend @scichelli tweeted about having done Roy Osherove’s String Calculator kata… again. I’d never heard of it so I hunted it down to try it out. Here goes!

Create a simple String calculator with a method int Add(string numbers) that takes 0, 1 or 2 numbers (for example “” or “1” or “1,2”) and returns their sum. For an empty string it will return 0.

[TestFixture]
public class When_asked_to_Add
{
	[Test]
	public void Given_an_empty_input()
	{
		Test.Given(_calculator)
			.When(asked_to_add)
			.With(an_empty_input)
			.Should(get_zero)
			.Verify();
	}
}

test details

	[SetUp]
	public void BeforeEachTest()
	{
		_calculator = new StringCalculator();
	}

	private void an_empty_input()
	{
		_numbers = "";
	}

	private void asked_to_add()
	{
		_result = _calculator.Add(_numbers);
	}

	private void get_zero()
	{
		_result.ShouldBeEqualTo(0);
	}

which implies

public class StringCalculator
{
	public int Add(string numbers)
	{
		return -1;
	}
}

red

STEP 1: an empty input
WHEN asked to add
SHOULD get zero - FAILED

fix it

	public int Add(string numbers)
	{
		return 0;
	}

green. Next:

	[Test]
	public void Given_an_input_containing_a_single_number()
	{
		Test.Given(_calculator)
			.When(asked_to_add)
			.With(an_input_containing_a_single_number)
			.Should(get_the_numeric_value_of_the_input)
			.Verify();
	}

	private void an_input_containing_a_single_number()
	{
		_numbers = "341";
		_expected = 341;
	}

	private void get_the_numeric_value_of_the_input()
	{
		_result.ShouldBeEqualTo(_expected);
	}

red

STEP 1: an input containing a single number
WHEN asked to add
SHOULD get the numeric value of the input - FAILED

fix it

	public int Add(string numbers)
	{
		if (numbers.Length == 0)
		{
			return 0;
		}
		return Convert.ToInt32(numbers);
	}

green. Now support two numbers

	[Test]
	public void Given_an_input_containing_two_numbers()
	{
		Test.Given(_calculator)
			.When(asked_to_add)
			.With(an_input_containing_two_numbers)
			.Should(get_the_sum_of_the_numeric_values_in_the_input)
			.Verify();
	}

	private void an_input_containing_two_numbers()
	{
		_numbers = "341,659";
		_expected = 1000;
	}

	private void get_the_sum_of_the_numeric_values_in_the_input()
	{
		_result.ShouldBeEqualTo(_expected);
	}

red

STEP 1: an input containing two numbers
WHEN asked to add
SHOULD get the sum of the numeric values in the input - FAILED

fix it

	public int Add(string numbers)
	{
		if (numbers.Length == 0)
		{
			return 0;
		}
		var values = numbers.Split(',').Select(x => Convert.ToInt32(x));
		return values.Sum();
	}

green. Next requirement:

Allow the Add method to handle an unknown amount of numbers

	[Test]
	public void Given_an_input_containing_more_than_two_numbers()
	{
		Test.Given(_calculator)
			.When(asked_to_add)
			.With(an_input_containing_more_than_two_numbers)
			.Should(get_the_sum_of_the_numeric_values_in_the_input)
			.Verify();
	}

	[SetUp]
	public void BeforeEachTest()
	{
		_expected = 0;
		_numbers = "";
		_calculator = new StringCalculator();
	}

	private void an_input_containing_more_than_two_numbers()
	{
		var random = new Random();
		for (int i = 0; i < 100; i++)
		{
			int randomValue = random.Next(5000);
			_expected += randomValue;
			_numbers += (_numbers.Length > 0 ? "," : "") + randomValue;
		}
	}

	private void get_the_sum_of_the_numeric_values_in_the_input()
	{
		_result.ShouldBeEqualTo(_expected, "input = "+_numbers);
	}

green. Next requirement:

Allow the Add method to handle either a newline or a comma between any two numbers. For example “1\n2,3” is OK but “1\n,3” is not.

First some test refactoring:

	[Test]
	public void Given_an_input_containing_two_numbers_separated_by_a_comma()
	{
		Test.Given(_calculator)
			.When(asked_to_add)
			.With(comma_separator)
			.With(an_input_containing_two_numbers)
			.Should(get_the_sum_of_the_numeric_values_in_the_input)
			.Verify();
	}

	[Test]
	public void Given_an_input_containing_more_than_two_numbers_separated_by_a_comma()
	{
		Test.Given(_calculator)
			.When(asked_to_add)
			.With(comma_separator)
			.With(an_input_containing_more_than_two_numbers)
			.Should(get_the_sum_of_the_numeric_values_in_the_input)
			.Verify();
	}

	private void comma_separator()
	{
		_separator = ",";
	}

Use _separator instead of hard-coded comma in the test details.

	private void an_input_containing_two_numbers()
	{
		_numbers = "341" + _separator + "659";
		_expected = 1000;
	}

	private void an_input_containing_more_than_two_numbers()
	{
		var random = new Random();
		for (int i = 0; i < 100; i++)
		{
			int randomValue = random.Next(5000);
			_expected += randomValue;
			_numbers += (_numbers.Length > 0 ? _separator : "") + randomValue;
		}
	}

still green. Now require support for newline separator too.

	[Test]
	public void Given_an_input_containing_two_numbers_separated_by_a_newline()
	{
		Test.Given(_calculator)
			.When(asked_to_add)
			.With(newline_separator)
			.With(an_input_containing_two_numbers)
			.Should(get_the_sum_of_the_numeric_values_in_the_input)
			.Verify();
	}

	private void newline_separator()
	{
		_separator = "\n";
	}

red

STEP 1: newline separator
STEP 2: an input containing two numbers
WHEN asked to add - FAILED

fix it

	public int Add(string numbers)
	{
		if (numbers.Length == 0)
		{
			return 0;
		}
		var values = numbers
			.Split(',', '\n')
			.Select(x => Convert.ToInt32(x));
		return values.Sum();
	}

green. refactor.

	public int Add(string numbers)
	{
		if (numbers.Length == 0)
		{
			return 0;
		}
		var values = GetValues(numbers);
		return values.Sum();
	}

	private static IEnumerable<int> GetValues(string numbers)
	{
		return numbers
			.Split(',', '\n')
			.Select(x => Convert.ToInt32(x));
	}

Next add a test for multiple numbers with newline separator.

	[Test]
	public void Given_an_input_containing_more_than_two_numbers_separated_by_a_newline()
	{
		Test.Given(_calculator)
			.When(asked_to_add)
			.With(newline_separator)
			.With(an_input_containing_more_than_two_numbers)
			.Should(get_the_sum_of_the_numeric_values_in_the_input)
			.Verify();
	}

green. Next to handle the invalid case in the requirement, multiple separators together. Start with handling two commas.

	[Test]
	public void Given_an_input_containing_numbers_separated_by_more_than_one_comma()
	{
		Test.Given(_calculator)
			.When(asked_to_add)
			.With(comma_separator)
			.With(comma_separator)
			.With(an_input_containing_two_numbers)
			.ShouldThrowException<ArgumentException>()
			.Verify();
	}

	private void comma_separator()
	{
		_separator += ",";
	}

	[SetUp]
	public void BeforeEachTest()
	{
		_separator = "";
		_expected = 0;
		_numbers = "";
		_calculator = new StringCalculator();
	}

red.

STEP 1: comma separator
STEP 2: comma separator
STEP 3: an input containing two numbers
WHEN asked to add - FAILED

  Expected: <System.ArgumentException>
  But was:  <System.FormatException>

fix it

	private static IEnumerable<int> GetValues(string numbers)
	{
		var values = numbers.Split(',', '\n');

		if (values.Any(x => x.Length == 0))
		{
			throw new ArgumentException("Found multiple separators between values in '"+numbers+"'");
		}

		return values.Select(x => Convert.ToInt32(x));
	}

green. refactor.

	private static IEnumerable<int> GetValues(string numbers)
	{
		var values = numbers.Split(',', '\n');

		if (values.Any(x => x.Length == 0))
		{
			throw new MultipleSeparatorsException(numbers);
		}

		return values.Select(x => Convert.ToInt32(x));
	}

public class MultipleSeparatorsException : ArgumentException
{
	public const string FoundMultipleSeparatorsBetweenValuesFormat 
		= "Found multiple separators between values in '{0}'";

	public MultipleSeparatorsException(string numbers) :
		base(String.Format(FoundMultipleSeparatorsBetweenValuesFormat, numbers))
	{
	}
}

	public void Given_an_input_containing_numbers_separated_by_more_than_one_comma()
	{
		Test.Given(_calculator)
			.When(asked_to_add)
			.With(comma_separator)
			.With(comma_separator)
			.With(an_input_containing_two_numbers)
			.ShouldThrowException<MultipleSeparatorsException>()
			.Verify();
	}

still green. Try multiple newlines:

	[Test]
	public void Given_an_input_containing_numbers_separated_by_more_than_one_newline()
	{
		Test.Given(_calculator)
			.When(asked_to_add)
			.With(newline_separator)
			.With(newline_separator)
			.With(an_input_containing_two_numbers)
			.ShouldThrowException<MultipleSeparatorsException>()
			.Verify();
	}

	private void newline_separator()
	{
		_separator += "\n";
	}

green. Try alternating newlines and commas:

	[Test]
	public void Given_an_input_containing_numbers_separated_by_comma_and_newline()
	{
		Test.Given(_calculator)
			.When(asked_to_add)
			.With(comma_separator)
			.With(newline_separator)
			.With(an_input_containing_two_numbers)
			.ShouldThrowException<MultipleSeparatorsException>()
			.Verify();
	}

green. Next requirement:

Allow the Add method to optionally handle a different delimiter. To add a delimiter, the beginning of the string will contain a separate line that looks like this: “//[delimiter]\n[numbers…]” for example “//;\n1;2” should return 3.

	[Test]
	public void Given_an_input_containing_two_numbers_separated_by_a_space_with_space_delimiter()
	{
		Test.Given(_calculator)
			.When(asked_to_add)
			.With(space_delimiter)
			.With(space_separator)
			.With(an_input_containing_two_numbers)
			.Should(get_the_sum_of_the_numeric_values_in_the_input)
			.Verify();
	}

	private void space_delimiter()
	{
		UseDelimiter(' ');
	}

	private void UseDelimiter(char delimiter)
	{
		_delimiter = "//" + delimiter + "\n";
	}

	private void space_separator()
	{
		_separator += " ";
	}

	[SetUp]
	public void BeforeEachTest()
	{
		_delimiter = "";
		_separator = "";
		_expected = 0;
		_numbers = "";
		_calculator = new StringCalculator();
	}

red

STEP 1: space delimiter
STEP 2: space separator
STEP 3: an input containing two numbers
WHEN asked to add - FAILED

System.FormatException : Input string was not in a correct format.

fix it

	private static IEnumerable<int> GetValues(string numbers)
	{
		var delimiters = new List<char> { ',', '\n' };
		if (numbers.StartsWith("//"))
		{
			delimiters.Add(numbers[2]);
			numbers = numbers.Substring(4);
		}

		var values = numbers.Split(delimiters.ToArray());

		if (values.Any(x => x.Length == 0))
		{
			throw new MultipleSeparatorsException(numbers);
		}

		return values.Select(x => Convert.ToInt32(x));
	}

green. refactor.

	private static IEnumerable<int> GetValues(string numbers)
	{
		var values = SeparateValues(numbers);

		if (values.Any(x => x.Length == 0))
		{
			throw new MultipleSeparatorsException(numbers);
		}

		return values.Select(x => Convert.ToInt32(x));
	}

	private static IEnumerable<string> SeparateValues(string numbers)
	{
		var delimiters = new List<char> { ',', '\n' };
		if (HasDelimiterSection(numbers))
		{
			delimiters.Add(GetDelimiter(numbers));
			numbers = RemoveDelimiterSection(numbers);
		}

		return numbers.Split(delimiters.ToArray());
	}

	private static string RemoveDelimiterSection(string numbers)
	{
		return numbers.Substring(4);
	}

	private static bool HasDelimiterSection(string numbers)
	{
		return numbers.StartsWith("//");
	}

	private static char GetDelimiter(string numbers)
	{
		return numbers[2];
	}

still green. Next requirement.

Calling Add with a negative number will throw an exception “negatives not allowed” followed by the negative that was passed. If there are multiple negatives, show all of them in the exception message.

refactor the tests — mostly naming.

	[SetUp]
	public void BeforeEachTest()
	{
		_delimiter = "";
		_separator = "";
		_expected = 0;
		_numbers = "";
		_calculator = new StringCalculator();
	}

	[Test]
	public void Given_an_empty_input()
	{
		Test.Given(_calculator)
			.When(asked_to_add)
			.With(an_empty_input)
			.Should(get_zero)
			.Verify();
	}

	[Test]
	public void Given_an_input_containing_a_single_non_negative_number()
	{
		Test.Given(_calculator)
			.When(asked_to_add)
			.With(a_random_non_negative_number)
			.Should(get_the_numeric_value_of_the_input)
			.Verify();
	}

	[Test]
	public void Given_an_input_containing_more_than_two_non_negative_numbers_separated_by_a_comma()
	{
		Test.Given(_calculator)
			.When(asked_to_add)
			.With(comma_separator)
			.With(an_input_containing_more_than_two_non_negative_numbers)
			.Should(get_the_sum_of_the_numeric_values_in_the_input)
			.Verify();
	}

	[Test]
	public void Given_an_input_containing_more_than_two_non_negative_numbers_separated_by_a_newline()
	{
		Test.Given(_calculator)
			.When(asked_to_add)
			.With(newline_separator)
			.With(an_input_containing_more_than_two_non_negative_numbers)
			.Should(get_the_sum_of_the_numeric_values_in_the_input)
			.Verify();
	}

	[Test]
	public void Given_an_input_containing_non_negative_numbers_separated_by_comma_and_newline()
	{
		Test.Given(_calculator)
			.When(asked_to_add)
			.With(a_random_non_negative_number)
			.With(separator_containing_comma_and_newline)
			.With(a_random_non_negative_number)
			.ShouldThrowException<MultipleSeparatorsException>()
			.Verify();
	}

	[Test]
	public void Given_an_input_containing_non_negative_numbers_separated_by_more_than_one_comma()
	{
		Test.Given(_calculator)
			.When(asked_to_add)
			.With(a_random_non_negative_number)
			.With(separator_containing_more_than_one_comma)
			.With(a_random_non_negative_number)
			.ShouldThrowException<MultipleSeparatorsException>()
			.Verify();
	}

	[Test]
	public void Given_an_input_containing_non_negative_numbers_separated_by_more_than_one_newline()
	{
		Test.Given(_calculator)
			.When(asked_to_add)
			.With(a_random_non_negative_number)
			.With(separator_containing_more_than_one_newline)
			.With(a_random_non_negative_number)
			.ShouldThrowException<MultipleSeparatorsException>()
			.Verify();
	}

	[Test]
	public void Given_an_input_containing_two_non_negative_numbers_separated_by_a_comma()
	{
		Test.Given(_calculator)
			.When(asked_to_add)
			.With(a_random_non_negative_number)
			.With(comma_separator)
			.With(a_random_non_negative_number)
			.Should(get_the_sum_of_the_numeric_values_in_the_input)
			.Verify();
	}

	[Test]
	public void Given_an_input_containing_two_non_negative_numbers_separated_by_a_newline()
	{
		Test.Given(_calculator)
			.When(asked_to_add)
			.With(a_random_non_negative_number)
			.With(newline_separator)
			.With(a_random_non_negative_number)
			.Should(get_the_sum_of_the_numeric_values_in_the_input)
			.Verify();
	}

	[Test]
	public void Given_an_input_containing_two_non_negative_numbers_separated_by_a_space_with_a_space_delimiter()
	{
		Test.Given(_calculator)
			.When(asked_to_add)
			.With(space_delimiter)
			.With(a_random_non_negative_number)
			.With(space_separator)
			.With(a_random_non_negative_number)
			.Should(get_the_sum_of_the_numeric_values_in_the_input)
			.Verify();
	}

	private void a_random_non_negative_number()
	{
		int randomValue = _random.Next(5000);
		_numbers += (_numbers.Length > 0 ? _separator : "") + randomValue;
		_expected += randomValue;
	}

	private void an_input_containing_more_than_two_non_negative_numbers()
	{
		for (int i = 0; i < 100; i++)
		{
			a_random_non_negative_number();
		}
	}

	private void separator_containing_comma_and_newline()
	{
		comma_separator();
		newline_separator();
	}

	private void separator_containing_more_than_one_comma()
	{
		comma_separator();
		comma_separator();
	}

	private void separator_containing_more_than_one_newline()
	{
		newline_separator();
		newline_separator();
	}

still green. Now handle the negative number requirement.

	[Test]
	public void Given_an_input_containing_a_single_negative_number()
	{
		Test.Given(_calculator)
			.When(asked_to_add)
			.With(a_random_negative_number)
			.ShouldThrowException<ArgumentException>("negatives not allowed " + _negativeValues)
			.Verify();
	}

	private void a_random_negative_number()
	{
		int randomValue = _random.Next(-5000, 0);
		_numbers += (_numbers.Length > 0 ? _separator : "") + randomValue;
		_expected += randomValue;
		_negativeValues += (_negativeValues.Length > 0 ? ", " : "") + randomValue;
	}

	[SetUp]
	public void BeforeEachTest()
	{
		_negativeValues = "";
		_delimiter = "";
		_separator = "";
		_expected = 0;
		_numbers = "";
		_calculator = new StringCalculator();
	}

red

STEP 1: a random negative number
WHEN asked to add - FAILED

  negatives not allowed 
  Expected: <System.ArgumentException>
  But was:  null

fix it

	private static IEnumerable<int> GetValues(string numbers)
	{
		var values = SeparateValues(numbers);

		if (values.Any(x => x.Length == 0))
		{
			throw new MultipleSeparatorsException(numbers);
		}

		var integerValues = GetIntegerValues(values);
		var negatives = integerValues.Where(x => x < 0);
		if (negatives.Any())
		{
			throw new ArgumentException ("negatives not allowed "+
				String.Join(", ", negativeValues.Select(x => x.ToString()).ToArray());
		}
		return integerValues;
	}

	private static IEnumerable<int> GetIntegerValues(IEnumerable<string> values)
	{
		return values.Select(x => Convert.ToInt32(x));
	}

green. refactor.

	private static IEnumerable<int> GetValues(string numbers)
	{
		var values = SeparateValues(numbers);

		if (values.Any(x => x.Length == 0))
		{
			throw new MultipleSeparatorsException(numbers);
		}

		var integerValues = GetIntegerValues(values);
		var negatives = integerValues.Where(x => x < 0);
		if (negatives.Any())
		{
			throw new NegativesNotAllowedException (negatives);
		}
		return integerValues;
	}

	private static IEnumerable<int> GetIntegerValues(IEnumerable<string> values)
	{
		return values.Select(x => Convert.ToInt32(x));
	}

public class NegativesNotAllowedException : ArgumentException
{
	public const string NegativesNotAllowedFormat = "negatives not allowed {0}";

	public NegativesNotAllowedException(IEnumerable<int> negativeValues) :
		base(String.Format(NegativesNotAllowedFormat, CombineValuesInString(negativeValues)))
	{
	}

	private static string CombineValuesInString(IEnumerable<int> negativeValues)
	{
		return String.Join(", ", negativeValues.Select(x => x.ToString()).ToArray());
	}
}

	[Test]
	public void Given_an_input_containing_a_single_negative_number()
	{
		Test.Given(_calculator)
			.When(asked_to_add)
			.With(a_random_negative_number)
			.ShouldThrowException<NegativesNotAllowedException>(String.Format(NegativesNotAllowedException .NegativesNotAllowedFormat, _negativeValues))
			.Verify();
	}

still green. Make sure multiple negatives end up in the exception message.

	[Test]
	public void Given_an_input_containing_two_negative_numbers_separated_by_a_comma()
	{
		Test.Given(_calculator)
			.When(asked_to_add)
			.With(a_random_negative_number)
			.With(comma_separator)
			.With(a_random_negative_number)
			.ShouldThrowException<NegativesNotAllowedException>(String.Format(NegativesNotAllowedException.NegativesNotAllowedFormat, _negativeValues))
			.Verify();
	}

green.

Next requirement… oh man. I didn’t know this was a timed kata! Oh well, I don’t know how much time I’ve put into this because of various interruptions and live blogging it so I’ll stop here.

Read Full Post »

In this kata we’ll drive out the implementation of an extension method with the following signature:

public static bool IsNullOrEmpty(this string input, bool trim)

The desired functionality is:

Given an input containing non-whitespace the method should return false.
Given a null input the method should return true.
Given an empty input the method should return true.
Given an input containing only whitespace the method should return true if trim is true, otherwise false.

I’ll be using NUnit with FluentAssert for the test framework .

Let’s start with the simplest test, a null input.

[TestFixture]
public class When_asked_if_a_string_is_null_or_empty_with_optional_trim
{
	[Test]
	public void Given_a_null_string_and_trim_is_false()
	{
		Test.Static()
			.When(asked_if_a_string_is_null_or_empty)
			.With(a_null_string)
			.With(trim_set_to_false)
			.Should(return_true)
			.Verify();
	}
}

with test details

	private void asked_if_a_string_is_null_or_empty()
	{
		_result = _input.IsNullOrEmpty(_trim);
	}

	private void a_null_string()
	{
		_input = null;
	}

	private void trim_set_to_false()
	{
		_trim = false;
	}

	private void return_true()
	{
		_result.ShouldBeTrue();
	}

Now stub a failing implementation.

public static class StringExtensions
{
	public static bool IsNullOrEmpty(this string input, bool trim)
	{
		return false;
	}
}

Run the test and get red.

STEP 1: a null string
STEP 2: trim set to false
WHEN asked if a string is null or empty
SHOULD return true - FAILED

fix it

	public static bool IsNullOrEmpty(this string input, bool trim)
	{
		return input == null;
	}

Run the test and get green. Next for completeness, we’ll add a test for the case where trim is true.

	[Test]
	public void Given_a_null_string_and_trim_is_true()
	{
		Test.Static()
			.When(asked_if_a_string_is_null_or_empty)
			.With(a_null_string)
			.With(trim_set_to_true)
			.Should(return_true)
			.Verify();
	}

with test details

	private void trim_set_to_true()
	{
		_trim = true;
	}

Run the test and get green. Now let’s try an empty string.

	[Test]
	public void Given_an_empty_string_and_trim_is_false()
	{
		Test.Static()
			.When(asked_if_a_string_is_null_or_empty)
			.With(an_empty_string)
			.With(trim_set_to_false)
			.Should(return_true)
			.Verify();
	}

with details

	private void an_empty_string()
	{
		_input = "";
	}

red

STEP 1: an empty string
STEP 2: trim set to false
WHEN asked if a string is null or empty
SHOULD return true - FAILED

fix it

	public static bool IsNullOrEmpty(this string input, bool trim)
	{
		return String.IsNullOrEmpty(input);
	}

Again for completeness let’s add a variant of the test where trim is true.

	[Test]
	public void Given_an_empty_string_and_trim_is_true()
	{
		Test.Static()
			.When(asked_if_a_string_is_null_or_empty)
			.With(an_empty_string)
			.With(trim_set_to_true)
			.Should(return_true)
			.Verify();
	}

green. Next we’ll drive out handling inputs that contain whitespace. To get a failure let’s also start with the variant where trim is true.

	[Test]
	public void Given_a_string_containing_only_whitespace_and_trim_is_true()
	{
		Test.Static()
			.When(asked_if_a_string_is_null_or_empty)
			.With(a_string_containing_only_whitespace)
			.With(trim_set_to_true)
			.Should(return_true)
			.Verify();
	}

with details

	private void a_string_containing_only_whitespace()
	{
		_input = "\n\r\t ";
	}

red

STEP 1: a string containing only whitespace
STEP 2: trim set to true
WHEN asked if a string is null or empty
SHOULD return true - FAILED

fix it

	public static bool IsNullOrEmpty(this string input, bool trim)
	{
		if (String.IsNullOrEmpty(input))
		{
			return true;
		}
		return trim && String.IsNullOrEmpty(input.Trim());
	}

green. Now to verify the implementation where trim is false.

	[Test]
	public void Given_a_string_containing_only_whitespace_and_trim_is_false()
	{
		Test.Static()
			.When(asked_if_a_string_is_null_or_empty)
			.With(a_string_containing_only_whitespace)
			.With(trim_set_to_false)
			.Should(return_false)
			.Verify();
	}

with details

	private void return_false()
	{
		_result.ShouldBeFalse();
	}

green. Finally, let’s write the test for the case where the input contains non-whitespace.

	[Test]
	public void Given_a_string_containing_non_whitespace_and_trim_is_false()
	{
		Test.Static()
			.When(asked_if_a_string_is_null_or_empty)
			.With(a_string_containing_non_whitespace)
			.With(trim_set_to_false)
			.Should(return_false)
			.Verify();
	}

with test details

	private void a_string_containing_non_whitespace()
	{
		_input = "aa";
	}

green. And the case where trim is set to true.

	[Test]
	public void Given_a_string_containing_non_whitespace_and_trim_is_true()
	{
		Test.Static()
			.When(asked_if_a_string_is_null_or_empty)
			.With(a_string_containing_non_whitespace)
			.With(trim_set_to_true)
			.Should(return_false)
			.Verify();
	}

Run the test and get green.

That’s it. All desired functionality has been implemented.

Read Full Post »

While Neal Ford was speaking about Emergent Design at the Austin Java Users Group tonight he showed a snippet of TDD code he wrote to find Perfect numbers (Wikipedia). I wondered how I might solve it using LINQ (and FluentAssert).

[TestFixture]
public class When_looking_for_Perfect_numbers
{
	[Test]
	public void Given_a_non_perfect_number()
	{
		Test.Given(_finder)
			.When(asked_if_a_number_is_perfect)
			.With(a_non_perfect_number)
			.Should(return_false)
			.Verify();
	}

	private void asked_if_a_number_is_perfect()
	{
		_result = _finder.IsPerfectNumber(_candidate);
	}

	private void a_non_perfect_number()
	{
		_candidate = 3;
	}

	private void return_false()
	{
		_result.ShouldBeFalse();
	}
}

make it compile

public class PerfectNumberFinder
{
	public bool IsPerfectNumber(int candidate)
	{
		return true;
	}
}

red

  WITH a non perfect number
  WHEN asked if a number is perfect
  SHOULD return false - FAILED

green (passive aggressive)

	public bool IsPerfectNumber(int candidate)
	{
		return false;
	}

Next test:

	[Test]
	public void Given_a_perfect_number()
	{
		Test.Given(_finder)
			.When(asked_if_a_number_is_perfect)
			.With(a_perfect_number)
			.Should(return_true)
			.Verify();
	}

	private void a_perfect_number()
	{
		_candidate = 6;
	}

	private void return_true()
	{
		_result.ShouldBeTrue();
	}

red

  WITH a perfect number
  WHEN asked if a number is perfect
  SHOULD return true - FAILED

Based on the Wikipedia article, getting to green is going to require more tools. Specifically we need a way to get the positive factors of an integer.

[TestFixture]
public class When_getting_the_factors_of_an_integer
{
	[Test]
	public void Given_6()
	{
		Test.Static()
			.When(asked_to_get_positive_factors)
			.With(the_number_6)
			.Should(get_the_correct_factors)
			.Verify();
	}

	private void asked_to_get_positive_factors()
	{
		_result = _input.GetPositiveFactors();
	}

	private void the_number_6()
	{
		_input = 6;
		_expected = new[] { 1, 2, 3, 6 };
	}

	private void get_the_correct_factors()
	{
		_result.OrderBy(x=>x).ShouldContainAllInOrder(_expected);
	}
}

make it compile

public static class IntExtensions
{
	public static IEnumerable<int> GetPositiveFactors(this int input)
	{
		return new int[]{};
	}
}

red

WITH the number 6
WHEN asked to get positive factors
SHOULD get the correct factors - FAILED

green

	public static IEnumerable<int> GetPositiveFactors(this int input)
	{
		return Enumerable.Range(1, (int)Math.Ceiling(Math.Sqrt(input)))
			.Where(potentialFactor => input % potentialFactor == 0)
			.SelectMany(factor => GetFactorPair(input, factor))
			.Distinct();
	}

	private static IEnumerable<int> GetFactorPair(int candidate, int factor)
	{
		yield return candidate / factor;
		yield return factor;
	}

We should also add a guard against negative inputs:

	[Test]
	public void Given_a_negative_value()
	{
		Test.Static()
			.When(asked_to_get_positive_factors)
			.With(a_negative_value)
			.Should(get_an_empty_result)
			.Verify();
	}

	private void a_negative_value()
	{
		_input = -25;
	}

	private void get_an_empty_result()
	{
		_result.Count().ShouldBeEqualTo(0);
	}

red

System.ArgumentOutOfRangeException : Specified argument was out of the range of valid values.

Indeed…. fix it:

	public static IEnumerable<int> GetPositiveFactors(this int input)
	{
		if (input < 1)
		{
			return new int[] { };
		}
		return Enumerable.Range(1, (int)Math.Ceiling(Math.Sqrt(input)))
			.Where(potentialFactor => input % potentialFactor == 0)
			.SelectMany(factor => GetFactorPair(input, factor))
			.Distinct();
	}

green

Now use the extension method to make our PerfectNumberFinder test pass:

	public bool IsPerfectNumber(int candidate)
	{
		return candidate * 2 == candidate.GetPositiveFactors().Sum();
	}

green

One other thing we might like is a list of Perfect numbers.

[TestFixture]
public class When_generating_Perfect_numbers
{
	[SetUp]
	public void BeforeEachTest()
	{
		_finder = new PerfectNumberFinder();
	}

	[Test]
	public void Given_a_maximum_value_of_1000()
	{
		Test.Given(_finder)
			.When(asked_for_perfect_numbers)
			.With(a_maximum_value_of_1000)
			.Should(not_exceed_the_maximum_value)
			.Should(get_the_set_of_perfect_numbers)
			.Verify();
	}

	private void a_maximum_value_of_1000()
	{
		_maxValue = 1000;
		_expected = new[] { 6, 28, 496 };
	}

	private void asked_for_perfect_numbers()
	{
		_result = _finder.GetPerfectNumbers(_maxValue);
	}

	private void get_the_set_of_perfect_numbers()
	{
		_result.OrderBy(x => x).ShouldContainAllInOrder(_expected);
	}

	private void not_exceed_the_maximum_value()
	{
		_result.Max().ShouldBeLessThan(_maxValue);
	}
}

make it compile

public class PerfectNumberFinder
{
	public IEnumerable<int> GetPerfectNumbers(int stopAt)
	{
		return new int[] { };
	}

red

WITH a maximum value of 1000
WHEN asked for perfect numbers
SHOULD not exceed the maximum value - FAILED

System.InvalidOperationException : Sequence contains no elements

Hmm. Should I put another assertion on the test or change the implementation? I think another assertion is in order as it is possible there are no Perfect numbers within the range (i.e. given a value less than 6).

	[Test]
	public void Given_a_maximum_value_of_1000()
	{
		Test.Given(_finder)
			.When(asked_for_perfect_numbers)
			.With(a_maximum_value_of_1000)
			.Should(not_get_an_empty_result)
			.Should(not_exceed_the_maximum_value)
			.Should(get_the_set_of_perfect_numbers)
			.Verify();
	}

	private void not_get_an_empty_result()
	{
		_result.Count().ShouldBeGreaterThan(0);
	}

red

WITH a maximum value of 1000
WHEN asked for perfect numbers
SHOULD not get an empty result - FAILED

green

	public IEnumerable<int> GetPerfectNumbers(int stopAt)
	{
		return Enumerable.Range(1, stopAt)
			.Where(IsPerfectNumber);
	}

This works for the first four Perfect numbers. Beyond that we need a faster way to skip the values that cannot be Perfect numbers. The Wikipedia article offers some good insight along those lines if you are interested.

Read Full Post »

Consider the following problem: We have two integer ranges (1,5) and (10,15) and we want to find the range of the gap between them, if any (6,9).

Lets start with a new project and add references to a test framework (NUnit) and a BDD framework (FluentAssert)

We’ll start with a simple container to hold an integer range:

public class Range
{
	public int Start;
	public int End;
}

Next we’ll stub the range gap finder

public class RangeGapFinder
{
}

and our test fixture

[TestFixture]
public class When_asked_for_the_gap_between_two_ranges
{
	
}

Now we can start driving out the implementation. First off, let’s check the happy path – a gap between the ranges:

	[Test]
	public void Given_a_value_gap()
	{
		Test.Given(new RangeGapFinder())
			.When(Asked_to_find_the_gap)
			.With(A_value_gap)
			.Should(Get_a_non_null_result)
			.Should(Get_the_correct_range_start_value)
			.Should(Get_the_correct_range_end_value)
			.Verify();
	}

now to add the test parts

	public void A_value_gap()
	{
		_range1 = new Range(1, 5);
		_range2 = new Range(10, 15);
		_expectedStart = 6;
		_expectedEnd = 9;
	}

	private void Asked_to_find_the_gap(RangeGapFinder rangeGapFinder)
	{
		_gap = rangeGapFinder.FindGap(_range1, _range2);
	}

	private void Get_a_non_null_result()
	{
		_gap.ShouldNotBeNull();
	}

	private void Get_the_correct_range_start_value()
	{
		_gap.Start.ShouldBeEqualTo(_expectedStart);
	}

	private void Get_the_correct_range_end_value()
	{
		_gap.End.ShouldBeEqualTo(_expectedEnd);
	}

which implies this method stub

public class RangeGapFinder
{
	public Range FindGap(Range range1, Range range2)
	{
		return null;
	}
}

and requires a constructor for the Range container

public class Range
{
	public int Start;
	public int End;

	public Range(int start, int end)
	{
		Start = start;
		End = end;
	}
}

Run the test. It fails. Now change the implementation so that the test passes.

	public Range FindGap(Range range1, Range range2)
	{
		return new Range(range1.End + 1, range2.Start - 1);
	}

That takes care of the happy path. Now what should happen if the ranges overlap (there is no gap) e.g. (1,5) and (2,9)? Let’s say the implementation should return null if there is no gap. First the failing test:

	[Test]
	public void Given_second_range_starts_in_the_first_range()
	{
		Test.Given(new RangeGapFinder())
			.When(Asked_to_find_the_gap)
			.With(Second_range_starting_in_the_first_range)
			.Should(Get_a_null_result)
			.Verify();
	}

	private void Second_range_starting_in_the_first_range()
	{
		_range1 = new Range(1, 5);
		_range2 = new Range(2, 9);
	}

	private void Get_a_null_result()
	{
		_gap.ShouldBeNull();
	}

Run the test and it fails. Now to update the implementation.

	public Range FindGap(Range range1, Range range2)
	{
		if (range1.End > range2.Start)
		{
			return null; // no gap
		}

		return new Range(range1.End + 1, range2.Start - 1);
	}

That covers the overlapping no-gap case. So now let’s think about inputs that could break the code. The first case is if the end value of range1 is the same as the start value of range2 e.g. (1,5) and (5,9). There is no gap so it should return null right?

	[Test]
	public void Given_first_range_End_value_equal_to_second_range_Start_value()
	{
		Test.Given(new RangeGapFinder())
			.When(Asked_to_find_the_gap)
			.With(First_range_End_value_same_as_second_range_Start_value)
			.Should(Get_a_null_result)
			.Verify();
	}

	private void First_range_End_value_same_as_second_range_Start_value()
	{
		_range1 = new Range(1, 5);
		_range2 = new Range(5, 9);
	}

Run the test and it fails! The fix is simple enough:

	public Range FindGap(Range range1, Range range2)
	{
		if (range1.End >= range2.Start)
		{
			return null; // no gap
		}

		return new Range(range1.End + 1, range2.Start-1);
	}

Okay. Another no-gap case is when the two ranges meet but do not overlap e.g. (1,5) (6,10). That should return null too. Does it?

	[Test]
	public void Given_adjacent_ranges()
	{
		Test.Given(new RangeGapFinder())
			.When(Asked_to_find_the_gap)
			.With(Adjacent_ranges)
			.Should(Get_a_null_result)
			.Verify();
	}

	private void Adjacent_ranges()
	{
		_range1 = new Range(1, 5);
		_range2 = new Range(6, 10);
	}

Run the test and it fails. The fix is another adjustment to the guard clause:

	public Range FindGap(Range range1, Range range2)
	{
		if (range1.End + 1 >= range2.Start)
		{
			return null; // no gap
		}

		return new Range(range1.End + 1, range2.Start - 1);
	}

What else could happen? The user could call the method with the ranges out of order e.g. (6,10) and (1,5). In this case let’s just do what the user expected instead of throwing an exception.

	[Test]
	public void Given_range2_Start_value_lower_than_range1_Start_value()
	{
		Test.Given(new RangeGapFinder())
			.When(Asked_to_find_the_gap)
			.With(Second_range_Start_lower_than_first_range_Start)
			.Should(Get_a_non_null_result)
			.Should(Get_the_correct_range_start_value)
			.Should(Get_the_correct_range_end_value)
			.Verify();
	}

	private void Second_range_Start_lower_than_first_range_Start()
	{
		_range1 = new Range(10, 15);
		_range2 = new Range(1, 5);
		_expectedStart = 6;
		_expectedEnd = 9;
	}

Test fails. Now to fix it we’ll just recurse:

	public Range FindGap(Range range1, Range range2)
	{
		if (range2.Start < range1.Start)
		{
			return FindGap(range2, range1);
		}
		if (range1.End + 1 >= range2.Start)
		{
			return null; // no gap
		}

		return new Range(range1.End + 1, range2.Start - 1);
	}

What else could happen? The user might send the start and end values swapped. A range is a range so let’s do the expected thing there too, starting with range1:

	[Test]
	public void Given_range1_End_value_lower_than_its_Start_value()
	{
		Test.Given(new RangeGapFinder())
			.When(Asked_to_find_the_gap)
			.With(Range1_end_value_lower_than_its_start_value)
			.Should(Get_a_non_null_result)
			.Should(Get_the_correct_range_start_value)
			.Should(Get_the_correct_range_end_value)
			.Verify();
	}

	private void Range1_end_value_lower_than_its_start_value()
	{
		_range1 = new Range(5, 1);
		_range2 = new Range(10, 15);
		_expectedStart = 6;
		_expectedEnd = 9;
	}

The test fails. We could simply update the Range

	if (range1.End < range1.Start)
	{
		int temp = range1.End;
		range1.End = range1.Start;
		range1.Start = range1.End;
	}

but that introduces a side effect. Let’s update the test to take the potential side effect into account:

	[Test]
	public void Given_range1_End_value_lower_than_its_Start_value()
	{
		Test.Given(new RangeGapFinder())
			.When(Asked_to_find_the_gap)
			.With(Range1_end_value_lower_than_its_start_value)
			.Should(Get_a_non_null_result)
			.Should(Get_the_correct_range_start_value)
			.Should(Get_the_correct_range_end_value)
			.Should(Not_change_the_values_of_range1)
			.Should(Not_change_the_values_of_range2)
			.Verify();
	}

	private void Not_change_the_values_of_range1()
	{
		_range1.Start.ShouldBeEqualTo(_originalRange1.Start);
		_range1.End.ShouldBeEqualTo(_originalRange1.End);
	}

	private void Not_change_the_values_of_range2()
	{
		_range2.Start.ShouldBeEqualTo(_originalRange2.Start);
		_range2.End.ShouldBeEqualTo(_originalRange2.End);
	}

	private void Asked_to_find_the_gap(RangeGapFinder rangeGapFinder)
	{
		_originalRange1 = new Range(_range1.Start, _range1.End);
		_originalRange2 = new Range(_range2.Start, _range2.End);
		_gap = rangeGapFinder.FindGap(_range1, _range2);
	}

Now the fix is create a new Range with the points swapped.

	public Range FindGap(Range range1, Range range2)
	{
		if (range2.Start < range1.Start)
		{
			return FindGap(range2, range1);
		}

		if (range1.End < range1.Start)
		{
			range1 = new Range(range1.End, range1.Start);
		}

		if (range1.End + 1 >= range2.Start)
		{
			return null; // no gap
		}

		return new Range(range1.End + 1, range2.Start - 1);
	}

Next we do the same for range2:

	[Test]
	public void Given_range2_End_value_lower_than_its_Start_value()
	{
		Test.Given(new RangeGapFinder())
			.When(Asked_to_find_the_gap)
			.With(Range2_end_value_lower_than_its_start_value)
			.Should(Get_a_non_null_result)
			.Should(Get_the_correct_range_start_value)
			.Should(Get_the_correct_range_end_value)
			.Should(Not_change_the_values_of_range1)
			.Should(Not_change_the_values_of_range2)
			.Verify();
	}

	private void Range2_end_value_lower_than_its_start_value()
	{
		_range1 = new Range(1, 5);
		_range2 = new Range(15, 10);
		_expectedStart = 6;
		_expectedEnd = 9;
	}

and implement the fix:

	public Range FindGap(Range range1, Range range2)
	{
		if (range2.Start < range1.Start)
		{
			return FindGap(range2, range1);
		}

		if (range1.End < range1.Start)
		{
			range1 = new Range(range1.End, range1.Start);
		}	
		
		if (range2.End < range2.Start)
		{
			range2 = new Range(range2.End, range2.Start);
		}

		if (range1.End + 1 >= range2.Start)
		{
			return null; // no gap
		}

		return new Range(range1.End + 1, range2.Start - 1);
	}

One last thing. We need some null checks on the inputs. Instead of retuning null we’ll make these exceptional cases. We could just let the first use trigger a NullReferenceException but a more meaningful exception might be nice.

	[Test]
	public void Given_a_null_for_range1()
	{
		Test.Given(new RangeGapFinder())
			.When(Asked_to_find_the_gap)
			.With(A_null_range1)
			.ShouldThrowException<ArgumentNullException>("range1 cannot be null")
			.Verify();
	}

	private void A_null_range1()
	{
		_range1 = null;
		_range2 = new Range(1, 5);
	}

	private void Asked_to_find_the_gap(RangeGapFinder rangeGapFinder)
	{
		if (_range1 != null)
		{
			_originalRange1 = new Range(_range1.Start, _range1.End);
		}
		_originalRange2 = new Range(_range2.Start, _range2.End);
		_gap = rangeGapFinder.FindGap(_range1, _range2);
	}
	public Range FindGap(Range range1, Range range2)
	{
		if (range1 == null)
		{
			throw new ArgumentNullException("range1","range1 cannot be null");
		}
	...

And the same for range2:

	[Test]
	public void Given_a_null_for_range2()
	{
		Test.Given(new RangeGapFinder())
			.When(Asked_to_find_the_gap)
			.With(A_null_range2)
			.ShouldThrowException<ArgumentNullException>("range2 cannot be null")
			.Verify();
	}

	private void A_null_range2()
	{
		_range1 = new Range(1, 5);
		_range2 = null;
	}

	private void Asked_to_find_the_gap(RangeGapFinder rangeGapFinder)
	{
		if (_range1 != null)
		{
			_originalRange1 = new Range(_range1.Start, _range1.End);
		}
		if (_range2 != null)
		{
			_originalRange2 = new Range(_range2.Start, _range2.End);
		}
		_gap = rangeGapFinder.FindGap(_range1, _range2);
	}

with final result:

	public Range FindGap(Range range1, Range range2)
	{
		if (range1 == null)
		{
			throw new ArgumentNullException("range1","range1 cannot be null");
		}

		if (range2 == null)
		{
			throw new ArgumentNullException("range2", "range2 cannot be null");
		}

		if (range2.Start < range1.Start)
		{
			return FindGap(range2, range1);
		}

		if (range1.End < range1.Start)
		{
			range1 = new Range(range1.End, range1.Start);
		}	
		
		if (range2.End < range2.Start)
		{
			range2 = new Range(range2.End, range2.Start);
		}

		if (range1.End + 1 >= range2.Start)
		{
			return null; // no gap
		}

		return new Range(range1.End + 1, range2.Start - 1);
	}

That’s it.

Read Full Post »

%d bloggers like this: