views:

338

answers:

1

Suppose I have several small test cases. Easy enough to setup and easy enough to execute.
Suppose each small test case could perform multiple checks.

As I buy into the fact that one test case should ideally check one condition, I believe to have a few options to implement such a requirement.

  1. Create a base class with all the test cases' setup and execute parts. Descendant classes each do a verification of one requirement. (solution 1)
  2. Keep everything into one class, write verification methods for each requirement and let each verification method call a shared setup/exercise routine. (solution 2)
  3. (ab)Use NUnit's Sequential attribute to achieve one test for one condition. (solution 3)

As I don't really like solution 1 or 2 because it requires you to lookup what the setup and excercise part of the test does, would you categorize using NUnit's Sequential attribute in such a scenario to be a smell or a solution.


Original: multiple checks/testmethod

[Test]
public void MyTest1()
{
    // Setup
    IMyObject object1 = new MyObject(/* Parameter combination X */)
    this.myObjectList.Add(object1);
    // Exercise
    result = this.LPSolve(this.myObjectList);
    // Check
    Assert.AreSame(null, result.Object);
    Assert.AreEqual(-1, result.Number);
    Assert.True(result.Messages.Contains(ErrorMessage1));
}

[Test]
public void MyTest2()
{
    // Setup
    IMyObject object1 = new MyObject(/* Parameter combination Y */)
    IMyObject object2 = new MyObject(/* Parameter combination Y */)
    this.myObjectList.Add(object1);
    this.myObjectList.Add(object2);
    // Exercise
    result = this.LPSolve(this.myObjectList);
    // Check
    Assert.AreSame(object2, result.Object);
    Assert.AreEqual(8, result.Number);
    Assert.True(result.Messages.Contains(ErrorMessage1));
    Assert.True(result.Messages.Contains(ErrorMessage2));
}


Solution 1: base class with setup and excerise code

[Test]
public virtual void MyTest1()
{
    // Setup
    IMyObject object1 = new MyObject(/* Parameter combination X */)
    this.myObjectList.Add(object1);
    // Exercise
    result = this.LPSolve(this.myObjectList);
    // Check
    MyTest1Check();
}

[Test]
public virtual void MyTest2()
{
    // Setup
    IMyObject object1 = new MyObject(/* Parameter combination Y */)
    IMyObject object2 = new MyObject(/* Parameter combination Y */)
    this.myObjectList.Add(object1);
    this.myObjectList.Add(object2);
    // Exercise
    result = this.LPSolve(this.myObjectList);
    // Check
    MyTest2Check();
}
...
/* Class ObjectCheck */
/* Check result object */
public override MyTest1Check()
{
   Assert.AreSame(null, this.result.Object);        
}

public override MyTest2Check()
{
   Assert.AreSame(this.myObjectList[1], this.result.Object);        
}
...
/* Class NumberCheck */
/* Check result number */
public override MyTest1Check()
{
   Assert.AreSame(-1, this.result.Number);        
}

public override MyTest2Check()
{
   Assert.AreSame(8, this.result.Number);        
}
...
/* and so on */


Solution 2: test methods calling setup/excercise methods

public void SetupMyTest1()
{
    // Setup
    IMyObject object1 = new MyObject(/* Parameter combination X */)
    this.myObjectList.Add(object1);
    // Exercise
    this.result = this.LPSolve(this.myObjectList);
}

[Test]
public void MyTest1ObjectShouldBeNull()
{
    SetupMyTest1();
    // Check
    Assert.AreSame(null, this.result.Object);
}

[Test]
public void MyTest1ResultShouldBeMinus1()
{
    SetupMyTest1();
    // Check
    Assert.AreEqual(-1, this.result.Number);
}

[Test]
public void MyTest1MessageShouldContainErrorMessage1()
{
    SetupMyTest1();
    // Check
    Assert.True(this.result.Messages.Contains(ErrorMessage1));
}

public void SetupMyTest2()
{
    // Setup
    IMyObject object1 = new MyObject(/* Parameter combination Y */)
    IMyObject object2 = new MyObject(/* Parameter combination Y */)
    this.myObjectList.Add(object1);
    this.myObjectList.Add(object2);
    // Exercise
    this.result = this.LPSolve(this.myObjectList);
}

[Test]
public void MyTest2ObjectShouldBeObject2()
{
    SetupMyTest2();
    // Check
    Assert.AreSame(this.myObjectList[1], this.result.Object);
}

[Test]
public void MyTest2ResultShouldBeEight()
{
    SetupMyTest2();
    // Check
    Assert.AreEqual(8, this.result.Number);
}

[Test]
public void MyTest2MessageShouldContainErrorMessage1()
{
    SetupMyTest2();
    // Check
    Assert.True(this.result.Messages.Contains(ErrorMessage1));
}

[Test]
public void MyTest2MessageShouldContainErrorMessage2()
{
    SetupMyTest2();
    // Check
    Assert.True(this.result.Messages.Contains(ErrorMessage2));
}


Solution 3: using attributes keeps the setup/excercise/check in one place and performs one check/test (the two methods are executed 3 & 4 times respectively)

[Test, Sequential]
public void MyTest1([Values("CheckObject", "CheckNumber", "CheckErrorMessage1") string check)
{
    // Setup
    IMyObject object1 = new MyObject(/* Parameter combination X */)
    this.myObjectList.Add(object1);
    // Exercise
    result = this.LPSolve(this.myObjectList);
    // Check
    if (check == "CheckObject") Assert.AreSame(null, result.Object);
    else if (check = "CheckNumber") Assert.AreEqual(-1, result.Number);
    else if (check = "CheckErrorMessage1") Assert.True(result.Messages.Contains(ErrorMessage1));
    else throw new ArgumentOutOfRangeException(check);
}

[Test, Sequential]
public void MyTest2([Values("CheckObject", "CheckNumber", "CheckErrorMessage1", "CheckErrorMessage2") string check)
{
    // Setup
    IMyObject object1 = new MyObject(/* Parameter combination Y */)
    IMyObject object2 = new MyObject(/* Parameter combination Y */)
    this.myObjectList.Add(object1);
    this.myObjectList.Add(object2);
    // Exercise
    result = this.LPSolve(this.myObjectList);
    // Check
    if (check == "CheckObject") Assert.AreSame(object2, result.Object);
    else if (check = "CheckNumber") Assert.AreEqual(8, result.Number);
    else if (check = "CheckErrorMessage1") Assert.True(result.Messages.Contains(ErrorMessage1));
    else if (check = "CheckErrorMessage2") Assert.True(result.Messages.Contains(ErrorMessage2));
    else throw new ArgumentOutOfRangeException(check);
}
+1  A: 

It seems to me that your original two unit tests are easy enough to understand and could be left alone. In a given unit test you want to check one "unit" of functionality, which is not necessarily represented in a single assert.

In the original code, look at MyTest1(). It appears to me that you are using invalid parameters and verifying an error is returned. I would typically name the unit test something like LPSolveReturnsErrorWithInvalidParameters(). This defines one unit to be tested. To prove this functionality is complete, there are three output requirements that must be met

  1. The Object property must be null
  2. The Number property is set to -1
  3. The Messages collection contains the appropriate error info

It seems reasonable to have these three Asserts in a single unit test. The other potential solutions you provide seem overly complex and unnecessary

Pedro
Your answer is very much in line with what I found here (http://www.qatutor.com/3_7.html). It reminds me to the fact that we don't live in a perfect world. I seem to be needing that reminder from time to time.
Lieven