I frequently work with very numeric / mathematical programs, where the exact result of a function is difficult to predict in advance.
In trying to apply TDD with this kind of code, I often find writing the code under test significantly easier than writing unit tests for that code, because the only way I know to find the expected result is to apply the algorithm itself (whether in my head, on paper, or by the computer). This feels wrong, because I am effectively using the code under test to verify my unit tests, instead of the other way around.
Are there known techniques for writing unit tests and applying TDD when the result of the code under test is difficult to predict?
A (real) example of code with difficult to predict results:
A function weightedTasksOnTime
that, given an amount of work done per day workPerDay
in range (0, 24], the current time initialTime
> 0, and a list of tasks taskArray
; each with a time to complete property time
> 0, due date due
, and importance value importance
; returns a normalized value in range [0, 1] representing the importance of tasks that can be completed before their due
date if each task if completed in the order given by taskArray
, starting at initialTime
.
The algorithm to implement this function is relatively straightforward: iterate over tasks in taskArray
. For each task, add time
to initialTime
. If the new time < due
, add importance
to an accumulator. Time is adjusted by inverse workPerDay. Before returning the accumulator, divide by sum of task importances to normalize.
function weightedTasksOnTime(workPerDay, initialTime, taskArray) {
let simulatedTime = initialTime
let accumulator = 0;
for (task in taskArray) {
simulatedTime += task.time * (24 / workPerDay)
if (simulatedTime < task.due) {
accumulator += task.importance
}
}
return accumulator / totalImportance(taskArray)
}
I believe the above problem can be simplified, while maintaining its core, by removing workPerDay
and the normalization requirement, to give:
function weightedTasksOnTime(initialTime, taskArray) {
let simulatedTime = initialTime
let accumulator = 0;
for (task in taskArray) {
simulatedTime += task.time
if (simulatedTime < task.due) {
accumulator += task.importance
}
}
return accumulator
}
This question addresses situations where the code under test is not a re-implementation of an existing algorithm. If code is a re-implementation, it intrinsically has easy to predict results, because existing trusted implementations of the algorithm act as a natural test oracle.