Assignment Chef icon Assignment Chef
All English tutorials

Programming lesson

White-Box Testing in Java: Mastering Branch and Condition Coverage with JUnit

Learn white-box testing techniques for Java methods using JUnit. This tutorial covers branch coverage, condition coverage, and MC/DC with real code examples from CS6300 Assignment 6, plus a timely AI analogy.

white-box testing Java white-box testing branch coverage condition coverage MC/DC coverage JUnit testing CS6300 assignment 6 defectMethod4 defectMethod5 structural testing Java code coverage AI model validation software testing tutorial JUnit 5 examples test case design division by zero testing

Introduction to White-Box Testing

White-box testing, also known as structural testing, is a software testing method where the internal structure, design, and implementation of the code are known to the tester. Unlike black-box testing, which focuses solely on inputs and outputs, white-box testing requires you to examine the code's logic and ensure that every path, branch, and condition is exercised. This approach is critical for achieving high coverage and detecting hidden defects.

In this tutorial, we'll explore white-box testing through the lens of a typical assignment from CS6300, focusing on methods like defectMethod4 and defectMethod5. You'll learn how to design JUnit test cases that achieve branch coverage, condition coverage, and Modified Condition/Decision Coverage (MC/DC). We'll also connect these concepts to current trends, such as AI model validation, where ensuring every logical branch is tested is as crucial as testing every neuron in a neural network.

Understanding the Code Under Test

Consider the following Java method, which is similar to those in the assignment:

public static int defectMethod4(boolean a, boolean b, int c, int d, int e) {
    int result = 0;
    if (a == b) {
        result = 1;
    } else {
        if ((c == 0) && ((d > 0) || (e < 0))) {
            result = 2;
        } else {
            result = 3;
        }
    }
    return result;
}

This method has nested conditionals and compound boolean expressions. To test it thoroughly, we need to cover all branches and conditions. The method returns 1 when a == b; otherwise, it checks the condition (c == 0) && ((d > 0) || (e < 0)). If true, returns 2; else returns 3.

Branch Coverage

Branch coverage ensures that each possible branch (true/false) of every decision point is executed at least once. For defectMethod4, the decision points are:

  • if (a == b) (true and false branches)
  • if ((c == 0) && ((d > 0) || (e < 0))) (true and false branches)

To achieve 100% branch coverage, we need at least two test cases: one where a == b is true (covers the true branch of the outer if), and one where it is false and the inner condition is both true and false. However, the inner condition's false branch can be covered by either making c != 0 or making (d > 0) || (e < 0) false. A minimal set might be:

  • Test 1: a=true, b=true (so a==b true) → result=1
  • Test 2: a=true, b=false, c=0, d=1, e=1 (inner condition true) → result=2
  • Test 3: a=true, b=false, c=1, d=0, e=0 (inner condition false) → result=3

This covers all branches.

Condition Coverage

Condition coverage requires that each atomic condition in a decision takes both true and false values. For the inner condition, the atomic conditions are: c == 0, d > 0, and e < 0. Note that the outer decision a == b is also an atomic condition. To achieve condition coverage, we need tests that make each atomic condition true and false at least once. For example:

  • Test A: a=true, b=true (a==b true)
  • Test B: a=true, b=false (a==b false), c=0, d=1, e=1 (c==0 true, d>0 true, e<0 false)
  • Test C: a=true, b=false, c=1, d=0, e=-1 (c==0 false, d>0 false, e<0 true)

Here, c==0 is true in Test B and false in Test C; d>0 is true in B and false in C; e<0 is false in B and true in C. Condition coverage is satisfied.

MC/DC Coverage

Modified Condition/Decision Coverage (MC/DC) is a stricter criterion that requires each condition to independently affect the outcome of the decision. For the inner decision (c == 0) && ((d > 0) || (e < 0)), we need to show that each condition can flip the decision while the others are held constant. For example, to show c == 0 independently affects the outcome, we need two test cases where c == 0 differs and the other conditions are fixed such that the decision changes. One typical set is:

  • Case 1: c=0, d=1, e=1 → decision true
  • Case 2: c=1, d=1, e=1 → decision false (c changed, d and e same)
  • Case 3: c=0, d=0, e=1 → decision false (to show d>0 independence? Actually d>0 is false here, but we need to show d>0 independence: keep c==0 true, e<0 false, vary d>0)

MC/DC is often required in safety-critical systems like avionics or medical devices. In the context of AI, ensuring that every condition in a decision tree is independently tested is similar to validating each feature's contribution in a model.

Writing JUnit Tests for White-Box Coverage

Now, let's write JUnit 5 test classes. Assume we have a class DefectClass containing defectMethod4. We'll create two test classes: one for branch coverage and one for condition coverage.

