TDD Kata | String Calculator

5

October 9, 2009 by temebele

Kata is a Japanese word describing detailed choreographed patterns of movements practiced either solo or in pairs. most commonly known for the presence in the martial arts.

The idea is if you practice something regularly, at some point it will be come natural. (As they call it "Muscle memory" :)).

Even though Test Driven Development has been preached as best practice for years now, there is still resistance in many IT shops to adapt TDD. So the best way to teach your team to start making TDD part of their daily development practice is by running small hands on sessions, KATA.

Yesterday, the architect (a very passionate developer and lead) at my job took the team through one 1hr journey of solving a simple but yet complex enough problem to have the taste of TDD (Red-Green-Re factor) .

You can find the problem definition at Roy’s String Calculator TDD Kata

I am sharing my first attempt to develop the String Calculator add functionality using TDD. I recommend that you try it yourself and in case you want to see how other people approached it, you may download the zip file below.

using System;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;

namespace TDDFun
{
    public class StringCalculator
    {
        public double Add(string numbers)
        {
            Double sum = 0;
            if (string.IsNullOrEmpty(numbers))
            {
                sum = 0;
            }
            else
            {
                const string regex1 = “(\\//)”;
                const string regex2 = “(.*?)”;
                const string regex3 = “(\\n)”;
                const string regex4 = “(.*?\\d)”;

                var delimiters = new List<string>();
                var r = new Regex(regex1 + regex2 + regex3 + regex4, RegexOptions.IgnoreCase | RegexOptions.Singleline);
                Match m = r.Match(numbers);
                if (m.Success)
                {
                    string delimiterCollection = m.Groups[2].ToString();
                    int numberStartIndex = m.Groups[3].Index;
                    const string re5 = “(\\[.*?\\])”;
                    var r2 = new Regex(re5, RegexOptions.IgnoreCase | RegexOptions.Singleline);
                    MatchCollection m2 = r2.Matches(delimiterCollection);
                    if (m2.Count > 0)
                    {
                        foreach (Match x in m2)
                        {
                            delimiters.Add(x.ToString().Replace(“[“, “”).Replace(“]”, “”));
                        }
                    }
                    else
                    {
                        delimiters.Add(delimiterCollection);
                    }
                    numbers = numbers.Remove(0, numberStartIndex + 1);
                }
                else
                {
                    delimiters.Add(“\n”);
                    delimiters.Add(“,”);
                }

                string[] splittedNumbers = numbers.Split(delimiters.ToArray(), StringSplitOptions.None);
                ValiateNumbers(splittedNumbers);

                foreach (string s in splittedNumbers)
                {
                    double ss = Double.Parse(s);
                    sum += ss <= 1000 ? ss : 0;
                }
            }
            return sum;
        }

        private static void ValiateNumbers(IEnumerable<string> numbers)
        {
            double x;
            var negativeNumbers = new List<string>();
            foreach (string s in numbers)
            {
                Validator.IsRequiredField(Double.TryParse(s, out x), “Validation Error”);
                if (Double.Parse(s) < 0)
                {
                    negativeNumbers.Add(s);
                }
            }
            Validator.IsRequiredField(negativeNumbers.Count <= 0,
                                      “Negatives not allowed “ + ShowAllNegatives(negativeNumbers));
        }

        private static string ShowAllNegatives(List<string> negativeNumbers)
        {
            var sb = new StringBuilder();
            int counter = 0;
            negativeNumbers.ForEach(k =>
                                        {
                                            if (counter == 0)
                                            {
                                                sb.Append(k);
                                                counter++;
                                            }
                                            else
                                            {
                                                sb.Append(“;” + k);
                                            }
                                        });

            return sb.ToString();
        }
    }

    public static class Validator
    {
        public static void IsRequiredField(bool criteria, string message)
        {
            if (!criteria)
            {
                throw new ValidationException(message);
            }
        }
    }

    public class ValidationException : ApplicationException
    {
        public ValidationException(string message) : base(message)
        {
        }
    }
}

using NUnit.Framework;

namespace TDDFun
{
    [TestFixture]
    public class StringCalculatorTest
    {
        private StringCalculator calculator;

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

