I have recently developed this pattern when I am writing tests that may not be new to some of you, but I wanted to share. It is using the TestCaseData class in combination with the TestCaseSource attribute. I have found that, in using the TestCaseSource for my tests, my code stays much cleaner, becomes easier to read, and allows it to be easily refactored in the future.

The TestCaseData class gives you a way to create a test case and set the information for that test case outside the test method. On TestCaseData, you can set the Test Name, Description, Category, and other information you want for the given test case. This allows you to document your test cases, and the code that is testing, so well that any developer can look at them and know what to expect from your code. You will clearly see with a given input to a method, what the expected output will be.

TestCaseSource is an attribute that you put on your test method to make it parameterized. If you are familiar with the Arrange, Act, and Assert pattern (Read More Here), this is a way that you can move part of your Arrange step out of your test. This allows you to reduce a lot of test code and put the focus of your testing on the behavior of your methods under test instead of all the boiler plate setup required. In using the TestCaseSource, you can collapse multiple test methods that are testing the same method under test into one test method.

Let's see how by digging into an example!

I am going to go back to our HelloWorld class, from previous posts, and make a new method. Let's create a method, GetGreetingByDay, that will take in a string and DayOfWeek and modify it based on those input values.

public static string GetGreetingByDay(DayOfWeek day, string name)  
    string retVal = string.Empty;
    switch (day)
        case DayOfWeek.Monday:
            retVal = $"{day.ToString()} is hard, make it easier with another cup of coffee, {name}!";
        case DayOfWeek.Friday:
            retVal = $"It's {day.ToString()}, {day.ToString()}, gotta get down on {day.ToString()}, {name}!";
            retVal = $"Keep on truckin', {name}!";
    return retVal;

Now with this as our Method Under Test, we can construct the TestCaseData for our test cases.

We will just cover the most basic scenarios:

  • DayOfWeek.Monday and valid string
  • DayOfWeek.Friday and valid string
  • Valid DayOfWeek and valid string
  • Invalid DayOfWeek and valid string
  • null and null

With our scenarios covered, we can start to construct our TestCaseData.

DayOfWeek.Monday and valid string scenario would translate to this TestCaseData:

new TestCaseData(DayOfWeek.Monday, "James")  
    .Returns("Monday is hard, make it easier with another cup of coffee, James!")

When you construct the TestCaseData, you give the values that you will be passing into the test method. In this scenario, that would be DayOfWeek.Monday and James. With that given input, we can specify what the Return value for these given inputs would be. Based off the GetGreetingByDay method, we would expect the string returned to be Monday is hard, make it easier with another cup of coffee, James!.

Now let's construct the rest of our TestCaseData objects and add it to a static list which will be given to the TestCaseSource attribute on our test method.

This could look like:

private static List<TestCaseData> _getGreetingByDayData = new List<TestCaseData>()  
    new TestCaseData(DayOfWeek.Monday, "James").Returns("Monday is hard, make it easier with another cup of coffee, James!"),
    new TestCaseData(DayOfWeek.Friday, "James").Returns("It's Friday, Friday, gotta get down on Friday, James!"),
    new TestCaseData(DayOfWeek.Sunday, "James").Returns("Keep on truckin', James!"),
    new TestCaseData(8, "James").Returns("Keep on truckin', James!"),
    new TestCaseData(null, null).Returns("Keep on truckin', !")

Now that we have our TestCaseData set up, we need to set it as the TestCaseSource of a test method. By sending this into a test method as the TestCaseSource, we will be creating four test cases.

This is what our test method could look like:

public string GetGreetingByDay(DayOfWeek day, string name)  
    string greeting = BlogSamples.HelloWorld.GetGreetingByDay(day, name);
    return greeting;

Now we can run our tests and verify our GetGreetingByDay method was implemented properly based off the criteria we set.

The output from the VS Adapter looks like this:


If we wanted more descript names, we could call the SetName method on each instance of TestCaseData we created to set the Name for the test. By default, this is what the names will look like if you don't explicitly set the name.

new TestCaseData(DayOfWeek.Monday, "James")  
    .Returns("Monday is hard, make it easier with another cup of coffee, James!")
    .SetName("Monday with valid name")

If we wanted to add a description, we could call the SetDescription method the same way we did with the SetName. This is where you can start to document each test case to pin down the expected behavior of your methods. Your future self, or future developers needed to work with your code will thank you!

new TestCaseData(DayOfWeek.Monday, "James")  
    .Returns("Monday is hard, make it easier with another cup of coffee, James!")
    .SetName("Monday with valid name")
    .SetDescription("When you send in Monday along with any string, you will receive a string formatted with Monday and the given string.")

The best part about using this pattern is how it can help you easily fix defects. Say you have a defect where performing some action errors out and in the stack trace you see your method throwing an unhandled exception. In a mature application that has logging, you can get the values that were passed in that caused it to blew up and create a new TestCaseData with those given inputs.

When you run that test, you can expect it to break. From there, you can make your changes and run your tests to verify your fix. Having the existing suite to help you handle any regressions, it makes fixing your code much easier while giving you that piece of mind that you aren't introducing any other defects.

It's worth noting that this isn't something that you always want to do. In the cases when you can, I suggest trying this pattern out. See if it organizes your test code better and gives you the ability to document your code without having to use a BDD framework like Specflow.

How do you guys organize your tests and what are you preferred patterns when writing them? Let me know!

© 2022. All Rights Reserved.

Proudly published with Ghost