76

I am writing a data-intensive application. I have the following tests. They work, but they're pretty redundant.

[Test]
public void DoSanityCheck_WithCountEqualsZeroAndHouseGrossIsGreater_InMerchantAggregateTotals_SetsWarning()
{
    report.Merchants[5461324658456716].AggregateTotals.ItemCount = 0;
    report.Merchants[5461324658456716].AggregateTotals._volume = 0;
    report.Merchants[5461324658456716].AggregateTotals._houseGross = 1;

    report.DoSanityCheck();

    Assert.IsTrue(report.FishyFlag);
    Assert.That(report.DataWarnings.Where(x=> x is Reports.WarningObjects.ImbalancedVariables && x.mid == 5461324658456716 && x.lineitem == "AggregateTotals").Count() > 0);
}


[Test]
public void DoSanityCheck_WithCountEqualsZeroAndHouseGrossIsGreater_InAggregateTotals_SetsWarning()
{
    report.AggregateTotals.ItemCount = 0;
    report.AggregateTotals._volume = 0;
    report.AggregateTotals._houseGross = 1;

    report.DoSanityCheck();

    Assert.IsTrue(report.FishyFlag);
    Assert.That(report.DataWarnings.Where(x=> x is Reports.WarningObjects.ImbalancedVariables && x.mid == null && x.lineitem == "AggregateTotals").Count() > 0);
}


[Test]
public void DoSanityCheck_WithCountEqualsZeroAndHouseGrossIsGreater_InAggregateTotalsLineItem_SetsWarning()
{
    report.AggregateTotals.LineItem["WirelessPerItem"].ItemCount = 0;
    report.AggregateTotals.LineItem["WirelessPerItem"]._volume = 0;
    report.AggregateTotals.LineItem["WirelessPerItem"]._houseGross = 1;

    report.DoSanityCheck();

    Assert.IsTrue(report.FishyFlag);
    Assert.That(report.DataWarnings.Where(x=> x is Reports.WarningObjects.ImbalancedVariables && x.mid == null && x.lineitem == "WirelessPerItem").Count() > 0);
}

The same properties are modified at the beginning, just as children of different container objects, and a couple of values in the assertion change at the end.

I need to write a few dozen of these, checking different properties. So I want to parameterize the test. The trick is passing the container object as a parameter to the test. The container object is instantiated in the test fixture SetUp.

I want to achieve something like this:

[TestCase(report.AggregateTotals.LineItem["WirelessPerItem"], 0, "WirelessPerItem")]
[TestCase(report.AggregateTotals, 4268435971532164, "AggregateTotals")]
[TestCase(report.Merchants[5461324658456716].AggregateTotals, 5461324658456716, "WirelessPerItem")]
[TestCase(report.Merchants[4268435971532164].LineItem["EBTPerItem"], 4268435971532164, "EBTPerItem")]
public void DoSanityCheck_WithCountEqualsZeroAndHouseGrossIsGreater_TestCase_SetsWarning(object container, long mid, string field)
{
    container.ItemCount = 0;
    container._volume = 0;
    container._houseGross = 1;

    report.DoSanityCheck();

    Assert.IsTrue(report.FishyFlag);
    Assert.That(report.DataWarnings.Where(x=> x is Reports.WarningObjects.ImbalancedVariables && x.mid == mid && x.lineitem == field).Count() > 0);
}

But that doesn't work and I'm not sure how to make it work, or if it's possible.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Alexander Garden
  • 4,468
  • 3
  • 31
  • 25

4 Answers4

167

I tracked it down. I can't pass an instantiated object into a test via TestCase because attributes are strictly for static meta-data. But the NUnit team has a solution for that, TestCaseSource. The post on the NUnit list that answered the question is here.

Here is what my solution now looks like:

public static IEnumerable<TestCaseData> CountEqualsZeroAndHouseGrossIsGreaterTestCases
{
    get
    {
        yield return new TestCaseData(report, report.Merchants[4268435971532164].LineItem["EBTPerItem"], 4268435971532164, "EBTPerItem").SetName("ReportMerchantsLineItem");
        yield return new TestCaseData(report, report.Merchants[5461324658456716].AggregateTotals, 5461324658456716, "WirelessPerItem").SetName("ReportMerchantsAggregateTotals");
        yield return new TestCaseData(report, report.AggregateTotals, null, "AggregateTotals").SetName("ReportAggregateTotals");
        yield return new TestCaseData(report, report.AggregateTotals.LineItem["WirelessPerItem"], null, "WirelessPerItem").SetName("ReportAggregateTotalsLineItem");
    }
}


[TestCaseSource("CountEqualsZeroAndHouseGrossIsGreaterTestCases")]
public void DoSanityCheck_WithCountEqualsZeroAndHouseGrossIsGreater_TestCase_SetsWarning(Reports.ResidualsReport report, Reports.LineItemObject container, long? mid, string field)
{
    container.ItemCount = 0;
    container._volume = 0;
    container._houseGross = 1;

    report.DoSanityCheck();

    Assert.IsTrue(report.FishyFlag);
    Assert.That(report.DataWarnings.Where(x=> x is Reports.WarningObjects.ImbalancedVariables && x.mid == mid && x.lineitem == field).Count() > 0);
}

It is not as pretty as I hoped and is not as easy to read. But it did succeed on cutting down code duplication, which should make things easier to maintain and fix.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Alexander Garden
  • 4,468
  • 3
  • 31
  • 25
  • 4
    I think that CountEqualsZeroAndHouseGrossIsGreaterTestCases property should be static – moyomeh Jan 02 '17 at 10:53
  • 24
    If you are using C# 6+, instead of using the name as a string you can use 'nameof'. [TestCaseSource(nameof(CountEqualsZeroAndHouseGrossIsGreaterTestCases))], which makes it strongly typed. – hatsrumandcode Apr 13 '17 at 14:26
  • 8
    As of NUnit 3, TestCaseSource is limited to static sources as well. – buckminst Oct 26 '17 at 20:24
  • @buckminst what is the preferred solution as of nunit 3, then? – Justin Nov 30 '17 at 15:00
  • I am passing an initialised objects (Company.Adult.MinAge) and get the error: The sourceName specified on a TestCaseSourceAttribute must refer to a static field, property or method. This adult was created as... public static Ages Adult = new Ages { MinAge = 16 }; any idea about how to avoid that failure and be able to pass that object to a TestCase/TestCaseSource,etc? – mickael Aug 05 '18 at 03:14
  • 1
    Just figure it out. The example above seems to be missing the 'static' keyword to make it work: public static IEnumerable CountEqualsZeroAndHouseGrossIsGreaterTestCases – mickael Aug 05 '18 at 16:00
  • I couldn't make it work with the SetName() method - using NUnit 3.12.0. Also for TestCaseData I recommend using a separate class implementing IEnumerable and then pass it with typeof. It's better than using strings even with nameof. Look at official documentation: https://github.com/nunit/docs/wiki/TestCaseSource-Attribute – Tomasz Chudzik Mar 28 '20 at 16:33
10

I pass strings that I parse sometimes, and I think it reads pretty well.

Example:

[TestCase("15°", "-10°", 25, typeof(Degrees))]
[TestCase("-10°", "15°", -25, typeof(Degrees))]
[TestCase("-10°", "0°", -10, typeof(Degrees))]
[TestCase("-90°", "1.5707 rad", -3.1414, typeof(Radians))]
[TestCase("1.5707 rad", "-90°", 3.1414, typeof(Radians))]
[TestCase("1.5707 rad", "1.5707 rad", 0, typeof(Radians))]
public void SubtractionTest(string lvs, string rvs, double ev, Type et)
{
    var lv = Angle.Parse(lvs);
    var rv = Angle.Parse(rvs);
    var diff = lv - rv;
    Assert.AreEqual(ev, diff.Value, 1e-3);
    Assert.AreEqual(et, diff.Unit.GetType());
}
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Johan Larsson
  • 17,112
  • 9
  • 74
  • 88
0

It would be much much easier to have a private method, base class method, or helper classes that do this for you.