        [Test]
        public void Add_DecimalNumbers_ShouldReturnDouble()
        {
            Assert.IsTrue(calculator.Add(“//;\n1.1;1.2”) == 2.3);
        }

        [Test]
        public void Add_EmptyString_ShouldReturnZero()
        {
            Assert.IsTrue(calculator.Add(string.Empty) == 0);
        }

        [Test]
        public void Add_NewLineSeparators_ShouldPass()
        {
            Assert.IsTrue(calculator.Add(“1\n2\n34\n5”) == 42);
        }

        [Test]
        public void Add_NewLineSeparatorsCommasCombined_ShouldPass()
        {
            Assert.IsTrue(calculator.Add(“1\n2,34,5”) == 42);
        }

        [Test]
        public void Add_NonNumericStringPassed_ShouldThrowException()
        {
            Assert.Throws<ValidationException>(() => calculator.Add(“XT”));
        }

        [Test]
        public void Add_NullString_ShouldReturnZero()
        {
            Assert.IsTrue(calculator.Add(null) == 0);
        }

        [Test]
        public void Add_NumbersGreaterThan1000_ShouldBeIgnored()
        {
            Assert.IsTrue(calculator.Add(“//[***]\n1.1***1000.2”) == 1.1);
        }

        [Test]
        public void Add_OneString_ShouldReturnItself()
        {
            Assert.IsTrue(calculator.Add(“3”) == 3);
        }

        [Test]
        public void Add_SpacesBetweenDelimiters_ShouldBeHandled()
        {
            Assert.IsTrue(calculator.Add(“//[***][kkk]\n1.1   ***  1.2kkk   2”) == 4.3);
        }

        [Test]
        public void Add_SpecialDelimiterWithMultipleNegativeNumber_ShouldThrows()
        {
            var exception = Assert.Throws<ValidationException>(() => calculator.Add(“//[***]\n-1***-2”));
            Assert.AreEqual(exception.Message, “Negatives not allowed -1;-2”);
        }

        [Test]
        public void Add_ThreeString_ShouldPass()
        {
            Assert.IsTrue(calculator.Add(“3,33,4”) == 40);
        }

        [Test]
        public void Add_TwoNegativeString_ShouldThrowException()
        {
            var exception = Assert.Throws<ValidationException>(() => calculator.Add(“-1,-2”));
            Assert.AreEqual(exception.Message, “Negatives not allowed -1;-2”);
        }

        [Test]
        public void Add_TwoString_ShouldPass()
        {
            Assert.IsTrue(calculator.Add(“3,33”) == 36);
        }

        [Test]
        public void Add_WithDelimiterAndColon_ShouldPass()
        {
            Assert.IsTrue(calculator.Add(“//;\n1;2”) == 3);
        }

        [Test]
        public void Add_WithMultipleNegativeNumber_ThrowsExceptionShowingAllNegativeNumbers()
        {
            var exception = Assert.Throws<ValidationException>(() => calculator.Add(“//;\n-1;-2”));
            Assert.AreEqual(exception.Message, “Negatives not allowed -1;-2”);
        }

        [Test]
        public void Add_WithNegativeNumber_ThrowsException()
        {
            Assert.Throws<ValidationException>(() => calculator.Add(“//;\n1;-2”));
        }

        [Test]
        public void Add_WithOneSpecialDelimiters_ShouldPass()
        {
            Assert.IsTrue(calculator.Add(“//[***]\n1.1***1.2”) == 2.3);
        }

        [Test]
        public void Add_WithSpecialDelimitersOnlyOneChar_ShouldPass()
        {
            Assert.IsTrue(calculator.Add(“//[*]\n1.1*1.2”) == 2.3);
        }

        [Test]
        public void Add_WithSwithAndAnySpecialDelimiters_ShouldPass()
        {
            Assert.IsTrue(calculator.Add(“//***\n1.1***1.2”) == 2.3);
        }

        [Test]
        public void Add_WithThreeSpecialDelimiters_ShouldPass()
        {
            Assert.IsTrue(calculator.Add(“//[***][kkk][GGG]\n1.1***1.2kkk2GGG4.7”) == 9.0);
        }

        [Test]
        public void Add_WithTwoSpecialDelimiters_ShouldPass()
        {
            Assert.IsTrue(calculator.Add(“//[***][kkk]\n1.1***1.2kkk2”) == 4.3);
        }

        [Test]
        public void Add_WithTwoSpecialDelimitersOnlyOneChar_ShouldPass()
        {
            Assert.IsTrue(calculator.Add(“//[*][%]\n1*2%3”) == 6);
        }
    }
}

5 thoughts on “TDD Kata | String Calculator

  1. chris says:

    I understand what you’re saying here. With a test driven design you’re creating a nice framework to make sure what you’re writing is going to do what you think it should do. Now if instead of calling it a test driven design we went ahead and called it a behavioral driven design I think you would get more people interested in it because you’re not testing, you actually creating a framework in which you will get predefined results created and built around. In the end you would have a known set of parameters that you would be coding against and have the ability to know when a behavior changes unexpectedly.

  2. 21apps says:

    Teme,

    Its good to see more people doing this, one question i would have about your code is that you don’t seem to have done much on the refactor front. The methods are too long and hard to read quickly. A key part of doing TDD is the refactoring, but you need to think very hard about the maintainability of the code, how easy will it be for a new developer to understand what you code is doing. I find it quite difficult to read your Add method quickly. As a simple example, if the string is null or empty why not just Return 0? That way I don’t have to scroll down to see if sum is later altered.

    But good that you are looking at and doing TDD, if you get the chance to attend Roys course I think you would find this a great use of your time.

    Andrew

    • temebele says:

      Hey Andrew,

      Thanks for your comment.

      If you really look at the Add method, it is actually very few lines of code that just do splitting the characters and the adding them back (Validation and Showing all negative numbers is done on separate helper private methods). One thing to note is that this add method covers every possible scenario in Roy’s TDD Kata(all 11 steps). If it was not for the power of Reg-ex, it wouldn’t have been that easy to implement all scenarios in a very concise way as this. But as we all know Reg-ex implementations are less readable because all the different rules in Roy’s scenarios are now encapsulated(hidden) in the regular expressions.

      About returning zero when string is empty, that goes back to the old “Single vs Multiple exit(return) in a method” arguments. In most scenarios it is my personal opinion that single exit is more readable and more flexible than scattered multiple return statements. But for this specific scenario I am quite OK with returning zero for an empty string.
      All that said, I still believe that there is always room for re-factoring and improvement…

      Unit testing(with mocked dependencies) and integration testing have always been part of our daily coding practice and is integrated into our build process and has really contributed to the quality of our product.

      TDD Katas are definitely great ways to teach and discuss about testing in development teams. Because the concept is more a hands-on thing that one has to try for themselves and see if it really adds value than just somebody else telling them what to do. You and Roy are doing great work in that aspect. Keep it up.

      Teme

  3. Leyu Sisay says:

    You can find additional katacasts in http://katas.softwarecraftsmanship.org/

  4. Wonde says:

    Good article. But you really need to re-factor the code. A constant in if…else block. 😦
    Use re-sharper http://www.jetbrains.com/resharper/

Leave a reply to chris Cancel reply