Wednesday, 6 August 2008

Pex - Test Case 6

Hello,

In the previous Pex posts I used examples that used primitive types and/or strings as parameters for methods under test. In this post I will "test" Pex how it wil cope with a method that accepts a "complex type". This test scenario is similar to the case study in Pex - Test case 3 except we don't work directly with primitive types a method parameters or return types.


Suppose the HR department asked you to include a new module in their Salary-application that will determine the contribution each employee has to make in a private pension fund. The salary administration uses the following desicion table.



While analysing the table you conclude you can simplify the table to help you code this rule into c#.



You already have some classes in the application :


public class Employee
{
//missing elementary checks in order to keep code concise....
public int EmployeeID { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
private DateTime _BirthDate;
public DateTime BirthDate
{
get { return this._BirthDate; }
set {
this._BirthDate = value;
this.Age = differenceInYears(_BirthDate, DateTime.Now);
}
}
private DateTime _StartDateContract;
public DateTime StartDateContract {
get { return _StartDateContract; }
set {
_StartDateContract=value;
this.ServiceYears = differenceInYears(_StartDateContract, DateTime.Now);
} }
public uint Salary { get; set; }
public Contract TypeContract { get; set; }
public uint Age { get; set; }
public uint ServiceYears { get; set; }
private uint differenceInYears(DateTime aDate1, DateTime aDate2)
{
//missing checks ....
//approximative
uint years = (uint)(aDate1.Year - aDate2.Year);
if (aDate1.Month == aDate2.Month)
{
if (aDate2.Day < aDate1.Day) years = years - 1;
}
return years;
}
}

public enum Contract
{
FullTime=0,
PartTime=1
}

public struct PensionContribution
{
public uint BaseContribution{get;set;}
public uint SupplementaryContribution {get;set;}
}


The rule has also been translated into c#. The method DeterminePensionContribution has a parameter of the type Employee as described in the above code snippet. The return type is the structure PensionContribution.


public class EmployeeServices
{
public PensionContribution DeterminePensionContribution(Employee aEmployee)
{
//no pre-condition checks .....

PensionContribution contribution = new PensionContribution ();

if (aEmployee.Salary <= 3000)
{
if (aEmployee.Age < 45)
{
if (aEmployee.TypeContract == Contract.FullTime)
{
contribution.BaseContribution = 80;
contribution.SupplementaryContribution = 15;
}
else
{
contribution.BaseContribution = 80;
contribution.SupplementaryContribution = 0;
}
}
else
{
if (aEmployee.ServiceYears < 5)
{
contribution.BaseContribution = 80;
contribution.SupplementaryContribution = 15;
}
else
{
contribution.BaseContribution = 80;
contribution.SupplementaryContribution = 25;
}
}
}
else
{
if (aEmployee.Age < 45)
{
if (aEmployee.ServiceYears < 5)
{
contribution.BaseContribution = 135;
contribution.SupplementaryContribution = 30;
}
else
{
if (aEmployee.TypeContract == Contract.FullTime)
{
contribution.BaseContribution = 135;
contribution.SupplementaryContribution = 25;
}
else
{
contribution.BaseContribution = 135;
contribution.SupplementaryContribution = 20;
}
}
}
else
{
if (aEmployee.ServiceYears < 5)
{
contribution.BaseContribution = 135;
contribution.SupplementaryContribution = 30;
}
else
{
contribution.BaseContribution = 135;
contribution.SupplementaryContribution = 20;

}
}

}
return contribution;
}






}


In a "test-after-you-code" scenario, you could use the desicion table to specify the values for actual for the different properties of a employee object that is being passed to the method under test. So 9 unit tests would be sufficient to touch all corners of the code.

A data-driven in test in Visual Studio is somewhat difficult because we are not dealing with simple datatype parameters. We could serialize the different instances and save it in string form into an external data source.

Can Pex help generate the necessary unit test to fully test our code?

Before heading to the PUT (Parameterized Unit Test) I would like to refer you to the Pex tutorial for more background information on setting up PexMethods.

A new element in this case study is the use of a factory method to create our Employee object that will be used in the generated unit test. Pex will call this method with appropriate values that it has discovered during its exploration of the code in the generated unit tests. The cool part is that Pex will first complain about not being able to create a Employee object but then suggest a soulution for it : the Factoty method and generates it for us.


[PexFactoryClass]
public partial class EmployeeFactory
{
[PexFactoryMethod(typeof(Employee))]
public static Employee Create(
int i0,
string s0,
string s1,
DateTime dt0,
DateTime dt1,
uint ui0,
Contract c0
)
{

Employee e0 = new Employee();
e0.EmployeeID = i0;
e0.FirstName = s0;
e0.LastName = s1;
e0.BirthDate = dt0;
e0.StartDateContract = dt1;
e0.Salary = ui0;
e0.TypeContract = c0;
return e0;
}

}

In the generated unit test , this factory method is used as follows:

[TestMethod]
[PexGeneratedBy(typeof(EmployeeServicesTest))]
public void DeterminePensionContributionEmployeeServicesEmployee_20080806_095656_001()
{
PexValue.Generated.Clear();
Employee e0;
DateTime s0 = new DateTime(626406282140467200L);
DateTime s1 = new DateTime(631149005309067264L);
e0 = EmployeeFactory.Create(2, "", "", s0, s1, 8192u, Contract.FullTime);
EmployeeServices es0 = new EmployeeServices();
this.DeterminePensionContribution(es0, e0);
PexValue.Generated.Validate("BaseContribution", "135");
PexValue.Generated.Validate("SupplementaryContribution", "20");
}


Let's return to the PUT. The following PUT was based on a PexMethod that you can let the Visual Studio Pex integration build for you.
During its explorartion Pex will try out different value for the properties of our Employee object. The assumptions should help Pex create reasonable test cases. The code in this example did not enforce them in order to keep the code somewhat limited in size.

Because this method will be used for all generated unit test, the asserts have to be more general than you would normally do for a single unit test.


[TestClass]
[PexClass(typeof(EmployeeServices))]
public partial class EmployeeServicesTest
{
[PexMethod(MaxConstraintSolverTime = int.MaxValue, Timeout = int.MaxValue)]
public void DeterminePensionContribution([PexAssumeUnderTest]EmployeeServices target, Employee aEmployee)
{
PexAssume.IsNotNull(aEmployee);
PexAssume.IsTrue(aEmployee.BirthDate < new DateTime(1992, 1, 1) && aEmployee.BirthDate > new DateTime(1955, 1, 1));
PexAssume.IsTrue(aEmployee.StartDateContract < DateTime.Now && aEmployee.StartDateContract > new DateTime(1968, 1, 1));
PexAssume.IsTrue(aEmployee.BirthDate < aEmployee.StartDateContract);
PexAssume.IsTrue(aEmployee.Age > 18);
PexAssume.IsTrue(aEmployee.Salary > 1400 && aEmployee.Salary < 10000);
PexAssume.IsTrue(aEmployee.TypeContract == Contract.FullTime || aEmployee.TypeContract == Contract.PartTime);


PensionContribution result = target.DeterminePensionContribution(aEmployee);

PexValue.AddForValidation("BaseContribution", result.BaseContribution);
PexValue.AddForValidation("SupplementaryContribution", result.SupplementaryContribution);

if (aEmployee.Salary <= 3000)
Assert.IsTrue(result.BaseContribution == 80);
if (aEmployee.Salary > 3000)
Assert.IsTrue(result.BaseContribution == 135);
if (aEmployee.Salary <= 3000 && aEmployee.Age >= 45 && aEmployee.ServiceYears < 5)
Assert.IsTrue(result.SupplementaryContribution == 15);
if (aEmployee.Salary <= 3000 && aEmployee.Age < 45 && aEmployee.TypeContract == Contract.FullTime)
Assert.IsTrue(result.SupplementaryContribution == 15);
if (aEmployee.Salary <= 3000 && aEmployee.Age >= 45 && aEmployee.ServiceYears >= 5)
Assert.IsTrue(result.SupplementaryContribution == 25);
if (aEmployee.Salary > 3000 && aEmployee.Age < 45 && aEmployee.ServiceYears >= 5 && aEmployee.TypeContract == Contract.FullTime)
Assert.IsTrue(result.SupplementaryContribution == 25);
if (aEmployee.Salary > 3000 && aEmployee.Age < 45 && aEmployee.ServiceYears >= 5 && aEmployee.TypeContract == Contract.PartTime)
Assert.IsTrue(result.SupplementaryContribution == 20);
if (aEmployee.Salary > 3000 && aEmployee.Age >= 45 && aEmployee.ServiceYears >= 5 )
Assert.IsTrue(result.SupplementaryContribution == 20);
if (aEmployee.Salary > 3000 && aEmployee.ServiceYears < 5)
Assert.IsTrue(result.SupplementaryContribution == 30);
}

}


Running the Pex exploration several times (and after quite a while) I got results but no full coverage. Maybe the assumptions and/or asserts are not "pex-friendly"?



The generated unit tests only cover half of the code blocks.

public partial class EmployeeServicesTest
{

[TestMethod]
[PexGeneratedBy(typeof(EmployeeServicesTest))]
public void DeterminePensionContributionEmployeeServicesEmployee_20080806_110101_000()
{
PexValue.Generated.Clear();
Employee e0;
DateTime s0 = new DateTime(619794039740301312L);
DateTime s1 = new DateTime(631148677322244096L);
e0 = EmployeeFactory.Create(2, "", "", s0, s1, 8192u, Contract.FullTime);
EmployeeServices es0 = new EmployeeServices();
this.DeterminePensionContribution(es0, e0);
PexValue.Generated.Validate("BaseContribution", "135");
PexValue.Generated.Validate("SupplementaryContribution", "20");
}

[TestMethod]
[PexGeneratedBy(typeof(EmployeeServicesTest))]
public void DeterminePensionContributionEmployeeServicesEmployee_20080806_110101_001()
{
PexValue.Generated.Clear();
Employee e0;
DateTime s0 = new DateTime(619794039740301312L);
DateTime s1 = new DateTime(631148677322244096L);
e0 = EmployeeFactory.Create(2, "", "", s0, s1, 1983u, Contract.FullTime);
EmployeeServices es0 = new EmployeeServices();
this.DeterminePensionContribution(es0, e0);
PexValue.Generated.Validate("BaseContribution", "80");
PexValue.Generated.Validate("SupplementaryContribution", "25");
}

[TestMethod]
[PexGeneratedBy(typeof(EmployeeServicesTest))]
public void DeterminePensionContributionEmployeeServicesEmployee_20080806_110121_002()
{
PexValue.Generated.Clear();
Employee e0;
DateTime s0 = new DateTime(619794039740301312L);
DateTime s1 = new DateTime(631148677322244096L);
e0 = EmployeeFactory.Create(2, "", "", s0, s1, 8192u, Contract.PartTime);
EmployeeServices es0 = new EmployeeServices();
this.DeterminePensionContribution(es0, e0);
PexValue.Generated.Validate("BaseContribution", "135");
PexValue.Generated.Validate("SupplementaryContribution", "20");
}

[TestMethod]
[PexGeneratedBy(typeof(EmployeeServicesTest))]
public void DeterminePensionContributionEmployeeServicesEmployee_20080806_110200_003()
{
PexValue.Generated.Clear();
Employee e0;
DateTime s0 = new DateTime(626094836655304192L);
DateTime s1 = new DateTime(633479291125776385L);
e0 = EmployeeFactory.Create(2, "", "", s0, s1, 8192u, Contract.FullTime);
EmployeeServices es0 = new EmployeeServices();
this.DeterminePensionContribution(es0, e0);
PexValue.Generated.Validate("BaseContribution", "135");
PexValue.Generated.Validate("SupplementaryContribution", "30");
}

[TestMethod]
[PexGeneratedBy(typeof(EmployeeServicesTest))]
public void DeterminePensionContributionEmployeeServicesEmployee_20080806_110201_004()
{
PexValue.Generated.Clear();
Employee e0;
DateTime s0 = new DateTime(626094836655304192L);
DateTime s1 = new DateTime(633479291125776385L);
e0 = EmployeeFactory.Create(2, "", "", s0, s1, 1983u, Contract.FullTime);
EmployeeServices es0 = new EmployeeServices();
this.DeterminePensionContribution(es0, e0);
PexValue.Generated.Validate("BaseContribution", "80");
PexValue.Generated.Validate("SupplementaryContribution", "15");
}

[TestMethod]
[PexGeneratedBy(typeof(EmployeeServicesTest))]
public void DeterminePensionContributionEmployeeServicesEmployee_20080806_112335_000()
{
PexValue.Generated.Clear();
Employee e0;
DateTime s0 = new DateTime(609681269903603218L);
DateTime s1 = new DateTime(621044308151107616L);
e0 = EmployeeFactory.Create(2, "", "", s0, s1, 8192u, Contract.FullTime);
EmployeeServices es0 = new EmployeeServices();
this.DeterminePensionContribution(es0, e0);
PexValue.Generated.Validate("BaseContribution", "135");
PexValue.Generated.Validate("SupplementaryContribution", "20");
}

[TestMethod]
[PexGeneratedBy(typeof(EmployeeServicesTest))]
public void DeterminePensionContributionEmployeeServicesEmployee_20080806_112335_001()
{
PexValue.Generated.Clear();
Employee e0;
DateTime s0 = new DateTime(609681269903603218L);
DateTime s1 = new DateTime(621044308151107616L);
e0 = EmployeeFactory.Create(2, "", "", s0, s1, 1983u, Contract.FullTime);
EmployeeServices es0 = new EmployeeServices();
this.DeterminePensionContribution(es0, e0);
PexValue.Generated.Validate("BaseContribution", "80");
PexValue.Generated.Validate("SupplementaryContribution", "25");
}

[TestMethod]
[PexGeneratedBy(typeof(EmployeeServicesTest))]
public void DeterminePensionContributionEmployeeServicesEmployee_20080806_112422_002()
{
PexValue.Generated.Clear();
Employee e0;
DateTime s0 = new DateTime(609681269903603218L);
DateTime s1 = new DateTime(621044308151107616L);
e0 = EmployeeFactory.Create(2, "", "", s0, s1, 8192u, Contract.PartTime);
EmployeeServices es0 = new EmployeeServices();
this.DeterminePensionContribution(es0, e0);
PexValue.Generated.Validate("BaseContribution", "135");
PexValue.Generated.Validate("SupplementaryContribution", "20");
}

[TestMethod]
[PexGeneratedBy(typeof(EmployeeServicesTest))]
public void DeterminePensionContributionEmployeeServicesEmployee_20080806_115647_003()
{
PexValue.Generated.Clear();
Employee e0;
DateTime s0 = new DateTime(627392712227689792L);
DateTime s1 = new DateTime(633479779357556736L);
e0 = EmployeeFactory.Create(2, "", "", s0, s1, 8192u, Contract.FullTime);
EmployeeServices es0 = new EmployeeServices();
this.DeterminePensionContribution(es0, e0);
PexValue.Generated.Validate("BaseContribution", "135");
PexValue.Generated.Validate("SupplementaryContribution", "30");
}

[TestMethod]
[PexGeneratedBy(typeof(EmployeeServicesTest))]
public void DeterminePensionContributionEmployeeServicesEmployee_20080806_115718_004()
{
PexValue.Generated.Clear();
Employee e0;
DateTime s0 = new DateTime(627392712227689792L);
DateTime s1 = new DateTime(633479779357556736L);
e0 = EmployeeFactory.Create(2, "", "", s0, s1, 1983u, Contract.FullTime);
EmployeeServices es0 = new EmployeeServices();
this.DeterminePensionContribution(es0, e0);
PexValue.Generated.Validate("BaseContribution", "80");
PexValue.Generated.Validate("SupplementaryContribution", "15");
}

}





If you have any suggestions to improve (re-factor) the test code or the code-under- test (test-friendliness), don't hesitate to drop a line.

Thanks for reading.

Best regards,

Alexander

No comments: