Consider the following pseudocode. It is designed to determine whether a grade is a passing grade.
class Student:
int grade
boolean IsStudentPassing():
return grade >= MIN_PASSING_GRADE
...
// In another file
constant int MIN_PASSING_GRADE = 70
If we were writing a unit test for IsStudentPassing
, we could use the constant value:
ensure that IsStudentPassing is false when grade is MIN_PASSING_GRADE - 1
ensure that IsStudentPassing is true when grade is MIN_PASSING_GRADE
Or, we could hand-pick the values:
ensure that IsStudentPassing is false when grade is 69
ensure that IsStudentPassing is true when grade is 70
For the second approach, our test must be re-written if MIN_PASSING_GRADE
changes. The first approach is more flexible, but relies on MIN_PASSING_GRADE
having a correct value.
I'm not entirely sure which approach to prefer, and general choose on a case-by-case basis. On the one hand, ensuring that MIN_PASSING_GRADE
is sane should be taken care of by a different test. On the other hand, I worry about a supposedly "unit" test touching too many other places in the code base.
This is a contrived example, but similar situations occur often in real programs. What is the best approach to address them?
By preference, you would inject the "constant" value with one of your own devising, such that your unit test is isolated from the vagaries of what, in fact, constitutes a passing grade. How easy this is to do varies by programming language. Consider this code for a language that makes it easy:
use MooseX::Declare;
class Student {
has grade => (
is => 'ro', isa => 'Num', required => 1,
);
method min_passing_grade {
return MIN_PASSING_GRADE;
)
method is_student_passing () {
return $self->grade >= $self->min_passing_grade
}
}
class t::Student {
use Test::Sweet;
use Test::MockObject::Extends;
test correctly_determines_student_is_passing () {
my $student = $self->_make_student($self->_passing_grade);
ok($student->is_student_passing);
}
method _make_student (Num $grade) {
my $student = Test::MockObject::Extends->new(
$student->new(grade => $grade)
);
# Here's the important line:
$student->set_always(
min_passing_grade => $self->_passing_grade
);
return $student;
}
method _passing_grade () { 40 }
test correctly_determines_student_is_failing () {
my $student = $self->_make_student($self->_passing_grade - 1);
ok(not $student->is_student_passing);
}
}
Now, that's Perl, which makes monkey patching pretty straightforward (the 'important line' above replaces the implementation of Student::min_passing_grade
at run time). You can also have the value in questino be an attribute that defaults to the constants or even provide a special version of the constants file for use by your unit test.
Absent a really strong performance imperative, I'm going to opt for the above in preference to having the value be a real constant. Only if I can't find a way to inject the value I want from my unit test will I reach for the commonly-defined constants. What I don't think you should do under pretty much any circumstances is duplicate the constant in this test, which is after all making sure the logic of Student
is correct.
The preference would be not to use a constant at all but make it a property of an interface that gets passed around. This is how dependency injection works (which in turn aids TDD).
You'd then use a test (or mocking) framework to generate the tests. N.B. you probably want to be testing more than a few values either side of the target value. You'll also want to test the bounds of the datatype to trap overflow/underflow errors.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With