Test Class 1: Branch Coverage

import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;

public class DefectMethod4BranchTest {
    @Test
    public void testBranchAEqualsBTrue() {
        assertEquals(1, DefectClass.defectMethod4(true, true, 0, 0, 0));
    }

    @Test
    public void testBranchInnerTrue() {
        assertEquals(2, DefectClass.defectMethod4(true, false, 0, 1, 1));
    }

    @Test
    public void testBranchInnerFalse() {
        assertEquals(3, DefectClass.defectMethod4(true, false, 1, 0, 0));
    }
}

Test Class 2: Condition Coverage

import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;

public class DefectMethod4ConditionTest {
    // a==b true
    @Test
    public void testAEqualsBTrue() {
        assertEquals(1, DefectClass.defectMethod4(true, true, 0, 0, 0));
    }

    // a==b false, c==0 true, d>0 true, e<0 false
    @Test
    public void testConditionCTrueDTrueEFalse() {
        assertEquals(2, DefectClass.defectMethod4(true, false, 0, 1, 1));
    }

    // a==b false, c==0 false, d>0 false, e<0 true
    @Test
    public void testConditionCFalseDFalseETrue() {
        assertEquals(3, DefectClass.defectMethod4(true, false, 1, 0, -1));
    }
}

These tests achieve condition coverage for the atomic conditions in the method.

Testing defectMethod5: Handling Division by Zero

The second method, defectMethod5, returns a boolean and involves integer division. The code is:

public static boolean defectMethod5(boolean a, boolean b) {
    int x = 3;
    int y = 1;
    if(a) {
        x += y;
    } else {
        y = y * x;
    }
    if(b) {
        y -= x;
    } else {
        y -= 1;
    }
    return ((x / y) >= 0);
}

Here, division by zero can occur if y becomes 0. For example, if a is false, y = y * x = 1*3 = 3; then if b is true, y = y - x = 3-3 = 0, causing division by zero. White-box testing must identify such paths and ensure they are tested. We need to cover all branches and also check for exceptions.

Branch Coverage for defectMethod5

Branches: if(a) true/false, if(b) true/false. Four combinations. But we also need to avoid division by zero or handle it. A test that triggers division by zero should be written to expect an ArithmeticException.

import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;

public class DefectMethod5Test {
    @Test
    public void testATrueBTrue() {
        // a=true: x=4, y=1; b=true: y=1-4=-3; x/y=4/-3 <0 → false
        assertFalse(DefectClass.defectMethod5(true, true));
    }

    @Test
    public void testATrueBFalse() {
        // a=true: x=4, y=1; b=false: y=1-1=0; x/y division by zero
        assertThrows(ArithmeticException.class, () -> DefectClass.defectMethod5(true, false));
    }

    @Test
    public void testAFalseBTrue() {
        // a=false: y=1*3=3; b=true: y=3-3=0; division by zero
        assertThrows(ArithmeticException.class, () -> DefectClass.defectMethod5(false, true));
    }

    @Test
    public void testAFalseBFalse() {
        // a=false: y=3; b=false: y=3-1=2; x=3; x/y=3/2 >=0 → true
        assertTrue(DefectClass.defectMethod5(false, false));
    }
}

This achieves full branch coverage and also tests the exceptional cases.

Connecting to Trends: AI and Validation

White-box testing is not just for academic assignments. In the world of AI, testing the logic of a decision tree or a rule-based system is essential. For instance, consider a recommendation algorithm used by a popular app like TikTok. The algorithm has many conditional branches based on user interactions. Testing each branch ensures that the app behaves correctly for different user profiles. Similarly, in autonomous vehicles, MC/DC is used to validate that every sensor condition independently affects the braking decision. By mastering white-box testing now, you're building skills that are directly applicable to cutting-edge technology.

Best Practices and Common Pitfalls

  • Don't rely solely on coverage tools: Tools like JaCoCo can measure coverage, but they don't guarantee that tests are meaningful. Always design tests to exercise specific logic.
  • Avoid redundant tests: If two test cases cover the same branch, you may not need both. Focus on unique paths.
  • Test for exceptions: When code can throw exceptions (like division by zero), write tests that expect them.
  • Use descriptive test names: Names like testATrueBTrue help document what the test does.

Conclusion

White-box testing is a powerful technique for ensuring code quality. By mastering branch, condition, and MC/DC coverage, you can write tests that uncover hidden defects. The examples from CS6300 Assignment 6 provide a practical foundation. As you apply these concepts to real-world projects—whether it's a school assignment, a financial app, or an AI system—you'll find that thorough testing saves time and prevents failures. Start with the methods above, experiment with your own code, and remember: every branch matters.