Testing
Automated tests are essential to ensure your application's stability,
quality, and correctness. It's common to write Tests for libraries or
server-side code, but many people struggle with writing automated
tests for Graphical User Interface (GUI) applications.
Following
the separation of concerns you learned about in the Process Model
chapter, we recommend you set up two kinds of tests:
- Unit Tests - Test frontend and Core functionality independently in isolated contexts.
- End-to-End Tests (E2E) - Spiin up full instances of your app and simulate real user interactions to make sure individually tested components work well together.
In this guide we walk you through setting up units tests for Rust and JavaScript, as well as End-to-End tests using WebDriver.
Unit Testing
Unit tests verify that individual units of source code are functioning as expected. This usually doesn't include UI (see End-to-end Tests for that) but small chunks of internal logic, for example individual functions or methods.
Rust
Cargo comes with a builtin test runner - cargo test
- that will run
unit tests and report passes and failures. The simplest test in rust
is a function with the test
attribute. To change a plain function
into a test, add #[test]
to the line before fn
.
Since it's common to write many small tests to ensure different expectations, Rust unit tests are commonly grouped into Test Modules:
The #[cfg(test)]
attribute ensures that the module is only compiled
when running cargo test
but stripped when you build the binary for
development or release. To learn more, see the Conditional
Compilation reference.
The function body uses the assert_eq!
macro to assert that 2 + 2
equals 4 and 4 + 4 equals 8.
JavaScript
Unit tests in JavaScript are more complicated, as there are many competing test runners: Jest, Mocha, and Vitest are popular choices. For the following code-snippets we will be using Vitest.
Contrary to Rust, where tests co-located with the source code (i.e. in
the same file), JavaScript tests are written in a separate file,
commonly named *.tests.js
.
Filename: tests/main.test.ts
As your tests are executed in an Isolated Context, you need to mock Tauri APIs. See Mocking Tauri APIs for more details.
A popular convention is to add a test
script to your package.json
file, so users immediately know how to test your application. Let's
add a test
script to our example application that just alises to the
vitest
test runner:
{
"name": "test-application",
"version": "1.0.0",
"scripts": {
"test": "vitest"
},
"devDependencies": {
"vitest": "0.3.5"
}
}
Now we can run our test suite by opening a terminal and executing the following command:
npm
npm test
yarn test
pnpm test
End-to-End Testing
End-to-end Tests (E2E) tests simulate a user’s step-by-step experience, testing the interactions between many components in the process.
WebDriver is a standardized interface to interact with web documents primarily intended for automated testing. It provides capabilities for navigating to web pages, user input, JavaScript execution, and more.
Tauri supports the WebDriver interface by leveraging the native
platform's WebDriver server underneath a cross-platform wrapper
provided by tauri-driver
.
Prerequisites
Install the latest tauri-driver or update an existing installation by running:
cargo install tauri-driver
Because we currently utilize the platform's native WebDriver server,
there are some requirements for running tauri-driver
on supported
platforms. Platform support is currently limited to Linux and Windows.
Linux
We use WebKitWebDriver
on Linux platforms. Check if this binary
exists already (command which WebKitWebDriver
) as some distributions
bundle it with the regular WebKit package. Other platforms may have a
separate package for them, such as webkit2gtk-driver
on Debian-based
distributions.
Windows
Make sure to grab the version of Microsoft Edge Driver that matches your Windows' Edge version that the application is being built and tested on. This should almost always be the latest stable version on up-to-date Windows installs. If the two versions do not match, you may experience your WebDriver testing suite hanging while trying to connect.
The download contains a binary called msedgedriver.exe
.
tauri-driver
looks for that binary in the $PATH
so make sure
it's either available on the path or use the --native-driver option on
tauri-driver
. You may want to download this automatically as part
of the CI setup process to ensure the Edge, and Edge Driver versions
stay in sync on Windows CI machines. A guide on how to do this may be
added at a later date.
With WebdriverIO
WebdriverIO (WDIO) is a test automation framework that provides a Node.js package for testing with WebDriver. Its ecosystem also includes various plugins (e.g. reporter and services) that can help you put together your test setup.
Install the test runner
Open a terminal and run the the WebdriverIO starter toolkit in your project with the following command:
npm
npx wdio .
yarn
yarn create wdio .
pnpm
pnpm create wdio .
This installs all necessary packages for you and generates a
wdio.conf.js
configuration file.
Connect your Tauri app
Update the wdio.conf.js
file with the following options:
Add tests
Let's add a test file and a couple e2e tests to show what WDIO is capabable of. The test runner will load these files and autimatically run them.
Filename: test/specs/example.e2e.js
The luma
function on top is just a helper function for one of our
tests and is not related to the actual testing of the application. If
you are familiar with other testing frameworks, you may notice similar
functions being exposed that are used, such as describe
, it
, and
expect
. The other APIs, such as items like $
and its exposed
methods, are covered by the WebdriverIO API docs.
Run your tests
To run your test suite, open a terminal and execute the following command:
npm
npx wdio run wdio.conf.json
yarn
yarn wdio run wdio.conf.json
pnpm
pnpm wdio run wdio.conf.json
You should see the following output:
➜ webdriverio git:(main) ✗ yarn test
yarn run v1.22.11
$ wdio run wdio.conf.js
Execution of 1 workers started at 2021-08-17T08:06:10.279Z
[0-0] RUNNING in undefined - /test/specs/example.e2e.js
[0-0] PASSED in undefined - /test/specs/example.e2e.js
"spec" Reporter:
------------------------------------------------------------------
[wry 0.12.1 linux #0-0] Running: wry (v0.12.1) on linux
[wry 0.12.1 linux #0-0] Session ID: 81e0107b-4d38-4eed-9b10-ee80ca47bb83
[wry 0.12.1 linux #0-0]
[wry 0.12.1 linux #0-0] » /test/specs/example.e2e.js
[wry 0.12.1 linux #0-0] Hello Tauri
[wry 0.12.1 linux #0-0] ✓ should be cordial
[wry 0.12.1 linux #0-0] ✓ should be excited
[wry 0.12.1 linux #0-0] ✓ should be easy on the eyes
[wry 0.12.1 linux #0-0]
[wry 0.12.1 linux #0-0] 3 passing (244ms)
With Selenium
Selenium is a web automation framework that exposes bindings to
WebDriver APIs in many languages. Their Node.js bindings are available
under the selenium-webdriver
package on NPM. Unlike the WebdriverIO
Test Suite, Selenium does not come out of the box with a Test Suite
and leaves it up to the developer to provide one. We chose Mocha for
this example, since it's a popular choice.
Install the test runner
npm
npm install mocha chai selenium-webdriver
yarn
yarn add -D mocha chai selenium-webdriver
pnpm
pnpm add -D mocha chai selenium-webdriver
Connect your Tauri app
The following code will start an instance of your app before tests are run and ensure the instance is terminated afterwards. Let's add it to the default mocha testing file:
Filename: test/test.js
const os = require("os");
const path = require("path");
const { spawn, spawnSync } = require("child_process");
const { Builder, By, Capabilities } = require("selenium-webdriver");
// create the path to the expected application binary
const application = path.resolve(
__dirname,
"..",
"..",
"..",
"target",
"release",
"hello-tauri-webdriver"
);
// keep track of the webdriver instance we create
let driver;
// keep track of the tauri-driver process we start
let tauriDriver;
before(async function () {
// set timeout to 2 minutes
// to allow the program to build if it needs to
this.timeout(120000);
// ensure the program has been built
spawnSync("cargo", ["build", "--release"]);
// start tauri-driver
tauriDriver = spawn(
path.resolve(os.homedir(), ".cargo", "bin", "tauri-driver"),
[],
{ stdio: [null, process.stdout, process.stderr] }
);
const capabilities = new Capabilities();
capabilities.set("tauri:options", { application });
capabilities.setBrowserName("wry");
// start the webdriver client
driver = await new Builder()
.withCapabilities(capabilities)
.usingServer("http://localhost:4444/")
.build();
});
after(async function () {
// stop the webdriver session
await driver.quit();
// kill the tauri-driver process
tauriDriver.kill();
});
Add tests
Now we can add tests to the file we created earlier. We will be using
assertion functions provided by Chai such as expect
to validate
our app works as expected.
Filename: test/test.js
const { expect } = require("chai");
describe("Hello Tauri", () => {
it("should be cordial", async () => {
const text = await driver
.findElement(By.css("body > h1"))
.getText();
expect(text).to.match(/^[hH]ello/);
});
it("should be excited", async () => {
const text = await driver
.findElement(By.css("body > h1"))
.getText();
expect(text).to.match(/!$/);
});
it("should be easy on the eyes", async () => {
// selenium returns color css values as rgb(r, g, b)
const text = await driver
.findElement(By.css("body"))
.getCssValue("background-color");
const rgb = text.match(
/^rgb\((?<r>\d+), (?<g>\d+), (?<b>\d+)\)$/
).groups;
expect(rgb).to.have.all.keys("r", "g", "b");
const luma = 0.2126 * rgb.r + 0.7152 * rgb.g + 0.0722 * rgb.b;
expect(luma).to.be.lessThan(100);
});
});
If you are familiar with JS testing frameworks, describe
, it
, and
expect
should look familiar. We also have semi-complex before()
and after()
callbacks to setup and teardown mocha. If you compare
this to the WebdriverIO example, you notice a lot
more code that isn't tests, as we have to set up a few more WebDriver
related items.
Run your tests
To run your test suite, open a terminal and execute the following command:
npm
npx mocha
yarn
yarn mocha
pnpm
pnpm mocha
We should see output the following output:
➜ selenium git:(main) ✗ yarn test
yarn run v1.22.11
$ Mocha
Hello Tauri
✔ should be cordial (120ms)
✔ should be excited
✔ should be easy on the eyes
3 passing (588ms)
Done in 0.93s.
Mocking Tauri APIs
When writing your frontend tests, having a "fake" Tauri environment to
simulate windows or intercept IPC calls is common, so-called
mocking. The @tauri-apps/api/mocks
module provides some helpful
tools to make this easier for you:
Remember to clear mocks after each test run to undo mock state changes between runs! See
clearMocks()
docs for more info.
IPC Requests
Most commonly, you want to intercept IPC requests; this can be helpful in a variety of situations:
- Ensure the correct backend calls are made
- Simulate different results from backend functions
Tauri provides the mockIPC function to intercept IPC requests. You can find more about the specific API in detail here.
The following examples use Vitest, but you can use any other frontend testing library such as jest.
import { beforeAll, expect, test } from "vitest";
import { randomFillSync } from "crypto";
import { mockIPC } from "@tauri-apps/api/mocks";
import { invoke } from "@tauri-apps/api/tauri";
// jsdom doesn't come with a WebCrypto implementation
beforeAll(() => {
window.crypto = {
getRandomValues: function (buffer) {
return randomFillSync(buffer);
},
};
});
test("invoke simple", async () => {
mockIPC((cmd, args) => {
// simulate rust command called "add"
if (cmd === "add") {
return args.a + args.b;
}
});
expect(invoke("add", { a: 12, b: 15 })).resolves.toBe(27);
});
Sometimes you want to track more information about an IPC call; how
many times was the command invoked? Was it invoked at all? You can use
mockIPC()
with other spying and mocking tools to test this:
import { beforeAll, expect, test, vi } from "vitest";
import { randomFillSync } from "crypto";
import { mockIPC } from "@tauri-apps/api/mocks";
import { invoke } from "@tauri-apps/api/tauri";
// jsdom doesn't come with a WebCrypto implementation
beforeAll(() => {
//@ts-ignore
window.crypto = {
getRandomValues: function (buffer) {
return randomFillSync(buffer);
},
};
});
test("invoke", async () => {
mockIPC((cmd, args) => {
// simulate rust command called "add"
if (cmd === "add") {
return args.a + args.b;
}
});
// we can use the spying tools provided by vitest
// to track the mocked function
const spy = vi.spyOn(window, "__TAURI_IPC__");
expect(invoke("add", { a: 12, b: 15 })).resolves.toBe(27);
expect(spy).toHaveBeenCalled();
});
Listing 2-TODO: The mocked __TAURI_IPC__
is compatible with existing
testing tools.
Windows
Sometimes you have window-specific code (a splash screen window, for
example), so you need to simulate different windows. You can use the
mockWindows()
method to create fake window labels. The first
string identifies the "current" window (i.e., the window your
JavaScript believes itself in), and all other strings are treated as
additional windows.
mockWindows()
only fakes the existence of windows but no window properties. To simulate window properties, you need to intercept the correct calls usingmockIPC()
import { beforeAll, expect, test } from "vitest";
import { randomFillSync } from "crypto";
import { mockWindows } from "@tauri-apps/api/mocks";
// jsdom doesn't come with a WebCrypto implementation
beforeAll(() => {
//@ts-ignore
window.crypto = {
getRandomValues: function (buffer) {
return randomFillSync(buffer);
},
};
});
test("invoke", async () => {
mockWindows("main", "second", "third");
const { getCurrent, getAll } = await import(
"@tauri-apps/api/window"
);
expect(getCurrent()).toHaveProperty("label", "main");
expect(getAll().map((w) => w.label)).toEqual([
"main",
"second",
"third",
]);
});