Friday, January 29, 2016

NUnit TestCaseSource Attribute

As you've probably picked up if you've read my other posts (I'm pretty sure I said it outright), I'm a huge fan of automated unit testing.  I've used MS Test and NUnit to test my C# code and I used to think they were pretty much equal.  However, I found something in NUnit recently that has me leaning their way.  To be fair, I'm not sure whether this is possible with MS Test and I have no reason to find out (we're using NUnit at the client).

I tend to write long test names.  Like, really long.  I tell people that I prefer clarity in the name over brevity.  I've also recently been converted to the single test philosophy where a single unit test only checks one thing.  That means that if you have a complex object of a Person you'd have one test for FirstName and another test for LastName, for example.  As you can probably imagine, my unit test files were pretty big.  That has a lot of negative ramifications, not the least of which is that it's hard to find whether you've already tested something.

UPDATE: This isn't the best example.  Check out this other post for a simpler way to do this.

Enter NUnit's TestCaseSourceAttribute and iterators.  What you can do is write a single test that accepts parameters, then create an iterator-based property that runs that test with different parameters.

Let's say we have a method called GetFullName, which takes a first, middle, and last name and concatenates them to make a full name:
   1:  public string GetFullName(string firstName, string lastName, string middleName)
   2:  {
   3:      throw new NotImplementedException();
   4:  }

So we write a basic test to confirm that when we pass all three parameters we get them back as "[first] [middle] [last]".  We'll call it "GetFullName_ShouldConcatenateFirstMiddleAndLastWhenAllThreeHaveValues":
   1:  [Test]
   2:  public void GetFullName_ShouldConcatenateFirstMiddleAndLastWhenAllThreeHaveValues()
   3:  {
   4:      // arrange
   5:      var program = new Program();
   6:   
   7:      // act
   8:      var fullName = program.GetFullName("Jumping", "Flash", "Jack");
   9:   
  10:      // assert
  11:      Assert.AreEqual("Jumping Jack Flash", fullName);
  12:  }

We update the method so the test passes:
   1:  public string GetFullName(string firstName, string lastName, string middleName)
   2:  {
   3:      return string.Format("{0} {1} {2}", firstName, middleName, lastName);
   4:  }

So now we have a problem.  Our test passes, but it's only the positive test case.  This method will clearly only work the way we want if we pass all three names.  If first name is null or empty we'll end up with a leading space we don't want.  We can create more tests to handle null values and empty strings, but that's a lot of combinations of values for a really simple method.  What we can do instead is use TestCaseSource and iterators.  Here's how we'd rewrite that one test this new way:
   1:  public static IEnumerable GetFullNameTestsCases
   2:  {
   3:      get
   4:      {
   5:          yield return
   6:              new TestCaseData("Jumping", "Flash", "Jack", "Jumping Jack Flash").SetName(
   7:                  "ShouldConcatenateFirstMiddleAndLastWhenAllThreeHaveValues");
   8:      }
   9:  }
  10:   
  11:  [Test, TestCaseSource("GetFullNameTestsCases")]
  12:  public void GetFullName(string firstName, string lastName, string middleName, string expectation)
  13:  {
  14:      // arrange
  15:      var program = new Program();
  16:   
  17:      // act
  18:      var fullName = program.GetFullName(firstName, lastName, middleName);
  19:   
  20:      // assert
  21:      Assert.AreEqual(expectation, fullName);
  22:  }

What we're doing here is creating the test data in the GetFullNameTestCases property, then we're specifying where the test should get its inputs by using the TestCaseSource attribute on the test itself.  As long as the number of parameters matches the number of arguments, everything will work fine.  Furthermore, we can add additional tests really easily just by adding more yield return statements.  Let's say we wanted to test for a null first name.  We just add this to the property:
   1:  yield return
   2:      new TestCaseData(null, "Flash", "Jack", "Jack Flash").SetName(
   3:          "ShouldConcatenateFirstMiddleAndLastWhenAllThreeHaveValues");

Next time we run our test, both sets of test data will get picked up and passed separately to the test method.  This method can greatly speed up your test writing and make it easier to cover more cases.

No comments:

Post a Comment