Matua Doc

Matua Doc

Testing

Learning intentions

In this lesson, you will revise:

  • The importance of testing in software development
  • How this relates to your assessment
  • What you should test
  • Expected, boundary, and invalid inputs
  • How to test using a testing table

What to test?

Ultimately you need to test everything that can change the outcome of the program At our current level, this just means testing the program’s:

  • Calculations
  • Handling of user input
  • Boolean expressions in conditions and loops
  • Functions and methods
  • Protocol compliance

Types of testing

When we test one aspect of the program we need to run a series of tests on it (not just one). These tests can be categorised into three types:

Three types of testing: expected, boundary, invalid

  • Expected: the values you expect a user to enter into the program.
  • Boundary: values at the boundaries of what is acceptable in the program.
  • Invalid: invalid data types or absence of data altogether.

Expected value testing

Expected cases are things that we reasonably expect the user would do with the program. This includes ensuring functions perform their tasks correctly and/or return values we expect.

For example, let’s talk about a program that checks whether or not the user is old enough to enter a bar. You need to be 18 or older to enter a bar, so a few expected values could include:

  • 19
  • 24
  • 39

The values don’t necessarily need to be correct, either. For example, if it were a quiz program, we might expect users to get some questions wrong Similarly, these values are what we might expect someone to enter:

  • 16
  • 12
  • 5

Expected value testing

For a non-numerical example, how about being asked for a name?

  • Jack
  • Rangimārie (macron)
  • Jean Luc (name with a space)
  • Annie-May (double barrelled)

What kind of name can be considered ‘expected’ depends on the target audience of the program — you might even need to include other writing systems!

Boundary value testing

Boundary cases are those that are just on the limit of what we might expect. Testing boundaries is useful to see that condition work as we want them to.

Boundaries can include:

  • numbers that must be provided to a function
  • numbers that a user must supply (within a range)
  • the length of strings supplied by a user (too short, too long)

These values exist along a spectrum — we are interested in the extremes!

Boundary value testing

The bounds are the values that define the lower and upper limit. The upper bound represents the highest value whilst the lower bound represents the lowest value acceptable by the program.

We also test the values immediately inside and outside the bounds. Values on the inside are safe (and can also be considered ‘expected’); values on the outside are too low/high.

Think about a program checking if you are old enough to receive a Working Holiday Visa, which is for people aged 18-35 from certain countries visiting New Zealand.

Outside Lower Inside … Inside Upper Outside
17 18 19 … 34 35 36

All six values need to be tested as evidence of boundary testing.

Invalid value testing

Invalid cases can be trickier again. There are three main types of invalid test cases when it comes to user input:

  • invalid data
  • wrong data type
  • null input (nil), especially where optional values are concerned

Invalid data is when the user provides some data that would result in an error. For example, if a user provides 0 as the right-side side of a mathematical division (i.e. 24/0), the code will fail. You would test that the code can handle the zero with an error message or asking for another number in its place.

Wrong data type is when a user might enter a data type that we are not expecting. For example, if you write code that expects a number but you provide a string instead, the program might crash. This is resolved by using type annotations and if let or guard let to safely cast types.

Null entry is simply where a user does not enter any data before continuing with the program if the program expects some data. An example: when a function expects a non-nil value but you force-unwrap it using ! — this should be fixed to safe unwrapping with if let or guard let. For another example, when a string is empty but should actually contain text.

When testing, you must…

  1. Read and understand the requirements of the code. What is it supposed to do? What are the inputs and expected outputs?
  2. Identify a sample of values that your code needs to handle
    • Consider boundary cases, normal cases, and exceptional cases
  3. Write test cases based on the identified scenarios. Include a mix of valid and invalid inputs to cover a range of possibilities
  4. Set up any necessary conditions for the test cases, such as initializing variables or creating objects.
  5. Execute the Function: call the function with the test inputs
  6. Capture the output or result of the function
  7. Compare the actual output with the expected output for each test case. If they match, the test case passes; otherwise, it fails.
  8. Keep a record of the test cases, their inputs, expected outputs, and whether they passed or failed.
  9. If a test case fails, debug the function to identify and fix the issue Re-run the failed test case to ensure it now passes without affecting other test cases
  10. Move on to the next test case.

Documenting your testing

