Tauri is a desktop apps framework built with Rust for its backend and JavaScript for its front end. The first thing I like to do in a project is set up a unit tests infrastructure. I found it surprisingly easy to do it in Rust.
It has become a summer tradition with my son: we start developing a game together. This time, we thought of creating a desktop game.
After a long time on my learning list, I finally had an excuse to use Tauri. Building the app went pretty smoothly (more on that in upcoming posts), but I was missing how to unit test the Rust code.
Table of Contents
The Bare Tauri Project
A Tauri project is composed of two main parts: Rust and JavaScript. Creating it is easy with a simple npm
command: npm create tauri-app
(you can replace npm
with your favorite JS package manager). I’ve used it to scaffold a basic Tauri boilerplate:
The src
folder holds the JavaScript code. We’ll get back to it in later posts when I’ll tell you about my amazing Desktop app.
The src-tauri
folder holds the Rust code. Let’s get inside.
Inside we can see the icons
folder, which holds icons for our desktop app and the src
which holds our app. Our app consists of only one file – main.rs
. Makes sense, as this the beginning of our project.
main.rs
is pretty simple:
The first line is a convention meant to help us build for windows.
The #[tauri::command]
allows us to build API commands for Tauri. It acts as a decorator to add more characteristics to the following code. We’ll get back to it in later posts.
Then I have the greet
function, which is the simple function we’re going to see a lot in this article.
fn main
starts the Rust app. It starts the Tauri builder and does some stuff. The important thing here is the invoke_handler
part. You see it adds a handler with greet
? This sets up the API between the client and the Rust server (remember that tauri::command
decorator?).
I know you want to get to business. Don’t worry! That’s the last time Tauri gets in the way of our story 😉
Now, we came here to test greet
and make sure it does what we want, right?
How to Write and Run Unit Tests in a Rust Project
There are two places where you can add tests in Rust. The module file itself or a test folder inside the module. We’ll talk about this later.
Our test code is going to test that when we call greet
with a name, it returns the expected string with the name. We will write two tests. One that passes and one that fails. Here’s how it looks like. Don’t worry, we’ll dive into the code in a bit 🙂
The code above is how you write unit tests in Rust.
[cfg(test)]
is actually “configuration(test)” and it tells the compiler to ignore this on build and run it on test.
The next lines define a module (mod tests
). When you use super::*
that means you “import” all the properties and methods of the current module. This is why I’m able to use greet
in my test functions. Which, at long last, brings us to our test functions.
fn test_greet
and fn test_greet_badly
are simple test functions that assert greet
‘s output.
assert_eq!(greet("Johnny"), "Hello, Johnny! You've been greeted from Rust!");
does exactly what you’d expect: asserts that the output of greet("Johnny")
equals to the string.
Let’s keep it simple and add this whole code to the main.rs
file, just below the main
function.
In order to run tests in Rust, we use the command cargo test
. Here are the results:
Assuming you are not overwhelmed by the usage of cargo
so far, let’s continue to the next step: modularizing our code.
How to Modularize Rust Code
Right now, we have our test inside our module. We actually have our greet
function inside the main.rs
file. It looks like this:
That’s fine now, but this will get more complicated as the project grows. Let’s break down the Rust code into modules.
In Rust, modules are either files or folders with a mod.rs
file in them.
So we could extract greet
to a greet.rs
file and then import it in our main
file like this:
main.rs
is now much smaller. All of our logic resides in the greet.rs
file. Notice we made the function public (the pub
preceding the function). We import the module via the mod greet
line. Rust expects the file greet.rs
to reside in the same folder as main.rs
.
The line that changed is our invoke handler:
.invoke_handler(tauri::generate_handler![greet::greet])
Notice we are using greet:greet
. Because I’m used to JavaScript, I look at it like object notation. Instead of greet.greet
, we now have greet::greet
. Not a big deal 🙂
In JavaScript we also have named imports. This can be done using the use
command like this:
mod greet;
use crate::greet::greet;
Then in our code, we can use greet
without mentioning its module:
.invoke_handler(tauri::generate_handler![greet])
Now we have our main file and a way to modularize our code. Our tests are still in the implementation file, though. Let’s fix that.
How to Separate Tests from Implementation in Rust
I mentioned earlier that a module can also be a folder with a mod.rs
file. That means, we can create a greet
folder with mod.rs
file and it will work the same:
Nothing changed except greet.rs
turned into greet/mod.rs
.
We can use that in order to extract the tests if we create a module test
inside greet
:
Our files are going to look like this:
Summary
Wow! We got modularization and unit tests working in Rust in like five minutes without any dependencies! That’s pretty awesome, in my opinion. I’m very happy to see nodejs going that way with node:test
.
Now my project is ready to begin. Because I’m working on the project with my son, it’s going to take a while, but I’ll update about it. We’re practically building a game that will run on a desktop. Hey, you might see our game on Steam one day 😉
Thanks a lot to Yael Oshri Balla and Yuval Bar Levi for the kind and thorough review.