1 of 27

CSE 331�Software Design & Implementation

Spring 2026

Section 2 – Testing and Immutable Reasoning

2 of 27

Administrivia

  • HW1 will be released tonight and is due next Wednesday (4/15) @ 11:59pm!

2

3 of 27

Review - Testing Heuristics

≥ 2 Tests

Statement Coverage

  • Test every executable statement reachable by an allowed input

Branch Coverage

  • For every conditional, test all branches for allowed inputs

Loop Coverage

  • Every loop/recursive call must be tested on 0, 1, any 2+ iterations for allowed inputs

Exhaustive Testing

  • Test all possible inputs for functions with <= 10 allowed inputs

Notes on Testing Requirements

4 of 27

Review: JUnit Testing

  • What is JUnit?
    • JUnit is a framework that lets us easily develop tests in Java!
  • JUnit Testing Structure
    • JUnit tests are split into multiple methods, each with the @Test annotation.
    • There are other annotations like @Before, @BeforeEach, and more to reduce redundancy
    • Each test method is made up of assertions that compare the expected behavior of the program to the actual behavior

5 of 27

Review: JUnit Testing

Method Name

Method Description

assertTrue / assertFalse

Takes in a boolean and asserts that it is true / false

assertEquals / assertNotEquals

Takes in two objects and asserts that they are equal (using the .equals() method) / not equal

assertNull / assertNotNull

Takes in an object and asserts that it is null / not null

assertThrows / assertNotThrows

Given a method, assert that it will throw a certain exception / won’t throw one (Note: this requires a lambda to work)

For more information, check out the official documentation! https://docs.junit.org/6.0.3/overview.html

6 of 27

Task 1 - Bop it! Twist it! Pull it! Test it!

For each of the following, consider the method implementation and determine whether the given tests satisfy our testing heuristics:

/*

* Returns the amount of money spent rounded up to the nearest 5.

* @param amount The amount spent

* @requires 0 <= amount <= 9

* @return 0 if amount = 0, 5 if 0 < amount <= 5, and 10 otherwise.

*/

public static int amountSpent(int amount) {

if (amount == 0) {

return 0;

} else if (amount <= 5) {

return 5;

} else {

return 10;

}

}

7 of 27

Task 1 - Bop it! Twist it! Pull it! Test it!

// Given tests:

@Test

void testAmountSpent() {

assertEquals(0, amountSpent(0), "0 does not round");

assertEquals(5, amountSpent(4), "4 should round up to 5");

assertEquals(10, amountSpent(9), "9 should round up to 10");

}

8 of 27

Task 1 - Bop it! Twist it! Pull it! Test it!

// Given tests:

@Test

void testAmountSpent() {

assertEquals(0, amountSpent(0), "0 does not round");

assertEquals(5, amountSpent(4), "4 should round up to 5");

assertEquals(10, amountSpent(9), "9 should round up to 10");

}

They do not! While branch and statement coverage are achieved, since there are only 10 possible values, we have to test all 10!

9 of 27

Task 1 - Bop it! Twist it! Pull it! Test it!

/*

* Attempts to purchase an item with price itemPrice, and returns

* the remaining money. If the price is over 100 or the price is

* higher than the available money, returns the available money.

* Otherwise, returns the difference between the two.

* @param availableMoney The amount of money available

* @param itemPrice The price of the item

* @requires availableMoney >= 0 && itemPrice > 0

* @return The remaining money after the purchase, or

* all the available money if the purchase cannot be made.

*/

public static int buyItem(int availableMoney, int itemPrice) {

if (itemPrice > 100 || availableMoney < itemPrice) {

return availableMoney;

} else {

return availableMoney - itemPrice;

}

}

10 of 27

Task 1 - Bop it! Twist it! Pull it! Test it!

// Given tests:

@Test

void testBuyItem() {

assertEquals(102, buyItem(102, 101), "Should not buy items > 100 in cost.");

assertEquals(0, buyItem(1, 1), "Should buy item and subtract cost, resulting in 0.");

assertEquals(1, buyItem(2, 1), "Should buy item and subtract cost, resulting in 1.");

assertEquals(1, buyItem(1, 2), "Should not buy item with cost > availableMoney.");

}

11 of 27

Task 1 - Bop it! Twist it! Pull it! Test it!

// Given tests:

@Test

void testBuyItem() {

assertEquals(102, buyItem(102, 101), "Should not buy items > 100 in cost.");

assertEquals(0, buyItem(1, 1), "Should buy item and subtract cost, resulting in 0.");

assertEquals(1, buyItem(2, 1), "Should buy item and subtract cost, resulting in 1.");

assertEquals(1, buyItem(1, 2), "Should not buy item with cost > availableMoney.");

}

