Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Should a unit test rely on constants defined by the application?

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?

like image 998
Matthew Avatar asked Sep 10 '12 16:09

Matthew


2 Answers

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.

like image 177
darch Avatar answered Sep 29 '22 21:09

darch


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.

like image 41
Robbie Dee Avatar answered Sep 29 '22 19:09

Robbie Dee