Skip to main content

Writing Unit Tests with Mocks for OP-TEE Modules

This guide explains how to write unit tests in optee-utee crate using the built-in mock feature of optee-utee-sys crate.

The mock feature is built on the mockall crate. It automatically generates mock implementations of the OP-TEE Internal Core API.

Running Mock-Based Tests​

Mock tests run on the host machine using standard cargo test:

cd crates
cargo test -p optee-utee --features no_panic_handler -vv

The no_panic_handler feature prevents the custom panic handler from interfering with test execution.

Writing Your First Mock Test​

Basic Test Structure​

All mock tests follow a consistent pattern:

#[cfg(test)]
mod tests {
// Required: import std into no_std crate for testing
extern crate std;

use optee_utee_sys::{
mock_api,
mock_utils::SERIAL_TEST_LOCK,
};
use super::*;

#[test]
fn test_my_function() {
// 1. Acquire the serial test lock
let _lock = SERIAL_TEST_LOCK.lock().expect("should get the lock");

// 2. Get mock context for the API you want to mock
let ctx = mock_api::TEE_SomeFunction_context();

// 3. Set up expectations
ctx.expect().return_once_st(|params| {
// Return success or specific error code
raw::TEE_SUCCESS
});

// 4. Execute code under test
let result = my_function();

// 5. Assert results
assert!(result.is_ok());
}
}

Key Imports​

use optee_utee_sys::{
mock_api, // Mock API contexts
mock_utils::SERIAL_TEST_LOCK, // Global test lock
mock_utils::object::MockHandle, // Mock object handles
};
use optee_utee_sys as raw; // For TEE_SUCCESS, etc.

Mock Expectation Methods​

The mockall crate provides several methods for setting up mock behavior:

MethodDescriptionUse Case
.return_once_st(value)Returns value exactly onceSingle API call
.returning_st(closure)Calls closure for each invocationMultiple calls with dynamic behavior
.return_const_st(value)Always returns constant valueSimple stubbing
.times(n)Expects exactly n callsVerify call count

The _st suffix stands for "single-threaded" — required because mockall's default methods use thread-local storage incompatible with this test setup.

Common Patterns​

Pattern 1: Testing Success Cases​

#[test]
fn test_open_success() {
let _lock = SERIAL_TEST_LOCK.lock().expect("should get the lock");

let mut raw_handle = MockHandle::new();
let handle = raw_handle.as_handle();

let ctx = mock_api::TEE_OpenPersistentObject_context();
ctx.expect()
.return_once_st(move |_, _, _, _, _, obj| {
unsafe { *obj = handle.clone() };
raw::TEE_SUCCESS
});

let result = PersistentObject::open(
ObjectStorageConstants::Private,
&[0x01],
DataFlag::ACCESS_READ,
);

assert!(result.is_ok());
}

Pattern 2: Testing Error Cases​

#[test]
fn test_open_not_found() {
let _lock = SERIAL_TEST_LOCK.lock().expect("should get the lock");

static RETURN_CODE: raw::TEE_Result = raw::TEE_ERROR_ITEM_NOT_FOUND;
let ctx = mock_api::TEE_OpenPersistentObject_context();
ctx.expect().return_const_st(RETURN_CODE);

let result = PersistentObject::open(
ObjectStorageConstants::Private,
&[0x01],
DataFlag::ACCESS_READ,
);

assert!(result.is_err());
assert_eq!(result.unwrap_err().raw_code(), RETURN_CODE);
}

Pattern 3: Testing Drop Behavior​

#[test]
fn test_create_and_drop() {
let _lock = SERIAL_TEST_LOCK.lock().expect("should get the lock");

let mut raw_handle = MockHandle::new();
let handle = raw_handle.as_handle();

let create_ctx = mock_api::TEE_CreatePersistentObject_context();
let close_ctx = mock_api::TEE_CloseObject_context();

create_ctx.expect()
.return_once_st(move |_, _, _, _, _, _, _, obj| {
unsafe { *obj = handle.clone() };
raw::TEE_SUCCESS
});

close_ctx.expect().return_once_st(move |obj| {
debug_assert_eq!(obj, handle);
});

{
let _obj = PersistentObject::create(
ObjectStorageConstants::Private,
&[],
DataFlag::ACCESS_WRITE,
None,
&[],
).expect("should succeed");
// _obj dropped here, triggering TEE_CloseObject
}
}

Pattern 4: Multiple API Calls​

Use .times(n) for checking APIs called multiple times:

use std::sync::{Arc, Mutex};

#[test]
fn test_multiple_calls() {
let _lock = SERIAL_TEST_LOCK.lock().expect("should get the lock");

let call_count = Arc::new(Mutex::new(0));

let ctx = mock_api::TEE_SomeApi_context();
ctx.expect()
.returning_st({
let call_count = call_count.clone();
move |_| {
let mut count = call_count.lock().unwrap();
*count += 1;
}
})
.times(3); // Expect exactly 3 calls

// ... execute code that calls the API 3 times

assert_eq!(*call_count.lock().unwrap(), 3);
}

Pattern 5: Buffer Manipulation​

For APIs that read/write through pointers, simulate buffer operations:

#[test]
fn test_read_data() {
let _lock = SERIAL_TEST_LOCK.lock().expect("should get the lock");

let expected_data = vec![0xDE, 0xAD, 0xBE, 0xEF];
let ctx = mock_api::TEE_ReadObjectData_context();

ctx.expect()
.return_once_st(move |_, buf, size, count| {
let buffer: &mut [u8] = unsafe {
core::slice::from_raw_parts_mut(buf as *mut u8, size)
};
let len = expected_data.len().min(size);
buffer[..len].copy_from_slice(&expected_data[..len]);
unsafe { *count = len };
raw::TEE_SUCCESS
});

// ... execute read operation
}

Mocking Object Handles​

Use MockHandle to create mock object handles for testing:

let mut raw_handle = MockHandle::new();
let handle = raw_handle.as_handle(); // Returns TEE_ObjectHandle

// Pass `handle` to mock expectations that return or compare object handles

The Serial Test Lock​

Always acquire SERIAL_TEST_LOCK at the start of every mock test:

let _lock = SERIAL_TEST_LOCK.lock().expect("should get the lock");

This is critical because mockall's mock contexts use global state. Without this lock, concurrent tests would interfere with each other's expectations, causing flaky test failures.

Examples​

Study these existing test implementations for more patterns:

FileDemonstrates
crates/optee-utee/src/extension.rsPlugin invocation with buffer manipulation
crates/optee-utee/src/object/persistent_object.rsCreate/open/drop lifecycle, error cases
crates/optee-utee/src/object/transient_object.rsTransient object allocation and freeing
crates/optee-utee/src/object/object_handle.rsHandle validation and close behavior

Limitations​

  • Mock tests only verify API call patterns, not actual cryptographic operations or hardware behavior
  • Tests must run sequentially — always use SERIAL_TEST_LOCK