Creating Test Cases for Parameterized Tests

Creating Test Cases for Parameterized Tests

Hello, my name is Kazys Račkauskas, and welcome to my third blog post. I'm writing about test-driven development and introducing my pet project, EasyTdd—a tool designed to streamline the TDD process by generating test-related code. In my first blog post, Easy way to start TDD, I covered the entire thought process behind TDD, including my approach, the steps I take, and provided examples. In my second blog post, EasyTdd Quick Start: A Short and Simple Introduction, I dI delved deeper into EasyTdd's test-from-template generation feature, highlighting its configurability to suit various needs and preferences. In this blog post, I will discuss the test case source method and class generation for parameterized tests.

The problem

There are many instances when having a parameterized test is quite convenient. For specific input, a specific output is expected, eliminating the need to write a test method for each case—all within a single test method. Each testing framework has an attribute to provide values for parameterized tests. For MsTest, it would be DataRow; for NUnit, TestCase; and for xUnit, MemberData.

[TestCase("3", 3)]
[TestCase("3.5", 3.5)]
[TestCase("2+2", 4)]
[TestCase("2.5-2.8", -0.3)]
[TestCase("10+20-5.5+8-3+2.8", 32.3)]
public void ReturnsExpectedCalculatedExpression(
    string expression, 
    double expectedResult)
{
    _expression = expression;

    var result = CallCalculate();

    result
        .Should()
        .BeApproximately(expectedResult, 0.001);
}

It works well and is quite useful. However, the issue is that the attribute only allows constant expressions, typeof expressions, and array creation expressions. You will not be successful if a complex type is needed as a parameter.

Fortunately, there are other attributes such as DynamicData, TestCaseSource, and ClassData for MsTest, NUnit, and xUnit, respectively.

[TestCaseSource(nameof(GetCallbackBadRequestWhenRequestIsNotValidCases))]
public async Task CallbackBadRequestWhenRequestIsNotValid(
    PaymentCallbackRequest request)
{
    _request = request;

    var result = await CallCallback();

    result
        .Should()
        .BeOfType<BadRequestResult>();
}

private static IEnumerable<TestCaseData> GetCallbackBadRequestWhenRequestIsNotValidCases()
{
    yield return new TestCaseData(
            new PaymentCallbackRequest { AmountPaid = 200, InvoiceNo = null }
        )
        .SetName("Invoice no is null");

    yield return new TestCaseData(
            new PaymentCallbackRequest { AmountPaid = -1, InvoiceNo = "xx1" }
        )
        .SetName("AmountPaid is negative");
}

In the above example, test cases are provided in the test class method, but it is possible to do the same in a separate class. This approach is convenient and effective. However, what I found annoying is the need to remember the structure and type repeatedly or resort to copying and pasting from elsewhere for each test. EasyTDD features—'Generate Test Cases' and 'Generate Test Cases in External File'—come to the rescue here. With just one click, you can have a test-specific method for test cases or even a separate class.

You might say, "not a big deal." Yes, but there are still some benefits

  • It saves precious seconds and speeds up the process.

  • It keeps me concentrated on the problem.

  • It maintains the same structure across the tests.

Generate Test Cases

Usage

When you have a parameterized test method open, open the Quick Action menu on the test method, and click 'Generate Test Cases'. This action adds the attribute depending on the test framework used in the test project and generates a test method-specific method with placeholders for a couple of test cases. It also adds the test cases attribute to use the newly generated method as a test cases source.

For above CallbackBadRequestWhenRequestIsNotValid test it would generate something like this:

[Test]
[TestCaseSource(nameof(GetCallbackBadRequestWhenRequestIsNotValidCases))]
public async Task CallbackBadRequestWhenRequestIsNotValid(
    PaymentCallbackRequest request)
{
    _request = request;

    var result = await CallCallback();

    result
        .Should()
        .BeOfType<BadRequestResult>();
}

private static IEnumerable<TestCaseData> GetCallbackBadRequestWhenRequestIsNotValidCases()
{
    yield return new TestCaseData(
            default //Set value for request
        )
        .SetName("[Test display name goes here]");

    yield return new TestCaseData(
            default //Set value for request
        )
        .SetName("[Test display name goes here]");
}

All you need to do is add the necessary test data, run the tests (remember, they should fail initially), implement the code, verify that the tests pass, and finally, refactor as needed.

Configuration

As noted in the EasyTdd quick start blog post, all configurations are located in the solution's .easyTdd folder. You can find templates there, as well as a settings.json file which gathers all configurations. The "Generate Test Cases" feature has a "TestCases" section in the settings.json file. Here are the descriptions:

  • NameInMenu - the default value is Test Cases. This minor setting enables you to modify the name displayed in the Quick Actions menu. By default, it will be "Generate Test Cases", but you can rename it to whatever you prefer. For instance, when MsTests are used, the DynamicData attribute is employed. Perhaps you want to name this action "Generate Dynamic Data". Alternatively, in the case of xUnit, you might opt for "Generate Member Data".

  • DefaultTestFramework - the default value is NUnit. EasyTdd attempts to identify the testing framework used in the target test project. If it fails to recognize the framework, it will use templates for the test framework specified here.

  • OutputSettings - this section contains configurations for NUnit, MsTest, and xUnit testing frameworks. Each configuration has the same structure, so I will provide an overview of the structure without focusing on a specific testing framework.

    • AttributeTemplateFile - the default value for NUnit is "DefaultTemplates\nunit.test-cases.attribute.tpl". Here is a path to the template for a test cases source attribute. You might want to change the test cases source method name template.

    • SourceMethodTemplateFile - the default value for NUnit is "DefaultTemplates\nunit.test-cases.source-method.tpl". Here is the path to the template for a test cases source method. You might want to change the test cases source method name, formatting, or structure.

    • TestAttributeNames - the default value for NUnit is [ "Test", "TestCase" ]. This indicates how attributed methods for test cases source method can be generated. I added this setting as otherwise I found it hard to distinguish regular methods from test methods.

