g.pickardou
g.pickardou

Reputation: 35963

Is there any way to use NUnit TestCaseAttribute with ValuesAttribute together?

I am using intensively NUnit TestCase attribute. For some of my tests are annotated with 20+ TestCase attributes defining 20+ test cases. However I would like to test all the 20 test cases say with an extra value what could be 1 or 0. This means for me different test cases. This easily could be implemented with ValuesAttribute:

My current state:

[TestCase(10, "Hello", false)] // 1
[TestCase(33, "Bye", true)]    // 2
// imagine 20+ testcase here)]
[TestCase(55, "CUL8R", true)]    // 20+
public void MyTest(int number, string text, bool result)

I would like to do something similar to this (what I can not:)

[TestCase(10, "Hello", false)] // 1
[TestCase(33, "Bye", true)]    // 2
// imagine 20+ testcase here)]
[TestCase(55, "CUL8R", true)]    // 20+
public void MyTest([Values(0,1)] int anyName, int number, string text, bool result)

Why I would like to do this? Because these 40+ combination means different test cases. Unfortunately NUnit does not allow using [TestCase] and [Values] attributes together, the test runner expects exactly the same number of parameters as it listed in TestCaseAttribute. (I can understand the architect, but still...) The only thing I could figure out was this:

[TestCase(1, 10, "Hello", false] // 1
[TestCase(1, 33, "Bye", true]    // 2
// imagine 20+ testcase here]
[TestCase(1, 55, "CUL8R", true]  // 20

[TestCase(0, 10, "Hello", false] // 21
[TestCase(0, 33, "Bye", true]    // 22
// imagine 20+ testcase here]
[TestCase(0, 55, "CUL8R", true]  // 40
public void MyTest(int anyName, int number, string text, bool result)

So I ended up to be forced to commit the sin of the copy and paste, and I duplicated the TestCases, now I have 40+. There must be some way... What if not only (0,1) the range of the value but 0,1,2,3. We are ending with 80+ copied testcases?

Missed I something?

Thx in advance

Upvotes: 19

Views: 5123

Answers (2)

Andrew Gray
Andrew Gray

Reputation: 3671

Firstly, The class that provides the test data is test code in its own right and is therefore first class code and should be maintained as such.

If you give the class a meaningful name I would contend it can be readable and maintainable, perhaps more so, as your test data is in one place.

This is how I would implement your case:

  1. Make a class, we'll call it TestCaseGenerator that has int, string and bool attributes. it would look like this:

     public class TestCaseGenerator : IEnumerable {
    
         #region Attributes
         internal static List<TestCase> TestCases { get; set; }
         internal int myInt { get; set; }
         internal string myString { get; set; }
         internal bool myBoolean { get; set; }
    
         #endregion
    
         static TestCaseGenerator() {
             var json = <jsonRepresentationOfMyTestData>
             TestCases = JsonConvert.DeserializeObject<List<TestCase>>(json);    
         }
    
         public IEnumerator GetEnumerator() {
             return TestCases != null
                       ? TestCases.Select(x => new TestCase(x.myInt, x.myString, x.myBool) ?? "null")).GetEnumerator()
                       : null;
         }
      }
    
  2. Construct the class containing your tests like this:

     public class MyTestClass {
         [TestCaseSource(typeof(TestCaseGenerator))]
         public void MyTest(TestCase case) {
             // Do useful things with case.myInt, case.myString and case.myBool
         }
     }
    

Once you have this infrastructure in place there is nothing to say TestCaseGenerator.GetEnumerator() which returns an enumerator over a list of TestCase objects couldn't be written in a way to return a combined list of two shorter lists.

Creation of a combined enumerable (borrowing from this post) would look like this:

class Program {
    static void Main(string[] args) {
        var firstArray = new object[][] { new object[] { 1, "A", false },
                                          new object[] { 2, "B", false },
                                          new object[] { 3, "C", false },
                                          new object[] { 4, "D", true },
                                          new object[] { 5, "E", true }
                                       };

        var secondArray = new object[] { 6, 7 };

        var joiner = new Joiner();
        IEnumerable<IEnumerable<object>> result = joiner.Join(firstArray.ToList(), secondArray.ToList());

        //result = new object[] { new object[] { 6, 1, "A", false },
        //                        new object[] { 7, 1, "A", false },
        //                        new object[] { 6, 2, "B", false },
        //                        new object[] { 7, 2, "B", false },
        //                        new object[] { 6, 3, "C", false },
        //                        new object[] { 7, 3, "C", false },
        //                        new object[] { 6, 4, "D", true },
        //                        new object[] { 7, 4, "D", true },
        //                        new object[] { 6, 5, "E", true },
        //                        new object[] { 7, 5, "E", true }
        //                      };
    }
}

public class Joiner
{
    public IEnumerable<IEnumerable<object>> Join(IEnumerable<IEnumerable<object>> source1, IEnumerable<object> source2) {
        foreach (IEnumerable<object> s1 in source1) {
            foreach (object s2 in source2) {
                yield return (new[] { s2 }).Concat(s1).ToArray();
            }
        }
    }
}

Upvotes: -1

AjimOthy
AjimOthy

Reputation: 711

This came up as a first page search result for me despite its age, but I wanted something lighter than the existing solution.

Using another ValuesAttribute or ValueSourceAttribute for the test cases allows combinations, but the trick is to get a set of linked values as a single case when using them - each affects only a single named parameter.

Attributes require compile-time constant inputs. That allows you to make a literal array, but if your values are of different types, you'll have to make the array a common base type, like object. You'll also have to access the items by index. I like short, obvious unit tests; parsing the array makes the test look busy and messy.

One terse solution is to use a ValueSource attribute with a static method providing tuples. This method can immediately precede the test method, and is only a little more verbose than using the TestCase attribute. Using the code from the question:

public static (int, string, bool)[] Mytest_TestCases()
{
    return new[] {
        (10, "Hello", false),
        (33, "Bye", true),
        (55, "CUL8R", true)
    };
}

[Test]
public void Mytest(
    [Values(0,1)] int anyName,
    [ValueSource(nameof(Mytest_TestCases))] (int number, string text, bool result) testCase)

The parameter following the ValueSource is a tuple named testCase, and its contents are named to match the original test case arguments. To reference those values in the test, precede it with the name of the tuple (e.g. testCase.result instead of just result).

As written here, six tests will run - one for each possible combination of anyName and testCase.

I don't mind tuples for simple data like in this example, but I went a little further and defined a very simple class to use in place of the tuples. Usage is basically the same, except you don't name the members within the parameter.

public class Mytest_TestCase
{
    public Mytest_TestCase(int number, string text, bool result)
    {
        Number = number;
        Text = text;
        Result = result;
    }
    public int Number;
    public string Text;
    public bool Result;
}

public static Mytest_TestCase[] Mytest_TestCases()
{
    return new[] {
        new Mytest_TestCase(10, "Hello", false),
        new Mytest_TestCase(33, "Bye", true),
        new Mytest_TestCase(55, "CUL8R", true)
    };
}

[Test]
public void Mytest(
    [Values(0,1)] int anyName,
    [ValueSource(nameof(Mytest_TestCases))] Mytest_TestCase testCase)

The test case class definition could be moved to the bottom of the test class, or to a separate file. Personally, I prefer to put both at the bottom of the test class - you can always peek the definition when you want to see it next to the test.

Upvotes: 10

Related Questions