Pyret Testing Design and Clarity Guide
Examples
- All functions, including helper functions, require examples. The only exception is if a function returns an
Image
. Examples are written in where
-blocks.
- Examples should generally be written before you start writing your function. Writing good examples will help you understand how your function should behave on different inputs.
- Examples should reflect various input scenarios so that you exercise the possible behaviors of your function. You do not need to test every input or every input combination (indeed, there are usually infinite possible combinations!), but you should have enough to cover a wide range of your function’s possible behaviors.
- In particular, your examples should cover edge cases–inputs at the boundaries of particular ranges. For instance, many functions that have a number as an input should include an example where that input is
0
.
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.
For most assignments, you can get a substantial amount of partial credit if you write good examples for a function even if your function doesn’t work correctly.
When writing examples before implementing a function, it may be helpful to include because
clauses as a way of figuring out what the function body could look like:
fun times-four(x :: Number) -> Number:
doc: "multiplies input x by four"
... # function body elided
where:
times-four(3) is 12 because 3 * 4
times-four(0) is 0 because 0 * 4
times-four(-1) is -4 because -1 * 4
end
Unless an assignment says otherwise, because
clauses are not required–but they might be useful to you!
Tests
For some assignments, we will ask you to write test cases as well as examples. Test cases are written in check blocks
. Test cases are generally written after you develop your function and believe it works, in order to gain more confidence in its correctness. Test cases should include inputs that might not have been included in examples–for instance, strings with unusual characters, or fractions.
We will sometimes ask you to write test cases for functions you aren’t actually writing in order to practice writing good tests.
Here’s an example of a function with both examples and test cases:
fun abs-val(x :: Number) -> Number:
doc: "return the (positive) absolute value of input x"
... # function body elided
where:
abs-val(-4) is 4
abs-val(0) is 0 # edge case!
abs-val(3) is 3
end
check:
abs-val(1908737) is 1908737 # a very large number
abs-val(-3498347) is 3498347 # a very large negative number
abs-val(22/7) is 22/7 # a fraction
abs-val(-0.8) is -0.8 # a negative decimal number
end
A handin with this work would receive full testing points.
Design
-
Use constants to capture relationships between values. For example, if you have rectangle(500, 300, "solid", "green")
but mean for the height to be a scaled fraction of the width, you should instead write:
width = 500
rectangle(width, 3/5 * width, "solid", "green")
-
Use helper functions to configure an expression that appears multiple times with slightly different arguments. For example,
stairs = beside-align("bottom", rectangle(50, 50, "solid", "gray"),
beside-align("bottom", rectangle(50, 100, "solid", "gray"),
beside-align("bottom", rectangle(50, 150, "solid", "gray"),
rectangle(50, 200, "solid", "gray"))))
could be rewritten as:
stair-width = 50
fun stair(height :: Number) -> Image:
doc: "make an individual stair given its height"
rectangle(stair-width, height, "solid", "gray")
end
stairs = beside-align("bottom", stair(50),
beside-align("bottom", stair(100),
beside-align("bottom", stair(150), stair(200))))
-
Make sure your helper functions and constants are not redundant in light of existing built-in functions. For example, there is no point in making this function:
fun string-lower(s :: String) -> String:
string-to-lower(s)
end
since you could always use string-to-lower
instead.
Clarity
-
Write docstrings for all functions, including helper and nested functions.
A good docstring gives a description of the function, including its input(s) and output. Ideally, by looking at the docstring you know what the function does and how to use it without looking at the function body itself.
fun three-stripes(bot-col :: String, mid-col :: String,
top-col :: String) -> Image:
doc: ```makes a rectangular flag with three stripes.
the inputs describe the stripes' colors, bottom-to-top.```
...
end
Bad docstring descriptions
doc: "helper function for flag making"
doc: "output the right image for this problem"
doc: "uses above to stack images"
- how your function body actually works is irrelevant to the docstring.
-
Give constants and helper functions useful names.
n1 = 3.1415
n2 = 500
n3 = 30
fun helper(x):
x + 3
end
should be rewritten as (for example)
pi = 3.1415
flag-width = 500
circle-radius = 30
fun add-3(x):
doc: "adds three to its input x"
x + 3
end
-
All functions require type annotations on both inputs and output.
-
Names of constants and functions should be lower case and dash separated. For configuration constants (for example the height of a character) it is acceptable to use all caps dash-separated names.
# Good Pyret Style:
flag-width = 500
width-to-height-ratio = 3/5
CHARACTER-HEIGHT = 40
# Bad Pyret Style:
flag_width = 500
widthHeightRatio = 3/5
CharacterHeight = 40
-
Keep lines under 100 characters. You will at some point see a vertical dashed blue line in Pyret.
If you see this line, your lines are too long and you need to add linebreaks (press enter
at places in your code).
The 100 character limit applies even when you have long strings and/or docstrings. Long docstrings can be written using backticks (`):
fun f(x :: Number)
doc: ```return 2; ignore inputs, always outputs 2 no matter
what, this function is going to return 2```
2
end
-
Indent your code properly. You can do this by pressing ctrl-a
then tab
on Windows and Linux, or cmd-a
then tab
on Mac.
Pyret Testing Design and Clarity Guide
Examples
Image
. Examples are written inwhere
-blocks.0
.As an example, let’s consider a function
compare
that takes two numerical inputsx
andy
and returns a string. It should return"way less"
ifx
is smaller thany - 2
,"about the same"
ifx
is betweeny - 2
andy + 2
(inclusive) and"way more"
ifx
is greater thany + 2
. Here’s a set of good examples for this function.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.For most assignments, you can get a substantial amount of partial credit if you write good examples for a function even if your function doesn’t work correctly.
When writing examples before implementing a function, it may be helpful to include
because
clauses as a way of figuring out what the function body could look like:Unless an assignment says otherwise,
because
clauses are not required–but they might be useful to you!Tests
For some assignments, we will ask you to write test cases as well as examples. Test cases are written in
check blocks
. Test cases are generally written after you develop your function and believe it works, in order to gain more confidence in its correctness. Test cases should include inputs that might not have been included in examples–for instance, strings with unusual characters, or fractions.We will sometimes ask you to write test cases for functions you aren’t actually writing in order to practice writing good tests.
Here’s an example of a function with both examples and test cases:
A handin with this work would receive full testing points.
Design
Use constants to capture relationships between values. For example, if you have
rectangle(500, 300, "solid", "green")
but mean for the height to be a scaled fraction of the width, you should instead write:width = 500 rectangle(width, 3/5 * width, "solid", "green")
Use helper functions to configure an expression that appears multiple times with slightly different arguments. For example,
stairs = beside-align("bottom", rectangle(50, 50, "solid", "gray"), beside-align("bottom", rectangle(50, 100, "solid", "gray"), beside-align("bottom", rectangle(50, 150, "solid", "gray"), rectangle(50, 200, "solid", "gray"))))
could be rewritten as:
Make sure your helper functions and constants are not redundant in light of existing built-in functions. For example, there is no point in making this function:
since you could always use
string-to-lower
instead.Clarity
Write docstrings for all functions, including helper and nested functions.
A good docstring gives a description of the function, including its input(s) and output. Ideally, by looking at the docstring you know what the function does and how to use it without looking at the function body itself.
Bad docstring descriptions
doc: "helper function for flag making"
doc: "output the right image for this problem"
doc: "uses above to stack images"
- how your function body actually works is irrelevant to the docstring.Give constants and helper functions useful names.
should be rewritten as (for example)
All functions require type annotations on both inputs and output.
Names of constants and functions should be lower case and dash separated. For configuration constants (for example the height of a character) it is acceptable to use all caps dash-separated names.
Keep lines under 100 characters. You will at some point see a vertical dashed blue line in Pyret.
If you see this line, your lines are too long and you need to add linebreaks (press
enter
at places in your code).The 100 character limit applies even when you have long strings and/or docstrings. Long docstrings can be written using backticks (`):
Indent your code properly. You can do this by pressing
ctrl-a
thentab
on Windows and Linux, orcmd-a
thentab
on Mac.