These tests are sufficient, since all statements and branches are covered.

12 of 27

Task 2 - East or Test, JUnit is the Best!

For each of the following, consider the following method specification and implementation. Then, write JUnit tests that fully cover our testing heuristics:

b) /**

* Returns the sum of the array

* @param arr an array of integers

* @requires arr is not null

* @returns the sum of the elements

* in arr or 0 if arr is empty

*/

public static int arraySum(int[] arr) {

if (arr == null) {

throw new IllegalArgumentException("arr cannot be null");

}

int sum = 0;

for (int i = 0; i < arr.length; i++) {

sum += arr[i];

}

return sum;

}

13 of 27

Task 2 - East or Test, JUnit is the Best!

/**

* Returns the sum of the array

* @param arr an array of integers

* @requires arr is not null

* @returns the sum of the elements

* in arr or 0 if arr is empty

*/

public static int arraySum(int[] arr) {

if (arr == null) {

throw new IllegalArgumentException("");

}

int sum = 0;

for (int i = 0; i < arr.length; i++) {

sum += arr[i];

}

return sum;

}

@Test

public void testArraySum() {

int[] arr1 = new int[0];

// 0 loop iterations

assertEquals(0, arraySum(arr1));

// 1 loop iteration

int[] arr2 = {5};

assertEquals(5, arraySum(arr2));

// 2+ loop iterations

int[] arr2 = {0, 3, 3, 1, 0};

assertEquals(7, arraySum(arr3));

}

Statement and Branch coverage are covered by tests 1 & 2. We don't need to test the first if statement as it is a disallowed input.

14 of 27

Reasoning with Immutable Code

  • Start with precondition
    • The precondition tells us what facts must be true at the very beginning of the method’s execution
  • Collecting facts
    • As we go through the code line-by-line, we must collect all known facts until we reach the return statement(s)
  • Implying the postcondition
    • Collected facts should imply the post condition to show the method implementation is correct

15 of 27

Reasoning With Immutable Code Example

/**

* Quadruples the value of x

* @requires x > 0

* @return a positive integer y

*/

public static int quadruple(int x) {

int y = 4 * x;

return y;

}

16 of 27

Reasoning With Immutable Code Example

  • At the return statement, we know these facts:
    • x > 0 and y = 4x
  • The desired postcondition is:
    • y > 0
  • As we can see, the known facts imply the postcondition since:

y = 4x

> 4(0)

= 0

  • Therefore, our code satisfies the postcondition

/**

* Quadruples the value of x

* @requires x > 0

* @return a positive integer y

*/

public static int quadruple(int x) {

int y = 4 * x;

return y;

}

17 of 27

Reasoning with Conditionals

  • When we have multiple branches, we must collect the facts at the end of every path!
  • Each of these paths should be consistent with the postcondition
  • Going into each conditional, you know everything before it and either
    • The condition in: if (condition) or
    • The negation of earlier branches and condition in: else if (condition) or
    • The negation of all earlier branches for: else
  • After all conditionals, you know the facts from the end of each branch
    • The postcondition should be true for each set of facts and combine to complete the postcondition

18 of 27

Reasoning with If Statements Example

/** Returns the absolute value of a given number x

* @return the integer y such that y = x if x >= 0 or y = -x

* if x < 0

*/

public static int abs(int x) {

if (x < 0) {

int y = -x;

return y; // Return #1

} else {

int y = x;

return y; // Return #2

}

}

19 of 27

Reasoning with If Statements Example

public static int abs(int x) {

if (x < 0) {

int y = -x;

return y; // Return #1

} else {

int y = x;

return y; // Return #2

}

}

  • At Return #1, we know these facts:
    • x < 0 and y = -x
  • At Return #2, we know these facts:
    • x >= 0 and y = x
  • The desired postcondition is:
    • (y = x and x >= 0) OR �(y = -x and x < 0)
  • In the first path, we can see the postcondition holds.
  • In the second path, we can see the postcondition hold.
  • Since these paths are exhaustive, our code satisfies the postcondition

20 of 27

Task 3 - Just Give Me A(mmutable) Reason(ing)

For each of the following methods, collect facts to show that the code is correct according to the specification (i.e., assuming the precondition, the code fulfills the postcondition).

Consider the following Java method:

/*

* Performs a magic math trick on a starting number.

* @param x The starting number

* @requires x > 0

* @return x

*/

public static int magicNumber(int x) {

int a = x * 2;

int b = a + 4;

int c = b / 2;

int d = c - 2;

return d;

}

