Criteria Tests

As stated on the Rubrics page, a criteria tests is a Rust function that verifies if a criteria was actually fulfilled. They will vary widely. This page will show how to write a test and a few examples to get you started.

A Single Test

Tests are just functions, but they must have a specific signature. They must always accept the same parameters and return the same type of value. Here's the signature

// Be sure TestData is imported
use rubric::TestData;

fn my_test(data: &TestData) -> bool {
    // Test code goes here...
}

Every test must accept a reference to a TestData struct. This TestData is stored on a Submission, which I'll cover in a different section. What you should know now is that it's an alias to HashMap<String, String>.

Sometimes you won't need TestData in a test, in which case you can just name the parameter _ and Rust won't complain.

Tests must also return a boolean. true if it passes, false otherwise. If a test returns true, then the associated criteria's worth will be added to the point total. If all the criteria tests return true, the maximum score is achieved.

Using TestData

Remember that a TestData struct is really just a HashMap. It will contains keys and values that you specify when setting up a Submission. You can use any of the methods that HashMap's have. 90% of the time, you'll just want to read a value from the TestData. There's 2 ways to do that.


fn some_test(data: &TestData) -> bool {
    // The easy but dangerous way to get a value
    // this will *crash* if the key doesn't exist
    let my_value = data["my_key"];

    // The safe way to get a value
    if let Some(value) = data.get("my_key") {
        // Key exists, now we have the value
        println!("Value is {}", value);
    } else {
        // Key doesn't exist, something went wrong, handle error
        println!("Value doesn't exist!");
    }

    // ...
}

It's important that you take precautions when writing a grader. You really don't want it to crash while your students are running it. The two examples above to the same thing, but the second method won't crash if the key doesn't exist.

Organization

I strongly recommend making a test.rs file alongside main.rs to keep your tests in. Of course, you don't have to. You could keep your tests as loose functions in main.rs, or maybe have a submodule in main.rs.

Again, I recommend making a tests.rs file and keeping them in there. Here's how I set things up.

// tests.rs
use rubric::TestData;

fn test_from_tests_rs(_: &TestData) -> bool {
    // test code goes here
    return true;
}

// more tests here...
// main.rs
extern crate rubric;

// declare tests.rs as a module
mod tests;
// bring all test functions into scope
use tests::*;
// ...

Attaching Tests

Each criteria has an associated test, but we need to tell our program which test goes with which criteria. In our rubric, each criteria should have a func key. This should exactly match the name of the test. Rust uses snake_case for function names. After we've written our tests and loaded our rubric, we can use the attach! macro to assign them.

name: Basic rubric

criteria:
  "Only criteria":
    worth: 50
    # This is the important bit
    # it allows attach!() to find the right function
    func: my_criteria_test
    # ...

fn my_criteria_test(_: &TestData) -> bool {
    // test code goes here...
    return true;
}


fn main() {
    // load rubric
    // code omitted
    // be sure it's mutable
    let mut rubric = //...

    attach!(rubric, my_criteria_test);
}

Helpers

There are a few helper modules and functions that perform some common tasks. Sometimes your tests will be one-liners from the helper modules. See the helpers module documentation on docs.rs for more info.

Examples

Some basic examples can be found in the examples directory on Github, specifically in this file in the git_lab example.