Solving the diamond kata with property-based testing series

  1. How to get started with Property-based Testing in C#
  2. Input generators in property-based tests with FsCheck
  3. First and Last line content
  4. Height equals Width
  5. Outside space symmetry
  6. Symmetry around the vertical axis
  7. Symmetry around the horizontal axis
  8. No padding for input letter row
  9. Letters order

All code samples are available on github


Letters order

In the diamond kata, letters that form the diamond have a particular order. The diamond always starts with A, then on each new row the letter is the next one in the alphabet until we reach the input letter, then we go back to A following a descendant alphabetical order. The diamond's rows follow a perfect alphabetical order, which will make an excellent property for our next test.

e.g.
Diamond-kata-letters-order

C# Tests

[Property(Arbitrary = new[] { typeof(LetterGenerator) })]
public Property RowsContainCorrectLetterInCorrectOrder(char c)
{
    var expected = new List<char>();
    for (var i = 'A'; i < c; i++) expected.Add(i);

    for (var i = c; i >= 'A'; i--) expected.Add(i);

    var actual = Diamond.Generate(c).ToList().Select(GetCharInRow);
    return actual.SequenceEqual(expected).ToProperty();
}

private static char GetCharInRow(string row)
{
    return row.First(x => x != ' ');
}

Here, we are using a testing pattern called the Oracle. The idea is to create a simple implementation of the problem, generate a result, and then compare it against the real implementation.

In this test;

  1. The oracle generates a sequence of characters from A to the input letter and then from the input letter to A in descending order.
  2. We then generate the diamond from the real implementation and flatten each row to a single char.
  3. Finally, we compare the two sequences to make sure they respect the correct letter ordering.

If you are wondering what's [Property(Arbitrary = new[] { typeof(LetterGenerator) })] it's probably because you missed my previous post

Test run

Our naive implementation will not pass this test, and we'll have to refactor our implementation again to support this case. It's not a bad thing; in fact, this is GREAT! It's how TDD is supposed to work—creating an implementation use case by use case incrementally by adding more tests and refactoring.

Refractor

private static IEnumerable<string> Generate(char c)
{
    for (var i = 'A'; i < c; i++) yield return GenerateRow(i, c);
    for (var i = c; i >= 'A'; i--) yield return GenerateRow(i, c);
}

private static string GenerateRow(char currentChar, char maxChar)
{
    var length = (maxChar - 'A' + 1) * 2 - 1;
    var padding = maxChar - currentChar;
    var sb = new StringBuilder();

    sb.Append(new string(' ', padding));
    sb.Append(currentChar);
    if (currentChar != 'A')
    {
        var insidePadding = length - padding * 2 - 2;
        sb.Append(new string(' ', insidePadding));
        sb.Append(currentChar);
    }

    sb.Append(new string(' ', padding));
    return sb.ToString();
}

Wrapping up

It turns out I've not been able to make a more straightforward implementation that passes all the tests than the final implementation of the Diamond kata algorithm. It's a good indicator that we are probably done with our test suite, as I cannot think of any other property that will make this fail. Don't get me wrong here, there are many more properties that we could think of, but they overlap with another existing properties and don't add any new differentiator.

e.g., All rows except the first and last one contain two identical letters.

I hope you enjoyed that complete series on property-based testing and learned a thing or two. Let me know what you think in the comments.


Solving the diamond kata with property-based testing series

  1. How to get started with Property-based Testing in C#
  2. Input generators in property-based tests with FsCheck
  3. First and Last line content
  4. Height equals Width
  5. Outside space symmetry
  6. Symmetry around the vertical axis
  7. Symmetry around the horizontal axis
  8. No padding for input letter row
  9. Letters order

All code samples are available on github