Examples and tests play an important role in the lifecycle of programs.
You may think that the programs you develop in 111 are too small for these practices to matter. This is less true on later assignments and projects. More importantly, 111 is teaching you the practice of responsible programming. That includes developing robust programming habits. If you head out from 111 to help a professor elsewhere at Brown with their research, the accuracy and lifecycle of your work could impact the validity of their results. These habits matter.
The rest of this document describes our expectations and offers guidelines for writing examples and tests.
An example or test has two critical parts: the expression that you want to run and the answer that you expect that expression to produce.
double(10) is 20
double(15) is 15 * 2 # the answer can be an expression
Examples and tests should be actual code that can be run, not just notes in a comment. People ignore most notes in comments. For examples and tests to help us develop robust programs, they need to be run against our code.
Generally speaking, write examples before you write your code. This helps make sure you are clear in your own mind about what you are trying to write.
Write tests before, during, and after you write your code. Tests capture more nuance, deeper examples, and more fine details than examples. They'll occur to you at different times. Write them down as you go.
where
blocks)Image
and lam
functions (which we will learn later in the course).check
blocks)Edge cases are inputs at the boundaries within the space of inputs. For instance, a function that checks for "numbers between 4 and 8" or "strings with at least 3 characters" has boundaries.
If the problem has boundaries, test values both at and on either side of each boundaries. For example, if a function looks at values "greater than or equal to 5", you should have examples that use inputs such as 4.5, 5, and 6 (this would catch typos in your use of >
vs >=
).
As an example, let's consider a function compare
that takes two numerical inputs x
and y
and returns a string. It should return "way less"
if x
is smaller than y - 2
, "about the same"
if x
is between y - 2
and y + 2
(inclusive) and "way more"
if x
is greater than y + 2
. Here's a set of good examples for this function.
fun compare(x :: Number, y :: Number) -> String:
doc: "decribe the relationship between input numbers x and y"
... # function body elided
where:
compare(1, 40) is "way less"
compare(0, 3) is "way less"
compare(8, 10) is "about the same"
compare(1, 1) is "about the same"
compare(0, 1) is "about the same"
compare(17, 20) is "way more"
end
Notice that these examples cover each interesting "case" of the program. They also include edge cases testing the boundaries–for instance, compare(8, 10)
is an edge case, since it's at the boundary between the "way less"
and "about the same"
cases.
Consider two pieces of information that you might represent with a string: names and passwords. Names might have spaces but likely not symbols like #
, while passwords won't have spaces but often have symbols. When coming up with inputs to test, think about the information your input represents and what might be meaningful.
For example, if you are writing a program that processes passwords, your input might have numbers, letters (lower and uppercase) and symbols. The symbols could be anywhere in the string (beginning, middle, end). There's likely a minimum length and a practical maximum. All of these scenarios should show up in your examples and/or tests.
Some types of data have structure to their variations. A list can be empty or non-empty. A traffic light color can be red, yellow, or green. When your data has structural variation (as opposed to representational variation, as in the previous point), you should have an example for each variation.
If we tell you that we are writing a program that takes a positive integer as input, use positive integers in all of your examples and tests. You do not have to test 0
, -1
, or some other input that is outside the scope of the problem.
Why? Error handling is a complex topic on its own. We'll touch it very lightly in 111; you'd see more about it in later CS courses, where you have the tools and skills that you need to actually deal with such situations. For now, our goal is to get you writing programs that are robust on valid inputs.
There is a difference between a program being called on an invalid input and a program needing to report that it can't produce a valid answer in a particular situation. If a problem statement tells you to report an error, you should have examples and/or tests that cover the error-inducing behavior.
We use the criteria that we just gave you: edge cases, representational variation, structural variation, and error checking (if errors are part of the problem statement).
Sometimes, we grade your tests for their correctness (do they accurately capture the problem statement) and thoroughness (did they do a good enough job with the criteria that they can distinguish working from broken solutions to a problem). We'll introduce this in more detail as we go.