现在阅读
Why I'm Now a TDD Believer (sort of…)
0

Why I'm Now a TDD Believer (sort of…)

由 ultracpy2018年1月26日

Introduction

In this article, I’ll present how TDD potentially saved me from some real problems. Additionally, I’ll enumerate some points where I think TDD may fall a bit short w/r/t architecture. Critiques absolutely welcome.

Background

In my dev work, my company has not really embraced unit testing (yet), much less TDD. I read an article today talking about using Fibonacci series as a coding test for interviewing developers, so of course I had to give it a shot.

Typically when undertaking a one-off app such as this, I’d just use a console app to verify my results (how lame). Of course, if my goal is to verify my results, then unit tests sound pretty sweet. Taking that a step further, why not give TDD a crack at this?

I’m using VS2012 Pro with the built-in MSFT testing framework. No other dependencies required, and you should be able to follow along with the code below if you’d like.

Walkthrough: TDD’ing the Fibonacci Series

First, let’s get our solution set up:

  • TDDFibonacci.Biz, class library
  • TDDFibonacci.Tests, unit test project, referencing the Biz project.

After that, delete the Class1 from the biz project (we’ll add our own class in a bit) and rename UnitTest1 to FibonacciGeneratorTest. Rename TestMethod1() to GetBasicSequenceTest(). The code will look something like this:

using System;
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using TDDFibonacci.Biz;
 
namespace TDDFibonacci.Tests {
   [TestClass]
   public class FibonacciGeneratorTest {
 
      [TestMethod]
      public void GetBasicSequenceTest() {
         // Arrange
         FibonacciGenerator generator = new FibonacciGenerator();
 
         // Act
         List<int> fibSequence = generator.GetFibonacciSequence(100);
 
         // Assert
         Assert.AreEqual(fibSequence[6], 13);
      }
 
   }
}

Of course, this won’t run without the FibonacciGenerator class being added, so create it in the biz project and make it public (TDD folks, is there a better way to do this? The Generate Class option puts it in the test project, which is exactly what I don’t want). Then generate the method stud, and we’ll get a non-implemented method. For now, I’m going to be lame, just to make sure my test runs properly:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
namespace TDDFibonacci.Biz {
   public class FibonacciGenerator {
 
      /// <summary>
      /// Gets a Fibonacci sequence
      /// </summary>
      /// <param name="numberOfResults">Number of items you want to return.</param>
      /// <returns>Fibonacci sequence for numberOfResults items.</returns>
      public List<int> GetFibonacciSequence(int numberOfResults) {
         List<int> retVal = new List<int>();
 
         retVal.Add(1);
         retVal.Add(1);
         retVal.Add(2);
         retVal.Add(3);
         retVal.Add(5);
         retVal.Add(8);
         retVal.Add(13);
 
         return retVal;
      }
 
   }
}

Now let’s refactor this to be a bit less lame. I’m not sure if this algorithm has been done before (I’m sure it probably has), but I did derive it on my own, hence no attribution, so don’t sue me.

public List<int> GetFibonacciSequence(int numberOfResults) {
 List<int> retVal = new List<int>();

 int nMinusOne = 0;
 int nMinusTwo = 1;
 int currVal = 0;
 while (retVal.Count < numberOfResults) {
    currVal = nMinusOne + nMinusTwo;
    retVal.Add(currVal);
    nMinusTwo = nMinusOne;
    nMinusOne = currVal;
 }

 return retVal;
}

Run tests again, and you should be still good.

Next, we need to make sure that a.) a non-negative number isn’t passed, and b.) a list of zero and one values return what we want. So, let’s add the following tests:

[TestMethod]
[ExpectedException(typeof(System.ArgumentOutOfRangeException))]
public void ValidateArgumentIsNotNegativeTest() {
 // Arrange
 FibonacciGenerator generator = new FibonacciGenerator();

 // Act
 List<int> fibSequence = generator.GetFibonacciSequence(-1);

 // Assert
 // Shouldn't need to assert here? 
}

[TestMethod]
public void ValidateZeroListTest() {
 // Arrange
 FibonacciGenerator generator = new FibonacciGenerator();

 // Act
 List<int> fibSequence = generator.GetFibonacciSequence(0);

 // Assert
 Assert.AreEqual(fibSequence.Count, 0);
}
 
[TestMethod]
public void ValidateOneItemInListTest() {
 // Arrange
 FibonacciGenerator generator = new FibonacciGenerator();

 // Act
 List<int> fibSequence = generator.GetFibonacciSequence(1);

 // Assert
 Assert.AreEqual(fibSequence.Count, 1);
 Assert.AreEqual(fibSequence[0], 1);
}

Run all, and all should pass except the ValidateArgumentIsNotNegativeTest(), since we’re not currently validating the param in the method. Add this line to the beginning of GetFibonacciSequence(int), and we should then pass the tests:

if (numberOfResults < 0) {
   throw new ArgumentOutOfRangeException("numberOfResults must be non-negative.");
}

All good, right? Hardly. We’ve tested a standard case, invalid parameters, and border cases on the small side, but not on the big side. Add this line to GetBasicSequenceTest(), and watch it fail:

Assert.IsTrue(fibSequence[99] > 0);

Oops. We’re hitting into overflow issues with the int type. IMO, this was the biggest “A-Ha!” moment for me; if I’d actually used this in production, I could have had all kinds of problems. Fortunately, changing the int to a ulong solves it, but I would have had some real problems updating production code, in all likelihood. Since it was caught in test, it’s simply a matter of updating my return value to a ulong and updating the individual tests, plus throwing in a large list test, just in case.

Complete code follows. Note: I’m sure there are other tests I can do here, but these sufficed my purposes for now. Plus, I’m not sure TDD is ever really ‘finished’, much like fashion is never ‘finished’ (props to anyone catching the movie reference).

using System;
using System.Collections.Generic;
 
namespace TDDFibonacci.Biz {
   public class FibonacciGenerator {
 
      /// <summary>
      /// Gets a Fibonacci sequence
      /// </summary>
      /// <param name="numberOfResults">Number of items you want to return.</param>
      /// <returns>Fibonacci sequence for numberOfResults items.</returns>
      public List<ulong> GetFibonacciSequence(int numberOfResults) {
         if (numberOfResults < 0) {
            throw new ArgumentOutOfRangeException("numberOfResults must be non-negative.");
         }
         List<ulong> retVal = new List<ulong>();
 
         ulong nMinusOne = 0;
         ulong nMinusTwo = 1;
         ulong currVal = 0;
         while (retVal.Count < numberOfResults) {
            currVal = nMinusOne + nMinusTwo;
            retVal.Add(currVal);
            nMinusTwo = nMinusOne;
            nMinusOne = currVal;
         }
 
         return retVal;
      }
 
   }
}


using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using TDDFibonacci.Biz;
 
namespace TDDFibonacci.Tests {
   [TestClass]
   public class FibonacciGeneratorTest {
 
      [TestMethod]
      public void GetBasicSequenceTest() {
         // Arrange
         FibonacciGenerator generator = new FibonacciGenerator();
 
         // Act
         List<ulong> fibSequence = generator.GetFibonacciSequence(100);
 
         // Assert
         Assert.AreEqual(fibSequence[6], (ulong)13);
         Assert.IsTrue(fibSequence[99] > 0);
      }
 
      [TestMethod]
      [ExpectedException(typeof(System.ArgumentOutOfRangeException))]
      public void ValidateArgumentIsNotNegativeTest() {
         // Arrange
         FibonacciGenerator generator = new FibonacciGenerator();
 
         // Act
         List<ulong> fibSequence = generator.GetFibonacciSequence(-1);
 
         // Assert
         // Shouldn't need to assert here? 
      }
 
      [TestMethod]
      public void ValidateZeroListTest() {
         // Arrange
         FibonacciGenerator generator = new FibonacciGenerator();
 
         // Act
         List<ulong> fibSequence = generator.GetFibonacciSequence(0);
 
         // Assert
         Assert.AreEqual(fibSequence.Count, 0);
      }
 
      [TestMethod]
      public void ValidateOneItemInListTest() {
         // Arrange
         FibonacciGenerator generator = new FibonacciGenerator();
 
         // Act
         List<ulong> fibSequence = generator.GetFibonacciSequence(1);
 
         // Assert
         Assert.AreEqual(fibSequence.Count, 1);
         Assert.AreEqual(fibSequence[0], (ulong)1);
      }
 
      [TestMethod]
      public void GetLargeListTest() {
         // Arrange
         FibonacciGenerator generator = new FibonacciGenerator();
 
         // Act
         List<ulong> fibSequence = generator.GetFibonacciSequence(1000);
 
         // Assert
         foreach (var item in fibSequence) {
            Assert.IsTrue(item > 0);
         }
      }
 
   }
}

Points of Interest

I started this just to do some practice on TDD, but the int to ulong bug really caught my eye. How likely would it have been to not even think about that, use it in code, (hopefully) catch it during testing, then have to modify a bunch of calling code? In my experience, pretty damn likely. Using TDD, I was able to catch it before it was even used anywhere. Now if I choose to modify my algorithm or whatever, I can be confident that anything I might break will (most likely) be caught and that I won’t have to worry about corrupting production code.

All that goodness comes at a price, IMO: I still don’t see how TDD can really be used for system architecture purposes. Scratch that: I see how, but it still seems rather inefficient to me as far as helper classes handling database connections, etc. Maybe that’s simply a lack of TDD experience on my part, so let me (and us!) know what you think.

出处:https://www.codeproject.com/Articles/593555/Why-Im-Now-a-TDD-Believer-sort-of

关于作者
ultracpy
评论

你必须 登录 提交评论