Comp 110 Test-Driven Programming: Using Use and Edge Cases

Best Practices when Writing and Testing Functions

STEP 1: Write a "function definition skeleton," meaning:

  1. Declare the function with the correct name, parameter, and return type
  2. Return a "dummy" value to begin with
    1. Return type is number? Return 0.
    2. Return type is string? Return "?".
    3. Return type is boolean? Return false.
    4. Return type is an array?? Return [].

STEP 2: Create Use Cases

1. What are some usual input parameters?

- These are called use cases.They represent how the function will used most of the time.

2. What are some valid but unusual input parameters?

- These are your edge cases.They represent the weird uncommon cases that we still want to account for.

3. Given those input parameters, what is your expected return value for each set of inputs?

STEP 3: Implement the Logic for the Most Common Cases, run your test cases!

  • Keep editing your code until all of your common use cases pass

STEP 4: Create and Address Edge Cases, run your test cases again!

  • Which edge cases are failing? What can be added to the code to accommodate these cases?


Repeat steps 3 & 4 with more cases until you are confident your code works!

Another example of this process in action can be found in Test Functions.

Example:

Task: write a function repeatStuff that takes in a string str and a number n and returns a array of strings that repeats 'str' 'n' times. If n is negative, return "Oh Bo".

STEP 1: Write a "function definition skeleton" meaning

import { print } from "introcs";

let repeatStuff = (str: string, n: number): string => {

     return "?";
}

STEP 2: Create Use Cases

What are the most common use cases that one would want to use this function for? Add these to your main function, while making note of the "expected" values that should be returned.

import { testArray } from "./test-util";

export let main = async () => {
     testString("Bo string", "BoBoBoBo", repeatStuff("Bo", 4)); // we expect the result of our function call to be "BoBoBoBo"
     testString("MakeHappy string", "MakeHappyMakeHappy", repeatStuff("MakeHappy", 2));
     testString("Repeat 0 times", "", repeatStuff("what.", 0)); // -> "

// before you write your function logic, these tests will appear in the browser as failures
};

main();

STEP 3: Implement the Logic for the Most Common Cases

This will vary based on the problem; use the tools you have learned in class to handled the most common use case.

import { testArray } from "./test-util";

export let main = async () => {
    testArray("Bo string", ["Bo", "Bo", "Bo," "Bo"], repeatStuff("Bo", 4));
    testArray("MakeHappy string", ["MakeHappy", "MakeHappy"], repeatStuff("MakeHappy", 2));
};

let repeatStuff = (str: string, n: number): string[] => {
    let finalArray = [];
    for (let i = 0; i < n; i++) { // loop n times
        finalArray[finalArray.length] =  str;
        // finalArray starts as an empty array
        // for each iteration of the loop, str will be appended to the end of the array
    }
    return finalArray;
};

main();

STEP 4: Create and Address Edge Cases

When you are sure your use case tests are returning what is expected, in the main function, include the less common 'edge' cases whose logic will differ from the most common use case. Then, work to incorporate this logic into your function.

import { testArray } from "./test-util";

export let main = async () => {
    testArray("Bo string", ["Bo", "Bo", "Bo," "Bo"], repeatStuff("Bo", 4));
    testArray("MakeHappy string", ["MakeHappy", "MakeHappy"], repeatStuff("MakeHappy", 2));
    testArray("Repeat 0 times", [], repeatStuff("what.", 0));
    testArray("Repeat negative number", [], repeatStuff("#Deep", -1));
};

let repeatStuff = (str: string, n: number): string[] => {
    if (n < 0) { // this takes care of our edge case!
        return [];
    } else {
        let finalArray = [];
        for (let i = 0; i < n; i++) { 
            finalArray[finalArray.length] =  str;
        }
    return finalString; 
};

main();