For my unit tests, I need many many mock entities because it's a very data-intensive application. I've created a structure of mock repositories that can create initialized entities on the fly, which I can combine to build up a representative database structure in memory.

Something like that could work for you:

// Wild guess at the class name, but you get the idea
private void InitializeTotals(AggregateItem item)
{
    item.ItemCount = 0;
    item._volume = 0;
    item._houseGross = 1;
}

[Test]
public void DoSanityCheck_WithCountEqualsZeroAndHouseGrossIsGreater_InMerchantAggregateTotals_SetsWarning()
{
    InitializeTotals(report.Merchants[5461324658456716].AggregateTotals);

    report.DoSanityCheck();

    Assert.IsTrue(report.FishyFlag);
    Assert.That(report.DataWarnings.Where(x => x is Reports.WarningObjects.ImbalancedVariables && x.mid == 5461324658456716 && x.lineitem == "AggregateTotals").Count() > 0);
}

[Test]
public void DoSanityCheck_WithCountEqualsZeroAndHouseGrossIsGreater_InAggregateTotals_SetsWarning()
{
    InitializeTotals(report.AggregateTotals);

    report.DoSanityCheck();

    Assert.IsTrue(report.FishyFlag);
    Assert.That(report.DataWarnings.Where(x => x is Reports.WarningObjects.ImbalancedVariables && x.mid == null && x.lineitem == "AggregateTotals").Count() > 0);
}

[Test]
public void DoSanityCheck_WithCountEqualsZeroAndHouseGrossIsGreater_InAggregateTotalsLineItem_SetsWarning()
{
    InitializeTotals(report.AggregateTotals.LineItem["WirelessPerItem"]);

    report.DoSanityCheck();

    Assert.IsTrue(report.FishyFlag);
    Assert.That(report.DataWarnings.Where(x => x is Reports.WarningObjects.ImbalancedVariables && x.mid == null && x.lineitem == "WirelessPerItem").Count() > 0);
}
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Pieter van Ginkel
  • 29,160
  • 8
  • 71
  • 111
  • That's an improvement but it still leaves an awful lot of redundancy. It's hard to read that code and quickly get at the gist of what is being tested in each iteration. Parameters would make it clear that the same thing is being tested on different levels. All those tests would collapse into one. – Alexander Garden Nov 19 '10 at 19:42
  • It's not advisable to collapse all tests into a single test. If you have separate tests for separate issues, it's easier to find out what goes right and what goes wrong. If you put too much into a single test, you test too much at once and it becomes more difficult to solve the issues. http://www.infoq.com/presentations/integration-tests-scam has a good presentation which also talks about such issues. – Pieter van Ginkel Nov 20 '10 at 05:28
  • 5
    Right. But with the parameterized tests, I write the code once, but it runs as if each set of parameters is a separate test. Each TestCase gets its own line in the NUnit test runner. So it's still clear which part exactly is failing, but the code redundancy is eliminated, which saves time writing and is easier to read. – Alexander Garden Nov 22 '10 at 15:46
0
 [Test]
 [TestCase("textbox", true, "Text is empty", null, false)]
 [TestCase("textbox", false, "Text is empty", null, true)]
 public void Test_Component_Validation_and_ValidationText__Whether_IsMandatory_IsSet(string textbox, bool isMandatory, string validationText, string value, bool expectedValue)
 {
     // Arrange
     var mockPublicPortalService = new Mock<IPublicPortalService>();
     PublicAssessmentController controller = new PublicAssessmentController(mockPublicPortalService.Object);

     // Set Component properties
     var Component = new Component()
     {
         ComponentDatatype = textbox,
         IsMandatory = isMandatory,
         ValidationText = validationText,
         Value = value
     };

     var context = new ValidationContext(Component);

     // Act
     var results = new List<ValidationResult>();
     var isModelStateValid = Validator.TryValidateObject(Component, context, results, true);

     // Assert
     Assert.AreEqual(expectedValue, isModelStateValid);
     if (isModelStateValid == false)
     {
         Assert.IsTrue(results.Any(x => x.ErrorMessage == validationText));
     };
 }
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Ben
  • 21
  • 2