Writing and testing table functions
Here’s the program we ended up with last time:
include tables include gdrive-sheets include shared-gdrive("cs111-2020.arr", "1imMXJxpNWFCUaawtzIJzPhbDuaLHtuDX") ssid = "1jHvn5CPE6RkTTQRIXQbY5n5p4aiOH7fZsnwK2s6s6tc" spreadsheet = load-spreadsheet(ssid) all-municipalities = load-table: name :: String, city :: Boolean, population-2000 :: Number, population-2010 :: Number # true because the sheet has a "header" row source: spreadsheet.sheet-by-name("municipalities", true) end fun is-town(r :: Row) -> Boolean: not(r["city"]) end fun percent-change(r :: Row) -> Number: (r["population-2010"] - r["population-2000"]) / r["population-2000"] end fun fastest-growing-towns(municipalities :: Table) -> Table: towns = filter-with(municipalities, is-town) towns-with-percent-change = build-column(towns, "percent-change", percent-change) sort-by(towns-with-percent-change, "percent-change", false) end
We’ve done a bit of a bad thing here: we have written three functions, but we don’t have tests for any of them! Let’s see how we can rectify this.
We can test table programs by using test tables–tables with the same structure as the larger tables we are interested in, but which are smaller and contain data that are useful for testing. What would good test data look like for this problem?
test-municipalities = table: name, city, population-2000, population-2010 row: "City", true, 100, 101 row: "Town 1", false, 100, 102 row: "Town 2", false, 100, 99 row: "Town 3", false, 50, 54 end
Let’s see how we use these test data to test our functions.
First, we can test is-town
. This is a boolean function; we want to make sure
it works on town and city rows.
fun is-town(r :: Row) -> Boolean: not(r["city"]) where: is-town(test-municipalities.row-n(0)) is false is-town(test-municipalities.row-n(1)) is true is-town(test-municipalities.row-n(2)) is true end
How about percent-change
? In the end we’ll only want to use it on towns, but
we can test it on city rows, too.
fun percent-change(r :: Row) -> Number: (r["population-2010"] - r["population-2000"]) / r["population-2000"] where: percent-change(test-municipalities.row-n(0)) is 0.01 percent-change(test-municipalities.row-n(1)) is 0.02 percent-change(test-municipalities.row-n(2)) is -0.01 end
Finally, we can test our top-level table function. It produces a table, so we’ll have to construct a new table that it should return.
fun fastest-growing-towns(municipalities :: Table) -> Table: towns = filter-with(municipalities, is-town) towns-with-percent-change = build-column(towns, "percent-change", percent-change) sort-by(towns-with-percent-change, "percent-change", false) where: test-municipalities-after = table: name, city, population-2000, population-2010, percent-change row: "Town 3", false, 50, 54, 0.08 row: "Town 1", false, 100, 102, 0.02 row: "Town 2", false, 100, 99, -0.01 end fastest-growing-towns(test-municipalities) is test-municipalities-after end
It’s important to write out the result table before you see the table produced by the function–we’re trying to make sure that the function does what it’s supposed to, which means that we have to predict its behavior! If we just copy in the table it produces, we’re testing the function against its own behavior rather than against our intuitions.
Testing a function against its own previous behavior can be useful if you are planning on changing the function and want to ensure that it still has the same behavior. This is called a regression test.