As mentioned above, there are two templates associated with this feature: templates associated with the AttributeTemplateFile and SourceMethodTemplateFile settings values. In most cases, defaults should be okay, but if you want to experiment a bit, don't forget to copy/paste the template from DefaultTemplates to the .easyTdd root folder, make changes, save, and update settings.json accordingly. Do not make any changes to templates in DefaultTemplates - those might be overridden with changes after the next release.

Generate Test Cases in External Class

Usage

Open the Quick Action menu on the parameterized test method, and click "Generate Test Cases In External File". This is very similar to "Generate Test Cases", but in this case, the test case source method is generated in a separate class. It is very useful when test cases take up a lot of visual space, as it helps to keep the test code itself cleaner. The "Generate Test Cases In External File" action accomplishes a couple of things:

  • It adds a test framework-specific attribute with values set for the class for test cases.

  • It generates a class with a method and a couple of placeholders for data.

  • It adds the newly added class's using directive to the test class.

For the above CallbackBadRequestWhenRequestIsNotValid test, it would generate something like this:

[Test]
[TestCaseSource(typeof(CallbackBadRequestWhenRequestIsNotValidCases))]
public async Task CallbackBadRequestWhenRequestIsNotValid(
    PaymentCallbackRequest request)
{
    _request = request;

    var result = await CallCallback();

    result
        .Should()
        .BeOfType<BadRequestResult>();
}

And the test case source class:

using System.Collections;

namespace EasyTdd.Blog.No1.MoreComplexExample.PaymentService.Tests.Controllers.TestCases.PaymentControllerTests;

public class CallbackBadRequestWhenRequestIsNotValidCases : IEnumerable
{
    public IEnumerator GetEnumerator()
    {
        yield return new TestCaseData(
                default //Set value for request
            )
            .SetName("[Test display name goes here]");

        yield return new TestCaseData(
                default //Set value for request
            )
            .SetName("[Test display name goes here]");
    }
}

Configuration

The "Generate Test Cases In External Class" feature has a section named TestExternalCases in the settings.json file. Here are the descriptions:

  • NameInMenu - the default value is "Test Cases In External File". This minor setting enables you to modify the name displayed in the Quick Actions menu. By default, it will be "Generate Test Cases In External File", but as mentioned in the above section, you can rename it to whatever you prefer.

  • SourceFilePathTemplate - the default value is TestCases{{testClassName}}. In this particular case, it means that a "TestCases" folder is created in the test's folder, followed by a folder named after the test class name. This is the path for all test case source classes for the test class. In Solution Explorer, it will look something like this:

    SourceClassNameTemplate - the default value is {{testName}}Cases. This is the name template for the test cases source class. If the test name is ResultShouldBeSumOfArguments, then the test cases source class name will be ResultShouldBeSumOfArgumentsCases.

  • OutputSettings - this section contains configurations for NUnit, MsTest, and xUnit testing frameworks. Each configuration has the same structure, so I will provide an overview of the structure without focusing on a specific testing framework.

    • AttributeTemplateFile - the default value for NUnit is "DefaultTemplates\nunit.test-cases-external.attribute.tpl". Here is the path to the template for a test cases source attribute.

    • SourceClassTemplateFile - the default value for NUnit is "DefaultTemplates\nunit.test-cases-external.source-class.tpl". Here is the path to the template for a test cases source class. You might want to change the test cases source class name, formatting, or structure.

    • TestAttributeNames - the default value for NUnit is [ "Test", "TestCase" ]. This indicates how attributed methods for test cases source method can be generated.

    • ToolingNamespaces - the default value for NUnit is [ "NUnit.Framework", "System.Collections" ]. Here additional namespaces could be defined which will be added to the model's usingNamespaces collection. For example, the default class template uses IEnumerable and TestCaseData class, so the above namespaces are required for the code to compile.

Summary

In this blog post, I have covered how to generate test cases methods and test cases classes for parameterized tests. I have also provided information on how it could be configured. I think in most cases, default values should work fine, but feel free to change for your taste and needs. It is not ChatGTP nor other AI, but still, I believe EasyTdd can save a couple of precious seconds or maybe even minutes per test, help to keep the same structure across the tests, and avoid the distraction of needing to type boilerplate code. As a reminder - EasyTdd can be downloaded from Visual Studio Extension Manager or directly from EasyTdd - Visual Studio Marketplace.

Did you find this article valuable?

Support Kazys Račkauskas by becoming a sponsor. Any amount is appreciated!