Expand description
Testing framework for known answer tests.
This crate aims to drastically reduce the boilerplate code associated with rust tests, as well as to make known-answer tests easier to write and extend.
This framework splits the tests into the test implementation and data, which is stored in .toml files.
Under the hood, Kat uses Serde and Toml-rs to deserialize test data. Both need to be added as dependencies to your crate.
Getting Started
Toml file layout
The toml file must contain two sections, the global section and the test section (or sections).
// In this section global variables are defined.
[global]
my_global_var = "This is a global variable"
// In these sections we define test cases.
// Each test owns its own data.
// Though every test must have the same
// data signature
[[test]]
id = 0 // int
data = "This is data for test 0" // string
input = "INPUT" // string
expected = "INPUT" // string
// Multiple tests can be defined with
// consecutive "test" tables
[[test]]
id = 1 // int
data = "This is data for test 1" // string
input = "INPUT" // string
expected = "INPUT" // string
If you’d like a comprehensive list of types, that you can include in your toml file, then visit the Toml Website
Writing the tests
Writing the tests is just as straight forward as writing the data. This tutorial will go step by step, in order of definition, and is based on the earlier demonstrated toml file layout.
Import the kat crate
This can be done, either in your test files global namespace (e.g tests/my_test.rs), or in a submodule (e.g tests/my_test.rs::my_submodule).
// Import Kat
use kat::*;
Configure the test file path
The kat_cfg macro configures the filepath of your test file. The path will be interpreted, relative to the workspace root. The file extension can be ommited, since we only support toml. String quotes around the path are not needed, since kat will directly interpolate the path from the macro expression.
// "WORKSPACE_ROOT/tests/data/my_data.toml"
kat_cfg!(tests/data/my_data);
Global and Test variables
Now we define the layout for our global and test variables. Define the variables, just like you would in a normal Rust struct.
Since Kat, internally uses Serde to deserialize the variables, every type in global and test must derive Deserialize.
More to deserialization of types, in the Deserializing Types section
The global and test macros will generate structs which will later be parsed as the test files content.
// Define global variables
global! {
// The name of the variable must match
// the one defined in your data file.
my_global_var: String
}
// Define our test specific variables.
// The same conventions, as in the global!
// macro apply.
test! {
id: usize,
data: String,
input: String,
expected: String,
}
Running the tests
And finally we provide the runner for our tests.
Depending on your IDE, you can see a “Run tests” hint (VS Code for example).
The tests will be run in the module “YOUR_MODULE::kat_tests”, and the main test function is simply called “tests”.
Inside the run macro, you get access to your global and
test variables, inside the here named variables globals
and test_case
.
Both can be named like you would any other variable.
On top of that you can execute any statements inside the macro.
Though, mutating globals
and test_case
is not possible, since
they’re internally defined as immutable aka read-only.
// Test Runner
run! {
// Test Runner
//
// Note the lambda like invocation syntax.
// It's specified in the macro as a match, for
// easier readability and familiarity.
|globals, test_case| -> {
// Now pass the statements you want to run
// We can access the global variable.
println!("{}", global.my_global_var);
// In similar fashion, the test case.
println!("{}", test_case.id);
// Any statements can be executed
// Assertions
assert_eq!(test_case.input, test_case.expected);
// Function call which is defined somewhere...
my_super_expensive_function();
// Also from other modules
mymod::my_function();
// Variables
let x = 25;
// Macros
my_crate::some_macro!();
}
}
Panics
The runner panics, if the test file wasn’t found, an IO Error occured (e.g File open unsuccessful), or if toml parsing was erroneous.
All in all, we end up with a structure like this:
// Path configuration
kat_cfg(...);
// Define global variables
global! {
...
}
// Define Test variables
test! {
...
}
// Implement Test Runner
run! {
|global, test| -> {
...
}
}
Runner attributes
As per usual rust tests, you can annotate the run macro with
test attributes.
The initial #[test]
attribute is already being added for you internally.
// Ignore tests
run! {
#[ignore = "not yet implemented"]
|global, test| -> {
...
}
}
// Should panic
//
// Note, that when running the tests from
// the 'run' hint in your IDE, the test will
// still be logged as fail. The test will
// only accept the panic, when run with
// "Cargo test"
run! {
#[should_panic(expected = "values don't match")]
|global, test| -> {
assert_eq!(1, 2, "values don't match");
}
}
Type Attributes
Kat supports type attributes for both, types defined in the global and test macro.
// Global macro as an example
global! {
my_type: String,
#[my_attribute]
my_attributed_type: usize
}
Deserializing Types
Common Types
Kat provides the major toml types in its types module. However, Kat does not support deserialization of multi-type arrays. For this case it is encouraged to deserialize an array of tables.
use kat::{types, DeriveTable};
// Kat provides a "DeriveTable" attribute,
// which actually is an alias for Serde's
// Deserialize proc-macro.
//
// This is how you define a table
#[derive(DeriveTable)]
struct MyTable {
value: types::TomlInt
}
global! {
toml_string: types::TomlString,
toml_int: types::TomlInt,
toml_float: types::TomlFloat,
toml_date: types::TomlDate,
toml_bool: types::TomlBool,
toml_int_array: types::TomlArray<types::TomlInt>,
toml_table: MyTable,
}
...
The test file would look something like this:
[global]
toml_string = "Toml String"
toml_int = 10
toml_float = 3.1415
toml_date = 1979-05-27
toml_bool = true
toml_int_array = [1, 2, 3, 4, 5]
[global.toml_table]
value = 22
...
Deserializing Custom Types
Since Kat internally deserializes its types with the help of Serde and Toml-rs,
primitive types like String
or usize
can be parsed directly from toml, without
any macro magic, because Serde or Toml-rs provide internal deserialization implementations.
So technically you could deserialize custom types with serde attributes.
// Your custom type
struct StringHolder(String);
impl From<String> for StringHolder {
fn from(s: String) -> Self {
Self(s)
}
}
// Generic String deserializer
// Deserialize a [T] if it's String constructable
fn deserialize_from_string<'de, D, T>(deserializer: D) -> Result<T, D::Error>
where
D: Deserializer<'de>,
T: From<String>
{
let s = String::deserialize(deserializer)?;
Ok(T::from(s))
}
global! {
// Use Serde attribute
#[serde(deserialize_with = "deserialize_from_string")]
string_holder: StringHolder
}
...
However, this results in a lot of boilerplate code.
Luckily, Kat provides you with streamlined ways, in which you can focus on the From implementation, and let Kat handle the code generation:
Deserialize Custom Types: The Kat way
Kat provides macros that generate the code needed to deserialize your value.
struct StringHolder(String);
// Note again the lambda syntax for
// familiarity and readability
impl_deserialize_from_toml_string!(
|s| -> StringHolder {
StringHolder(s)
}
);
// Now use it
global! {
string_holder: StringHolder
}
Inside Toml file
[global]
string_holder = "Hey Ho!"
Here, s
denotes the variable name for the passed
TomlString, you can name it whatever
you wish for. Then follows an arrow with the type, the code is
to be generated for, here StringHolder
. And finally the function
body.
The function body, is essentially the body of the
impl From<TomlString> for StringHolder
implementation,
this macro generates. The macro also generates a deserialize
implementation.
Macros like this exist for all types in the types module, but Table and Array. For these two, you will need to call the impl_deserialize_from_deserializable macro.
The impl_deserialize_from_deserializable macro can deserialize a custom type from any type that implements Serde’s Deserialize trait.
#[derive(DeriveTable)]
struct MyTable {
value: TomlInt
}
struct MyTableHolder(MyTable)
// Denote the input type being typed.
// As stated earlier, this macro
// generates `impls` from any type that
// is deserializable, so it needs the type
// annotation.
// The rest stays exactly the same.
// Note, that MyTableHolder doesn't
// have to be a tuple, this still is
// simply a From<T> implementation
impl_deserialize_from_deserializable!(
|table: MyTable| -> MyTableHolder {
MyTableHolder(table)
}
);
// From Array
struct MyArrayHolder(TomlArray<usize>);
impl_deserialize_from_deserializable!(
|array: TomlArray<usize>| -> MyArrayHolder {
MyArrayHolder(array)
}
);
With this macro, it’s also possible to chain your custom types.
// MyArrayHolder from previous example
struct HoldsArrayHolder(MyArrayHolder);
impl_deserialize_from_deserializable!(
|holder: MyArrayHolder| -> HoldsArrayHolder {
HoldsArrayHolder(holder)
}
);
This is possible, since the macro generated
the code for the Deserialize trait for MyArrayHolder
Final Notes
It is discouraged to rename the crate, since many macros
inside the crate use the kat::
module namespace
in order to directly depent on a type, thus not cluttering the global namespace.
On top of that, many exported traits and macros use the __XXX
prefix.
These items typically abstract the code generation away, thus, are private.
They should not be used directly.
Modules
Deserializable types
Macros
private. should not be used directly
Defines the global variables inside the test file.
Generate Deserialize trait for any type that is constructable from a type that implements Serde’s Deserialize trait
Generate Deserialize trait for any type that is constructable from a TomlBool
Generate Deserialize trait for any type that is constructable from a TomlDate
Generate Deserialize trait for any type that is constructable from a TomlFloat
Generate Deserialize trait for any type that is constructable from a TomlInt
Generate Deserialize trait for any type that is constructable from a TomlString
Configure the test files location.
Runs the tests.
Defines the test specific variables inside the test file.
Traits
A data structure that can be deserialized from any data format supported by Serde.
A data structure that can be deserialized without borrowing any data from the deserializer.
A data format that can deserialize any data structure supported by Serde.
Derive Macros
Type to generate a table from a struct
private. should not be used directly