Matthew
Matthew

Reputation: 29206

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?

Upvotes: 7

Views: 465

Answers (3)

Robbie Dee
Robbie Dee

Reputation: 1977

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.

Upvotes: 1

darch
darch

Reputation: 4311

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.

Upvotes: 1

im so confused
im so confused

Reputation: 2111

As I have always tried to operate in the past: unit tests should check for all possible boundary conditions / stress test a specific function or subset of the whole's functionality, and that a larger regression suite should test the entire program for validity from the business requirement side.

Therefore, in your example, the first test would be in my unit tests, and the second in my regression.

I am a relative newcomer to the industry, however, and would love to know if I'm not on the right track.

Upvotes: 0

Related Questions