Just like in 12SWE, you are expected to document your testing as you develop. As in 12SWE, you will use a testing table.

In the testing table, you will record:

  • 📛 The variable or function you are testing
  • 🙅 Whether you are testing an expected, boundary, or invalid case
  • 📥 What the input is (what a user types or function arguments)
  • 📤 What the expected program output is and the actual output
  • 🙆 Whether the program worked or did not, and notes on what to fix

Example table

Read through your code, then come up with some testing values before you start testing. You should be able to predict the expected values without having to run the code. Then, start testing until your program matches these expectations.

What to test Type Input/args Expected result
getUserName ✅ E Toby Welcome, Toby
getUserName ✅ E Anahera Welcome, Anahera
getUserName ✅ LB+1 Ax Welcome, Ax
getUserName ⚠️ LB E Welcome, E
getUserName ⛔️ LB-1 (no name) Invalid name. Please enter again.
getMenuOption ✅ E 1 (first option) Please enter the quantity of eggs to add:
getMenuOption ✅ E q (quit) Thank you for using egg shop.
getMenuOption ✅ E Q (quit) Thank you for using egg shop.
getMenuOption ✅ UB- 4 Showing all eggs in stock:
getMenuOption ⚠️ UB 5 (last option) Showing today’s sales:
getMenuOption ⛔️ UB+ 6 (no such option) Invalid option. Please select from 1 to 5, or Q.
divide ✅ E 1, 2 0.5
divide ✅ E 1.0, 5.5 0.18181818
divide ⛔️ I 1, 0 (division by 0) Unable to divide by zero. Please enter another number.

Task

1. The paragraph/slide below contains some code that contains some errors, which your testing will help detect.
2. The program asks for guests at a wedding, how much they contributed as a gift, and then ranks them on a tier list. The more money they give, the higher the tier.
3. Lowest tier is F, then D, then C, then B, then A, then top tier is S.
4. Use the testing table in Google Classroom to record your testing of the program. Note any errors and what you think went wrong, then fix the errors. Keep adding new lines showing testing of that part of the program until it is fixed.
5. For **Kaiaka/M** and **Kairangi/E**, you need to ensure boundaries are handled correctly (both top and bottom, where applicable, inside and out) and invalid values (such as `nil`) don't cause the program to crash.

Code to test and debug

import Foundation

let rankLabels = ["F Tier", "D Tier", "C Tier", "B Tier", "A Tier", "S Tier"]

func rankIndex(from amount: Double) -> Int {
    if amount >= 250 { return 0 } // Should be S
    if amount >= 100 { return 1 } // Should be A
    if amount >= 50  { return 2 }
    if amount >= 25  { return 3 }
    if amount >= 10  { return 4 }
    return 5 // Should be F
}

func addPreMadeGuests(to guestList: inout [[String]]) {
    // Pre-made guests to help you test.
    guestList.append(["Rich Richard", "500.0"])
    guestList.append(["Broke Bob", "2.0"])
}

func printTierList(_ guestList: [[String]]) {
    print("\n--- BROKEN PARTY TIER LIST ---")

    let sortedList = guestList.sorted { lhs, rhs in
        let lhsAmount = Double(lhs[1]) ?? 0
        let rhsAmount = Double(rhs[1]) ?? 0
        let lhsRank = rankIndex(from: lhsAmount)
        let rhsRank = rankIndex(from: rhsAmount)

        if lhsRank != rhsRank {
            return lhsRank > rhsRank
        }

        return lhs[0] > rhs[0]
    }

    for guest in sortedList {
        let amount = Double(guest[1]) ?? 0
        let rank = rankLabels[rankIndex(from: amount)]
        print("[\(rank)] \(guest[0]) | $\(guest[1])")
    }
}

var guestList: [[String]] = []
addPreMadeGuests(to: &guestList)
var isRunning = true

while isRunning {
    print("\nEnter Name (or 'done'): ", terminator: "")
    let nameInput = readLine()!

    if nameInput.lowercased() == "done" {
        isRunning = false
        break
    }

    print("Enter Amount: ", terminator: "")
    let amountInput = readLine()!
    let amount = Double(amountInput)!

    guestList.append([nameInput, String(amount)])
    print("Added \(nameInput).")
}

printTierList(guestList)