Skip to main content

Avoid logic in unit tests

When you write unit tests, you should avoid logical structures such as conditions and loops. Adding unnecessary logic to your unit test increases the chances of introducing new bugs into your tests. When a unit test fails, it should be an indicator that something went wrong in production code and not in the unit test itself.

If you find yourself in a situation where it is unavoidable to add logic to a unit test, you can for example try to split the test into two or more unit tests.

In addition to the above, having simple, clear and readable unit tests is primordial, as discussed before.

Example

Consider the following class that calculates the Shipping fees. The calculateShippingFee method charges 10$ for the first 1 Kg, and then the additional weight will be multiplied by 5.

public class ShippingCalculator {

public static double BASE_FEE = 10.0;
public static double PRICE_BY_ADDITIONAL_KG = 5.0;

public static double calculateShippingFee(double weight){

if(weight <= 1.0){ // if less than 1 kg
return BASE_FEE;
} else {
return BASE_FEE + ( weight - 1.0 ) * PRICE_BY_ADDITIONAL_KG;
}
}

}

The following JUnit test contains too much logic for one test case. If it fails for some reason it will be a little confusing to find the exact cause the problem. In addition it does replicate production code.

@Test
void calculateShippingFee_Should_Return_The_Correct_Fee(){
double [] shippingWeights = { 0.5, 1.0 , 1.1 , 1.5, 10};
for(double weight:shippingWeights) {
if(weight <= 1.0){
double fee = ShippingCalculator.calculateShippingFee(weight);
assertEquals(ShippingCalculator.BASE_FEE, fee);
} else {
double fee = ShippingCalculator.calculateShippingFee(weight);
final double EXPECTED_FEE = ( weight - 1.0 ) *
ShippingCalculator.PRICE_BY_ADDITIONAL_KG;
assertEquals(EXPECTED_FEE, fee);
}
}
}

Improved example

It is always better to split a complicated unit tests into multiple smaller tests, each one is validating one specific behavior. The previous test was divided into three unit tests that do not contain any logic. Reading them gives a clear idea of the expected result for each situation.

@Test
void calculateShippingFee_Should_Return_Small_Box_Fee_When_Weight_Lower_Than_1Gk(){

final double WEIGHT = 0.5;
double fee = ShippingCalculator.calculateShippingFee(WEIGHT);

assertEquals(ShippingCalculator.BASE_FEE, fee);
}

@Test
void calculateShippingFee_Should_Return_Small_Box_Fee_When_Weight_is_1Gk(){

final double WEIGHT = 1.0;
double fee = ShippingCalculator.calculateShippingFee(WEIGHT);

assertEquals(ShippingCalculator.BASE_FEE, fee);
}

@Test
void calculateShippingFee_Should_Return_Correct_Fee_When_Weight_is_Greater_Than_1Gk(){

final double WEIGHT = 1.1;
final double EXPECTED_FEE = ShippingCalculator.BASE_FEE + (0.1 * ShippingCalculator.PRICE_BY_ADDITIONAL_KG);

double fee = ShippingCalculator.calculateShippingFee(WEIGHT);
assertEquals(EXPECTED_FEE, fee);
}