In this Pex test case I will investigate unit test on classes the have dependencies.( feel free to read the previous posts regarding this subject).
I'll first start with how you would unit test this scenario without the help of Pex and afterwards I'll investigate how Pex might help.
I have requirement to calculate the bonus a salesperson earns at the end of the year. The bonus depends on the sales he/she made this year and the year before.
The rules are
- The bonus amounts to 1% of the sales for that person
- If this year's sales is higher than 30000 € AND ALSO this year's sales are higher than the Sales of the previuos year for that salesperson, an extra bonus is added of 250 €
A quick test analysis should result in following test cases
currentYearSales greater or equal than 30000 € | Yes | Yes | No | No |
currentYearSales greater than LastYearSales | Yes | No | Yes | No |
Base bonus (1% currentYearSales ) | X | X | X | X |
Extra bonus (250€) | X | - | - | - |
So three test cases would be sufficient for complete test coverage (last two test cases can be collapsed).
Suppose you implements the rule in a bonus class and to know the sales amounts you have a Sales class that has appropriate methods to give you them. You've also encapsulated the retrival of these sales amounts inside the bonus class method that calculates the bonus.
public class Bonus
{
public double CalculateForEmployee(uint EmployeeId)
{
double baseBonusPercentage = 1;
double extraBonusAmount = 250;
uint minSalesForBonus = 30000;
Sales sales = new Sales();
uint currentYearSales = sales.CurrentYearSalesForEmployee(EmployeeId);
uint lastYearSales = sales.LastYearSalesForEmployee(EmployeeId);
double bonus = currentYearSales * (baseBonusPercentage / 100);
if (currentYearSales >= minSalesForBonus && currentYearSales > lastYearSales)
bonus += extraBonusAmount;
return System.Math.Floor(bonus);
}
}
public class Sales
{
public uint LastYearSalesForEmployee(uint EmployeeID)
{
//Could be a database call
}
public uint CurrentYearSalesForEmployee(uint EmployeeID)
{
//Could be a database call
}
}
Suppose you want to unit-test the method CalculateForEmployee in the Bonus class. The method calls into a Sales object to retrieve the sales for this year and the year before. Our code-under-test depends on another object to do its job. Nothing prevents you to write a standard unit test calling CalculateForEmployee.
[TestMethod()]
public void CalculateForEmployeeTest()
{
Bonus target = new Bonus();
uint EmployeeId = 0; // ?
double expected = 0; // ?
double actual;
actual = target.CalculateForEmployee(EmployeeId);
Assert.AreEqual(expected, actual);
}
But how do I know which EmployeeId to use and how do I know what the expected value for the bonus should be? In fact the method-under-test has dependencies we don't control in the unit test. The bonus class doesn't control the value of the sales. it's the responsibility of the sales class. As a matter a fact this situation isn't a pure unit test anymore. It's a unit integration test. So how can we control this dependency on the sales object and know in advance the EmployeeId and Sales amounts necessary for our method-under-test. Suppose a database is used to store sales information. We could setup a test database with all the necassary records for retrieving the sales for a particular sales employee. But this would still not classify our test as a unit test (only testing the rules for caculating the bonus). Also you must account for setting up three different test situation in the database.
Rewritting our classes a little bit could helps us. Interfaces, dependency injection and test doubles are techniques I will us to make the CalculateForEmployeeTest easier to test : knowing it advance what the sales amounts will be in order to assert the bonus.
First we extract an interface from our Sales class
public interface ISales
{
uint LastYearSalesForEmployee(uint EmployeeID);
uint CurrentYearSalesForEmployee(uint EmployeeID);
}
public class Sales : ISales
{
public uint LastYearSalesForEmployee(uint EmployeeID)
{
//Could be a database call
}
public uint CurrentYearSalesForEmployee(uint EmployeeID)
{
//Could be a database call
}
}
In this example I'll use "manual" dependency injection (constructor injection) to demostrate the dependency injection principle.
public class Bonus
{
ISales mSalesdao;
public Bonus(ISales aSalesDao)
{
mSalesdao = aSalesDao;
}
public double CalculateForEmployee(uint EmployeeID)
{
double baseBonusPercentage = 1;
double extraBonusAmount = 250;
uint minSalesForBonus = 30000;
double bonus;
uint currentYearSales = mSalesdao.CurrentYearSalesForEmployee(EmployeeID);
uint lastYearSales = mSalesdao.LastYearSalesForEmployee(EmployeeID);
bonus = currentYearSales * (baseBonusPercentage / 100);
if (currentYearSales >= minSalesForBonus && currentYearSales > lastYearSales)
bonus += extraBonusAmount;
return System.Math.Floor(bonus);
}
}
So how does this help me in my unit test you might wonder? A test-double is a test-specific class that we will use in our unit tests so the code-under-test will not notice it that much. There are several variations of test-doubles. For example a test stub is an equivalent of the dependend class but gives "fixed answers" if we ask for something or does something in a pre-determined way.
So instead of passing a real instance of the type Sales to the constructor of Bonus, I slip in a test stub when I write the unit tests. Because it gives fixed answers , I'll need a test stub for each test case.
public class SalesStub_CYSsmaller30000 : ISales
{
public uint LastYearSalesForEmployee(uint EmployeeID)
{
return 29999;
}
public uint CurrentYearSalesForEmployee(uint EmployeeID)
{
return 29998;
}
}
public class SalesStub_CYSgreater30000_CYSgreaterLYS : ISales
{
public uint LastYearSalesForEmployee(uint EmployeeID)
{
return 30001;
}
public uint CurrentYearSalesForEmployee(uint EmployeeID)
{
return 30002;
}
}
public class SalesStub_CYSgreater30000_CYSsmallerLYS : ISales
{
public uint LastYearSalesForEmployee(uint EmployeeID)
{
return 30003;
}
public uint CurrentYearSalesForEmployee(uint EmployeeID)
{
return 30002;
}
}
The unit tests use these stubs to do their thing. I create a teststub that is appropriate for the test case and inject it into the constructor of bonus.
[TestMethod()]
public void CalculateForEmployee_Lys29999_cs29998_withStub()
{
ISales aSalesDao = new SalesStub_CYSsmaller30000();
Bonus target = new Bonus(aSalesDao);
uint EmployeeID = 0; // Dummy EmployeeID not important for this example
double expected = 299; //
double actual;
actual = target.CalculateForEmployee(EmployeeID);
Assert.AreEqual(expected, actual);
}
[TestMethod()]
public void CalculateForEmployee_Lys30001_cs30002_WithStub()
{
ISales aSalesDao = new SalesStub_CYSgreater30000_CYSgreaterLYS();
Bonus target = new Bonus(aSalesDao);
uint EmployeeID = 0; // Dummy EmployeeID not important for this example
double expected = 550; //
double actual;
actual = target.CalculateForEmployee(EmployeeID);
Assert.AreEqual(expected, actual);
}
//and so
Another implementation for test doubles is the use of a Mock framework. This utility dynamically create test-equivalent objects that are setup (expectations) through the framework facilities . I'll illustrate this with the help of Rhino.Mock.
[TestMethod()]
public void CalculateForEmployee_Lys29999_cs29998_withMock()
{
uint EmployeeID = 0; // Dummy EmployeeID not important for this test purpose
Rhino.Mocks.MockRepository mr = new Rhino.Mocks.MockRepository();
ISales salesDao = mr.CreateMock();
Rhino.Mocks.Expect.Call(salesDao.LastYearSalesForEmployee(EmployeeID)).Return(29999);
Rhino.Mocks.Expect.Call(salesDao.CurrentYearSalesForEmployee(EmployeeID)).Return(29998);
mr.ReplayAll();
Bonus target = new Bonus(salesDao);
double expected = 299;
double actual;
actual = target.CalculateForEmployee(EmployeeID);
Assert.AreEqual(expected, actual);
mr.VerifyAll();
}
[TestMethod()]
public void CalculateForEmployee_Lys30001_cs30002_WithMock()
{
uint EmployeeID = 0; // Dummy EmployeeID not important for this test purpose
Rhino.Mocks.MockRepository mr = new Rhino.Mocks.MockRepository();
ISales salesDao = mr.CreateMock();
Rhino.Mocks.Expect.Call(salesDao.LastYearSalesForEmployee(EmployeeID)).Return(30001);
Rhino.Mocks.Expect.Call(salesDao.CurrentYearSalesForEmployee(EmployeeID)).Return(30002);
mr.ReplayAll();
Bonus target = new Bonus(salesDao);
double expected = 550; //
double actual;
actual = target.CalculateForEmployee(EmployeeID);
Assert.AreEqual(expected, actual);
mr.VerifyAll();
}
//and so on
The use of a Mock framework doesn't force me to explicitly write numerous of test stubs.
So where does Pex come in? For Pex to do its job I'll need a parametrized unit test. This is a unit test that will take arguments. Consider it as a test refactoring thing. Things that are common in the unit test are extracted and put into separate method/function. Pex needs this setup because it will be in charge with generating "traditional" unit tests with test data values that it will determine during its exploration phase. These generated unit test will call the parametrized unit test , passing the necessary arguments.
In our "mock" example we explicitly setup the mock for each unit test. In analogy with the parametrized unit test we need a parametrized Mock object (see Pex Tutorial).
[TestClass]
[PexClass]
public partial class BonusTest
{
[PexMethod]
[PexUseType(typeof(SalesPexMock))]
public void Calculate(ISales aSalesPexMock)
{
PexAssume.IsNotNull(aSalesPexMock);
uint EmployeeID = 9999; //dummy , not important for our test
Bonus target = new Bonus(aSalesPexMock);
double result = target.CalculateForEmployee(EmployeeID);
PexValue.AddForValidation("result", result);
}
}
[PexMock]
public class SalesPexMock : ISales
{
public uint LastYearSalesForEmployee(uint aEmployeeID)
{
var call = PexOracle.Call(this);
uint lys = (uint)call.ChooseFrom("lastYearSales", new uint[] { 29999, 30000, 30001 });
PexValue.Add("lastYearSales", lys);
return lys;
}
public uint CurrentYearSalesForEmployee(uint EmployeeID)
{
var call = PexOracle.Call(this);
uint lys = (uint)call.ChooseFrom("CurrentYearSales", new uint[] {29999, 30000, 30001});
PexValue.Add("CurrentYearSales", lys);
return lys;
}
}
Running the PexExplorartion on the PexMethod will result in three unit test being generated.
[TestMethod]
[PexGeneratedBy(typeof(BonusTest))]
public void Calculate01()
{
PexValue.Generated.Clear();
IPexOracleRecorder oracle = PexOracle.NewTest();
((IPexOracleSessionBuilder)
(oracle.OnCall(0, "SalesPexMock.CurrentYearSalesForEmployee(UInt32)")))
.ChooseAt(0, "CurrentYearSales", (object)30000u);
((IPexOracleSessionBuilder)
(oracle.OnCall(1, "SalesPexMock.LastYearSalesForEmployee(UInt32)")))
.ChooseAt(0, "lastYearSales", (object)30000u);
SalesPexMock salesPexMock = new SalesPexMock();
this.Calculate((ISales)salesPexMock);
global::Microsoft.Pex.Framework.PexValue.Generated.Validate("result", "300");
}
[TestMethod]
[PexGeneratedBy(typeof(BonusTest))]
public void Calculate02()
{
PexValue.Generated.Clear();
IPexOracleRecorder oracle = PexOracle.NewTest();
((IPexOracleSessionBuilder)
(oracle.OnCall(0, "SalesPexMock.CurrentYearSalesForEmployee(UInt32)")))
.ChooseAt(0, "CurrentYearSales", (object)29999u);
((IPexOracleSessionBuilder)
(oracle.OnCall(1, "SalesPexMock.LastYearSalesForEmployee(UInt32)")))
.ChooseAt(0, "lastYearSales", (object)30000u);
SalesPexMock salesPexMock = new SalesPexMock();
this.Calculate((ISales)salesPexMock);
global::Microsoft.Pex.Framework.PexValue.Generated.Validate("result", "299");
}
[TestMethod]
[PexGeneratedBy(typeof(BonusTest))]
public void Calculate03()
{
PexValue.Generated.Clear();
IPexOracleRecorder oracle = PexOracle.NewTest();
((IPexOracleSessionBuilder)
(oracle.OnCall(0, "SalesPexMock.CurrentYearSalesForEmployee(UInt32)")))
.ChooseAt(0, "CurrentYearSales", (object)30001u);
((IPexOracleSessionBuilder)
(oracle.OnCall(1, "SalesPexMock.LastYearSalesForEmployee(UInt32)")))
.ChooseAt(0, "lastYearSales", (object)29999u);
SalesPexMock salesPexMock = new SalesPexMock();
this.Calculate((ISales)salesPexMock);
global::Microsoft.Pex.Framework.PexValue.Generated.Validate("result", "550");
}
Of course meanwhile you verified the test coverage results :-) in the Pex report or via the Visual Studio Test Coverage facility.
So with the PexMock framework expectations for the indivudual unit test are setup , just like we did in our Rhino.Mock example. Of we had to help the Pex mock framework a little bit just like we did with the parametrized unit test.
Thanks for reading. Of course your comments are most welcome.
Best regards,
Alexander
ps: We already had complaints of Salespersons that find this bonus rule not fair. for example sales-persons that are above the minimum sales amount eligable for extra bonus but are not progressing , don't get it. Like wise someone who is progressing doesn't receive extra incentives.... :-)