Collect all facts from the method. Do they prove that the method is correct?

21 of 27

Task 3 - Just Give Me A(mmutable) Reason(ing)

For each of the following methods, collect facts to show that the code is correct according to the specification (i.e., assuming the precondition, the code fulfills the postcondition).

Consider the following Java method:

/*

* Performs a magic math trick on a starting number.

* @param x The starting number

* @requires x > 0

* @return x

*/

public static int magicNumber(int x) {

int a = x * 2;

int b = a + 4;

int c = b / 2;

int d = c - 2;

return d;

}

Collect all facts from the method. Do they prove that the method is correct?

d = c - 2 and c = b / 2 and b = a + 4 and a = x * 2 and x > 0.

22 of 27

Task 3 - Just Give Me A(mmutable) Reason(ing)

Collect all facts from the method. Do they prove that the method is correct?

d = c - 2 and c = b / 2 and b = a + 4 and a = x * 2 and x > 0.

Yes! To prove the method is correct, we can use these facts to find the true value of d

d = c - 2

= (b / 2) - 2

= ((a + 4) / 2) - 2

= (((x * 2) + 4) / 2) - 2

= (x + 2) - 2

= x

Since d = x follows from the collected facts, the method returning d satisfies the @return specification.

23 of 27

Task 3 - Just Give Me A(mmutable) Reason(ing)

Collect all facts from the method. Do they prove that the method is correct?

/*

* Returns the amount of money spent rounded up to the nearest 5

* @param amount The amount spent

* @requires 0 <= amount <= 9

* @return 0 if amount = 0, 5 if 0 < amount <= 5, and 10 otherwise.

*/

public static int amountSpent(int amount) {

if (amount == 0) {

return 0; // Return #1

} else if (amount <= 5) {

return 5; // Return #2

} else {

return 10; // Return #3

}

}

24 of 27

Task 3 - Just Give Me A(mmutable) Reason(ing)

Collect all facts from the method. Do they prove that the method is correct?

/*

* Returns the amount of money spent rounded up to the nearest 5

* @param amount The amount spent

* @requires 0 <= amount <= 9

* @return 0 if amount = 0, 5 if 0 < amount <= 5, and 10 otherwise.

*/

public static int amountSpent(int amount) {

if (amount == 0) {

return 0; // Return #1

} else if (amount <= 5) {

return 5; // Return #2

} else {

return 10; // Return #3

}

}

Return #1: amount = 0.

Return #2: amount != 0 and amount <= 5 and 0 <= amount <= 9.

Return #3: amount != 0 and 5 < amount and 0 <= amount <= 9.

25 of 27

Task 3 - Just Give Me A(mmutable) Reason(ing)

Collect all facts from the method. Do they prove that the method is correct?

Return #1: amount = 0.

Return #2: amount != 0 and amount <= 5 and 0 <= amount <= 9.

Return #3: amount != 0 and 5 < amount and 0 <= amount <= 9.

Yes! In the first branch, 0 rounds to 0.

In the second branch, amount must be between 1 and 5, and correctly returns 5.

In the third branch, amount must be between 6 and 9, and correctly returns 10.

These cases cover all allowed inputs and exhaustively cover the postcondition.

26 of 27

Task 3 - Just Give Me A(mmutable) Reason(ing)

Consider the following (buggy) Java method:

/*

* Returns the shipping fee based on the total cost of the order.

* @param cost The total cost of the order

* @requires 0 <= cost <= 100

* @return 0 if cost >= 50, 10 if 0 < cost < 50, and 0 if cost == 0.

*/

public static int shippingFee(int cost) {

if ((cost + 49) / 50 == 1) {

return 10; // Return #1

} else {

return 0; // Return #2

}

}

27 of 27

Task 3 - Just Give Me A(mmutable) Reason(ing)

Consider the following (buggy) Java method:

/*

* Returns the shipping fee based on the total cost of the order.

* @param cost The total cost of the order

* @requires 0 <= cost <= 100

* @return 0 if cost >= 50, 10 if 0 < cost < 50, and 0 if cost == 0.

*/

public static int shippingFee(int cost) {

if ((cost + 49) / 50 == 1) {

return 10; // Return #1

} else {

return 0; // Return #2

}

}

Return #1: (cost + 49) / 50 = 1 and 0 <= cost <= 100.

Return #2: (cost + 49) / 50 != 1 and 0 <= cost <= 100.

No! The first branch incorrectly executes when the cost is exactly 50 (since 99 / 50 = 1), returning 10. The specification states that if cost >= 50, the method must return 0.