Home > Articles

This chapter is from the book

This chapter is from the book

Behavioral Boundaries

A behavioral boundary is the conceptual boundary separating two (or more) related scenarios. In practice, it is often the boundary between two similar input values that give different results.

Let’s return to the volume dial example. Suppose that rather than having the dial’s API set the actual phone volume (as previously shown), we decide to take an event-driven approach instead. An observer of a Dial object will need to query the setting whenever the graphical dial is turned. What the observer wants to know is not which value was set on the dial, but what percent of the maximum volume the setting represents.

A developer might be tempted to aim for the “happy path” (the scenario that typically avoids all edge cases and error conditions), write a simple implementation (for example, return currentSettingFromZeroToTen * 10; ), and be done with it:

@Test
public void levelAsPercent_HappyPath_RightDownTheMiddle() {
    Dial dial = new Dial("Volume");
    dial.setTo(5);
    assertThat(dial.levelAsPercent()).isEqualTo(50);
}

This is a good start, but there is another way to approach this kind of problem, particularly when you are working with something more sophisticated than a volume dial.

TDD is first and foremost a practice that facilitates and supports a developer’s thinking and coding. Writing one test scenario often leads to an awareness of other related scenarios. Writing tests for those related scenarios is known as the “triangulation” technique; it is covered in detail in later chapters. In its most refined form, triangulation helps the developer identify behavioral boundaries by asking, “What value(s) will cause us to write smarter code?”

Let’s review what we know about Dial:

  • Negative numbers should throw an exception.

  • Numbers greater than 10 should throw the same exception with a different message.

  • Zero is effectively “off.”

  • All other values represent a percentage of the maximum volume.

There is a behavioral boundary between –1 (an error) and 0 (off), and another behavioral boundary between 10 (maximum) and 11 (another error). However, there are no behavioral boundaries between any two of the integers between 1 and 10. The behavior—the complete code path—is the same for all: 1, 2, 5, 6, 9, and 10 must all exhibit the same behavior (in this case, a simple calculation) resulting in different answers.

Because 0 (off) could also be represented by 0 percent, is there a behavioral boundary between a dial setting of 0 and 1? It’s impossible to say without learning more about the product specification. If we are asked to have the software turn on a little green light whenever the sound system is turned on, that would call for a behavioral boundary between 0 and 1 (though not necessarily implemented by Dial). Until that behavior is requested, however, the test-driven developer would not build it.

When you spot a behavioral boundary, you can write your tests for the values that represent the “edges” of that boundary:

@BeforeEach
public void initializeVolumeDial() {
    dial = new Dial("Volume");
}

@Test
public void dialThrowsExceptionForValueUnderMin() {
    assertThatIllegalArgumentException().isThrownBy(() -> {
        dial.setTo(-1);
    });
}

@Test
public void levelAsPercent_WhenOff() {
    dial.setTo(0);
    assertThat(dial.levelAsPercent()).isEqualTo(0);
}

@Test
public void levelAsPercent_AtMaximum() {
    dial.setTo(10);
    assertThat(dial.levelAsPercent()).isEqualTo(100);
}

@Test
public void dialThrowsExceptionForValueOverMax() {
    assertThatIllegalArgumentException().isThrownBy(() -> {
        dial.setTo(11);
    });
}

What if you hadn’t noticed the boundaries before writing your tests, and you had written that earlier happy path test for the dial setting of 5? You could either leave the original test within the suite or delete it. If the happy path test provides even the tiniest additional clarity in the specification, leave it. It would take perhaps one additional microsecond for each run. Alternatively, you could adjust the happy path test to be right at the edge of the boundary by changing the 5 to a 0 or a 10 and by changing the expected outcome to 0 or 100, respectively.

If you don’t test the edges of the behavioral boundaries, your team’s safety net will not be as clear and thorough as possible. For example, if we use 42 instead of 11 as the erroneous input greater than 10, and later another developer mistakenly increases the maximum value to 30, our test suite wouldn’t catch this problem. Our team might not learn about the defect until some unlucky consumer receives a very unpleasant (and very loud) surprise.

Because thinking test-driven requires human thought, mistakes will still happen, but far less frequently. When TDD is used diligently, every software defect is either a missing test or a vaguely specified test (that is, a misunderstanding). In either case, the mistake is a gap in the team’s safety net. It often takes just one more test to close the gap.

InformIT Promotional Mailings & Special Offers

I would like to receive exclusive offers and hear about products from InformIT and its family of brands. I can unsubscribe at any time.