| |

Rust 47 🦀 Integration Testing in the tests Directory

A unit test verifies a function in isolation. An integration test verifies the library as the consumer uses it. The unit tests live in the src/ files, and they can access the private items. The integration tests live in the tests/ directory, and each file is a separate crate that depends on the library. The integration test calls the public API the same way the external user does, and it verifies the behavior that crosses the module boundaries. The two kinds of tests are the complements, and the cargo test runs both.

The tests/ directory is the convention. The cargo test discovers the .rs files in the tests/ directory and compiles each one as a separate crate. The test files use the library’s name in the use statement, and the library is the dependency. The integration test does not need the #[cfg(test)] module or the use super::*, because it is the separate crate. The #[test] attribute is the same, and the assertions are the same. The difference is the scope: the integration test sees the public API, and the unit test sees the internals.

The integration tests have the several patterns. The single-file test is the standalone file with the tests. The shared utility module is the tests/common/mod.rs that the multiple test files use. The setup and teardown is the std::sync::Once or the #[ctor] for the expensive resources. The cargo test --test name runs the specific integration test file, and the cargo test --test-threads=1 runs them serially. The integration tests are the black-box, and they verify the library as the product.

This chapter covers three areas. First, why the integration tests exist — the black-box problem and the public-API contract. Second, the structure — the tests/ directory, the single file, the shared module, and the cargo test discovery. Third, the patterns — the setup and teardown, the async tests, the test organization, and the best practices. The chapter ends with a complete example session, a quick reference, best practices, common pitfalls, real-world examples, and diagrams showing the integration test organization.

Key point: The integration tests live in the tests/ directory, and each file is a separate crate that depends on the library. The integration tests can only access the public API, and they verify the library as the consumer uses it. The cargo test discovers the .rs files in the tests/ directory, and the cargo test --test name runs the specific file. The tests/common/mod.rs is the shared utilities, and the #[test] and the assertions are the same as the unit tests.


Why the integration tests exist

The black-box problem. The unit test verifies the function with the knowledge of the internals. The integration test verifies the library without the knowledge of the internals. The integration test uses the public API, and it catches the problems that the unit test misses: the missing pub, the wrong module path, the inconsistent API, the broken example. The integration test is the user’s perspective, and the user’s perspective is the contract.

The public-API problem. The public API is the library’s contract. The integration test verifies the contract by calling the public functions in the same way the external crate would. The test that passes in the unit but fails in the integration means the API is not usable as intended. The integration test is the API acceptance test.

The module-boundary problem. The unit tests verify the functions within the module. The integration tests verify the interactions between the modules. The function is correct in isolation, and the combination is broken. The integration test catches the combination problem, and the unit test does not.

The example-verification problem. The documentation example shows how the library is used, and the documentation test verifies the example. The integration test verifies the real usage, and the two complement each other. The documentation example is the minimal, and the integration test is the comprehensive.

The regression problem. The change to the library can break the public behavior. The unit tests still pass, because the internals are correct. The integration tests fail, because the public behavior changed. The integration tests are the regression net for the public API.

The trade-off. The integration tests are the separate crate, and they compile the library as the dependency. The compilation is the slower, and the test runs are the longer. The integration tests are the fewer than the unit tests, and they focus on the public behaviors. The trade-off is between the coverage of the public API and the speed of the unit tests.


a. The structure of the tests directory

The tests/ directory is the sibling of the src/ directory, and the Cargo.toml is the project root.

my_project/
├── src/
│   └── lib.rs
├── tests/
│   ├── integration_test.rs
│   └── common/
│       └── mod.rs
└── Cargo.toml

The cargo test discovers the .rs files in the tests/ directory and compiles each one as a separate crate. The tests/common/mod.rs is the shared module, and the cargo test does not treat it as the separate test file because the mod.rs is the module file, not the test file.

The single integration test.

// tests/integration_test.rs
use my_project::add;

#[test]
fn it_adds_from_the_outside() {
    assert_eq!(add(2, 3), 5);
}

The use my_project::add imports the public function from the library. The #[test] marks the test, and the assert_eq! is the assertion. The test runs the same way the unit test does, and the difference is the scope.

The shared utility module.

// tests/common/mod.rs
pub struct TestContext {
    pub client: Client,
    pub database: Database,
}

impl TestContext {
    pub async fn new() -> Self {
        let database = Database::connect().await;
        let client = Client::new(&database);
        TestContext { client, database }
    }

    pub async fn cleanup(self) {
        self.database.drop().await;
    }
}

The common/mod.rs is the shared module. The test file imports it:

// tests/integration_test.rs
mod common;

use common::TestContext;

#[tokio::test]
async fn it_creates_the_user() {
    let context = TestContext::new().await;
    let user = context.client.create_user("Alice").await;
    assert_eq!(user.name, "Alice");
    context.cleanup().await;
}

The mod common; declares the module, and the use common::TestContext imports the struct. The cargo test does not treat the common/mod.rs as the test file, because the mod.rs is the module.

The cargo test discovery.

cargo test

The output shows the unit tests, the integration tests, and the documentation tests in the separate sections:

running 3 tests (the unit)
test tests::it_adds ... ok
test tests::it_subtracts ... ok

running 1 test (the integration_test)
test it_adds_from_the_outside ... ok

running 1 test (the doc)
test src/lib.rs - add (line 5) ... ok

The cargo test --test integration_test runs only the specific integration test file, and the cargo test --test-threads=1 runs the tests serially.


b. The setup and the teardown

The integration tests often need the expensive setup: the database connection, the file system, the network server. The setup and the teardown patterns are the several.

The Once pattern. The std::sync::Once runs the setup once for the whole test process:

use std::sync::Once;

static INIT: Once = Once::new();

fn setup() {
    INIT.call_once(|| {
        // The expensive setup
        std::env::set_var("DATABASE_URL", "postgres://localhost/test");
    });
}

#[test]
fn it_uses_the_setup() {
    setup();
    // The test
}

The INIT.call_once runs the closure only the first time the setup is called, and the subsequent calls are the no-ops. The pattern is the simple for the one-time setup.

The TestContext pattern. The struct with the new and the Drop:

pub struct TestContext {
    pub db: Database,
}

impl TestContext {
    pub async fn new() -> Self {
        let db = Database::connect("postgres://localhost/test").await;
        db.migrate().await;
        TestContext { db }
    }
}

impl Drop for TestContext {
    fn drop(&mut self) {
        // The cleanup
        let db = self.db.clone();
        tokio::spawn(async move {
            db.drop_all().await;
        });
    }
}

The TestContext::new creates the resource, and the Drop cleanup runs when the context goes out of scope. The pattern is the RAII, and it ensures the cleanup even when the test panics.

The serial execution. The integration tests that share the global state (the database, the file system) must run serially:

cargo test -- --test-threads=1

The --test-threads=1 runs the tests one at a time, and the shared state is safe. The alternative is the separate database per test, which is the slower but the isolated.

The #[ignore] for the expensive. The expensive integration test is ignored by default:

#[test]
#[ignore = "the requires the database"]
fn it_uses_the_database() {
    // The test
}

The cargo test skips it, and the cargo test -- --ignored runs it. The CI runs the ignored tests in the separate job.


c. The patterns and the best practices

The test organization. The tests/ directory contains the multiple files, and each file is the logical group:

tests/
├── common/
│   └── mod.rs
├── api_test.rs
├── database_test.rs
└── auth_test.rs

The api_test.rs, the database_test.rs, and the auth_test.rs are the separate crates, and the common/mod.rs is the shared. The cargo test --test api_test runs the specific file.

The descriptive names. The integration test description is the behavior:

#[test]
fn it_returns_the_user_when_the_found() { ... }

#[test]
fn it_returns_the_error_when_the_not_found() { ... }

The names are the readable, and the failure message is the clear.

The one concept per test. The integration test verifies one behavior:

// ✅ The one concept
#[test]
fn it_creates_the_user() { ... }

// ❌ The multiple concepts
#[test]
fn it_creates_the_user_and_sends_the_email_and_logs() { ... }

The assert! with the message. The integration test uses the descriptive message:

assert_eq!(user.name, "Alice", "the created user should have the name 'Alice'");

The Result return. The integration test can return the Result:

#[tokio::test]
async fn it_creates_the_user() -> Result<(), Box<dyn std::error::Error>> {
    let context = TestContext::new().await;
    let user = context.client.create_user("Alice").await?;
    assert_eq!(user.name, "Alice");
    Ok(())
}

The ? operator propagates the error, and the test fails with the clear message.

The cargo test --test for the specific. The --test flag runs the specific integration test file:

cargo test --test api_test
cargo test --test database_test

The cargo test --test-threads for the parallel. The --test-threads controls the parallelism:

cargo test -- --test-threads=4

The default is the number of the CPU cores, and the 1 is the serial.

The coverage. The cargo test with the --coverage flag or the cargo-tarpaulin measures the coverage:

cargo tarpaulin --out Html

The coverage shows the lines that the tests execute, and the gaps are the untested. The coverage is the metric, not the goal.

The CI integration. The CI runs the tests with the different configurations:

- name: Run tests
  run: cargo test --all-features

- name: Run the ignored tests
  run: cargo test -- --ignored

- name: Run the doc tests
  run: cargo test --doc

The --all-features enables the feature flags, and the separate jobs run the different test kinds.


Complete Example Session

// ============================================
// PART 1: THE LIBRARY
// ============================================

// src/lib.rs
pub struct Calculator {
    memory: f64,
}

impl Calculator {
    pub fn new() -> Self {
        Calculator { memory: 0.0 }
    }

    pub fn add(&mut self, value: f64) -> f64 {
        self.memory += value;
        self.memory
    }

    pub fn divide(&mut self, value: f64) -> Result<f64, String> {
        if value == 0.0 {
            return Err("the division by the zero".to_string());
        }
        self.memory /= value;
        Ok(self.memory)
    }

    pub fn memory(&self) -> f64 {
        self.memory
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_adds() {
        let mut calc = Calculator::new();
        assert_eq!(calc.add(5.0), 5.0);
    }
}


// ============================================
// PART 2: THE BASIC INTEGRATION TEST
// ============================================

// tests/calculator_test.rs
use my_project::Calculator;

#[test]
fn it_adds_from_the_outside() {
    let mut calc = Calculator::new();
    assert_eq!(calc.add(5.0), 5.0);
    assert_eq!(calc.memory(), 5.0);
}

#[test]
fn it_divides_from_the_outside() {
    let mut calc = Calculator::new();
    calc.add(10.0);
    assert_eq!(calc.divide(2.0), Ok(5.0));
}

#[test]
fn it_rejects_the_zero_division() {
    let mut calc = Calculator::new();
    calc.add(10.0);
    assert!(calc.divide(0.0).is_err());
}


// ============================================
// PART 3: THE SHARED UTILITY MODULE
// ============================================

// tests/common/mod.rs
use my_project::Calculator;

pub fn make_calculator() -> Calculator {
    let mut calc = Calculator::new();
    calc.add(100.0);
    calc
}

pub fn assert_close(a: f64, b: f64) {
    assert!(
        (a - b).abs() < 1e-9,
        "the expected {} but got {}",
        b,
        a,
    );
}


// ============================================
// PART 4: THE TEST WITH THE SHARED UTILITY
// ============================================

// tests/shared_test.rs
mod common;

use common::{make_calculator, assert_close};
use my_project::Calculator;

#[test]
fn it_uses_the_shared() {
    let mut calc = make_calculator();
    calc.add(50.0);
    assert_close(calc.memory(), 150.0);
}

#[test]
fn it_starts_from_the_new() {
    let calc = Calculator::new();
    assert_close(calc.memory(), 0.0);
}


// ============================================
// PART 5: THE SETUP PATTERN
// ============================================

use std::sync::Once;

static INIT: Once = Once::new();

fn setup() {
    INIT.call_once(|| {
        std::env::set_var("CALC_PRECISION", "high");
    });
}

#[test]
fn it_uses_the_environment() {
    setup();
    assert_eq!(std::env::var("CALC_PRECISION").unwrap(), "high");
}


// ============================================
// PART 6: THE TEST CONTEXT
// ============================================

// tests/common/mod.rs
pub struct TestContext {
    pub calc: Calculator,
}

impl TestContext {
    pub fn new() -> Self {
        let mut calc = Calculator::new();
        calc.add(1000.0);
        TestContext { calc }
    }
}

// tests/context_test.rs
mod common;

use common::TestContext;

#[test]
fn it_uses_the_context() {
    let mut context = TestContext::new();
    context.calc.add(500.0);
    assert_eq!(context.calc.memory(), 1500.0);
}


// ============================================
// PART 7: THE RESULT RETURN
// ============================================

// tests/result_test.rs
use my_project::Calculator;

#[test]
fn it_handles_the_result() -> Result<(), Box<dyn std::error::Error>> {
    let mut calc = Calculator::new();
    calc.add(10.0);
    let result = calc.divide(2.0)?;
    assert_eq!(result, 5.0);
    Ok(())
}


// ============================================
// PART 8: THE IGNORED TEST
// ============================================

// tests/expensive_test.rs
use my_project::Calculator;

#[test]
#[ignore = "the expensive"]
fn it_is_the_expensive() {
    let mut calc = Calculator::new();
    for i in 0..1_000_000 {
        calc.add(i as f64);
    }
    assert!(calc.memory() > 0.0);
}


// ============================================
// PART 9: THE COMMANDS
// ============================================

// cargo test                            → all the tests
// cargo test --test calculator_test     → the specific file
// cargo test --test-threads=1           → the serial
// cargo test -- --ignored               → the ignored
// cargo test --doc                      → the doc tests


// ============================================
// PART 10: THE COMPLETE PROJECT
// ============================================

// my_project/
// ├── src/
// │   └── lib.rs          ← the Calculator + the unit tests
// ├── tests/
// │   ├── common/
// │   │   └── mod.rs      ← the shared utilities
// │   ├── calculator_test.rs
// │   ├── shared_test.rs
// │   ├── context_test.rs
// │   ├── result_test.rs
// │   └── expensive_test.rs
// └── Cargo.toml

// The cargo test runs:
//   1. The unit tests in the src/
//   2. The integration tests in the tests/
//   3. The documentation tests in the doc comments

The ten parts show the library, the basic integration test, the shared utility module, the test with the shared utility, the setup pattern, the test context, the Result return, the ignored test, the commands, and the complete project.


Quick Reference

The Directory Structure

The pathThe purpose
src/lib.rsThe library
src/*.rs (the #[cfg(test)])The unit tests
tests/*.rsThe integration tests
tests/common/mod.rsThe shared utilities
Cargo.tomlThe project manifest

The Test Attributes

The attributeThe purpose
#[test]The test function
#[tokio::test]The async test
#[ignore]The skipped test
#[should_panic]The must panic

The Commands

The commandThe purpose
cargo testAll the tests
cargo test --test nameThe specific file
cargo test --libThe unit tests
cargo test --docThe doc tests
cargo test -- --ignoredThe ignored
cargo test -- --test-threads=1The serial
cargo test -- --nocaptureThe show output

The Setup Patterns

The patternThe use case
The OnceThe one-time global setup
The TestContextThe per-test resource
The DropThe automatic cleanup
The #[ignore]The expensive tests
The --test-threads=1The shared state

The Shared Utilities

The fileThe import
tests/common/mod.rsmod common;
The public functionsuse common::foo;
The public structsuse common::TestContext;

Best Practices

✅ Do This:

// Use the tests/ directory for the integration
// tests/integration_test.rs                                            // ✅
// Use the common/mod.rs for the shared utilities
// tests/common/mod.rs                                                  // ✅
// Use the descriptive names
fn it_returns_the_user_when_the_found() { ... }                         // ✅
// Use the Result return for the fallible
fn it_works() -> Result<(), Box<dyn Error>> { ... }                     // ✅
// Use the #[ignore] for the expensive
#[ignore = "the requires the database"]                                 // ✅
// Use the --test-threads=1 for the shared state
// cargo test -- --test-threads=1                                       // ✅
// Use the TestContext for the setup and the cleanup
let context = TestContext::new();                                       // ✅

❌ Don’t Do This:

// Don't put the unit tests in the tests/ directory
// The tests/ is the integration, the src/ is the unit               // ❌
// Don't use the private API in the integration
use my_project::internal_function;  // the private                     // ❌
// Don't test the multiple concepts in one test
fn it_creates_and_sends_and_logs() { ... }  // the unclear             // ❌
// Don't forget the cleanup
let context = TestContext::new();  // the leak                         // ❌
// Don't run the expensive tests by default
#[test] fn it_is_the_slow() { ... }  // the slow                       // ❌
// Don't share the state without the serial
// cargo test (the parallel)                                            // ❌
// Don't use the unwrap in the integration without the message
let user = client.create().unwrap();  // the unclear                  // ❌

Common Pitfalls

The pitfallThe whyThe fix
The test cannot access the privateThe external crateUse the unit test
The import errorThe missing mod commonAdd the declaration
The shared state conflictThe parallel executionUse the --test-threads=1
The resource leakThe missing cleanupUse the Drop
The test is the slowThe no #[ignore]Add the attribute
The common/mod.rs is treated as the testThe wrong filenameUse the mod.rs
The async test failsThe missing #[tokio::test]Add the attribute

Real-World Examples

1. The Basic Integration Test

// tests/calculator_test.rs
use my_project::Calculator;
#[test]
fn it_adds() { ... }

2. The Shared Module

// tests/common/mod.rs
pub fn make_calculator() -> Calculator { ... }

3. The Test with the Shared

mod common;
use common::make_calculator;

4. The Setup

static INIT: Once = Once::new();
INIT.call_once(|| { ... });

5. The Test Context

pub struct TestContext { pub calc: Calculator }

6. The Result Return

fn it_works() -> Result<(), Box<dyn Error>> { ... }

7. The Ignored

#[ignore = "the expensive"]

8. The Async

#[tokio::test]
async fn it_works() { ... }

9. The Specific File

cargo test --test calculator_test

10. The Complete

cargo test
cargo test --test calculator_test
cargo test -- --ignored

Visual

The Integration Test Organization

┌──────────────────────────────────────────────────────────────┐
│  THE INTEGRATION TEST ORGANIZATION                           │
│                                                              │
│  my_project/                                                 │
│  ├── src/                                                    │
│  │   └── lib.rs                                              │
│  │       ├── the public API                                  │
│  │       └── #[cfg(test)] mod tests  ← the unit tests        │
│  │                                                           │
│  ├── tests/                                                  │
│  │   ├── common/                                             │
│  │   │   └── mod.rs               ← the shared utilities     │
│  │   ├── calculator_test.rs       ← the crate 1              │
│  │   ├── shared_test.rs           ← the crate 2              │
│  │   └── context_test.rs          ← the crate 3              │
│  │                                                           │
│  └── Cargo.toml                                              │
│                                                              │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  Each .rs file in the tests/ is a separate crate.    │    │
│  │  Each crate depends on the library.                  │    │
│  │  Each crate can only access the public API.          │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
└──────────────────────────────────────────────────────────────┘

The Unit vs the Integration

┌──────────────────────────────────────────────────────────────┐
│  THE UNIT vs THE INTEGRATION                                 │
│                                                              │
│  THE UNIT TEST:                                              │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  The location: the src/ with the #[cfg(test)]        │    │
│  │  The access: the private and the public              │    │
│  │  The speed: the fast                                 │    │
│  │  The scope: the function                             │    │
│  │  The count: the many                                 │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  THE INTEGRATION TEST:                                       │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  The location: the tests/ directory                  │    │
│  │  The access: the public only                         │    │
│  │  The speed: the slower                               │    │
│  │  The scope: the public API                           │    │
│  │  The count: the fewer                                │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  The two are the complements.                                │
│                                                              │
└──────────────────────────────────────────────────────────────┘

The Test Context

┌──────────────────────────────────────────────────────────────┐
│  THE TEST CONTEXT                                            │
│                                                              │
│  pub struct TestContext {                                    │
│      pub client: Client,                                     │
│      pub database: Database,                                 │
│  }                                                           │
│                                                              │
│  impl TestContext {                                          │
│      pub async fn new() -> Self {                            │
│          let database = Database::connect().await;           │
│          let client = Client::new(&database);                │
│          TestContext { client, database }                    │
│      }                                                       │
│  }                                                           │
│                                                              │
│  impl Drop for TestContext {                                 │
│      fn drop(&mut self) {                                    │
│          // the cleanup                                       │
│      }                                                       │
│  }                                                           │
│                                                              │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  The test:                                           │    │
│  │    let context = TestContext::new().await;           │    │
│  │    let user = context.client.create_user("Alice");   │    │
│  │    assert_eq!(user.name, "Alice");                   │    │
│  │    // the Drop runs the cleanup                      │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
└──────────────────────────────────────────────────────────────┘

The Cargo Test Discovery

┌──────────────────────────────────────────────────────────────┐
│  THE CARGO TEST DISCOVERY                                    │
│                                                              │
│  cargo test                                                  │
│    │                                                         │
│    ├── The unit tests                                        │
│    │     The #[cfg(test)] modules in the src/                │
│    │                                                         │
│    ├── The integration tests                                 │
│    │     The .rs files in the tests/                         │
│    │     Each file is the separate crate                     │
│    │                                                         │
│    └── The documentation tests                               │
│          The code examples in the /// comments               │
│                                                              │
│  The output:                                                 │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  running 3 tests (the src/lib.rs)                    │    │
│  │  test tests::it_adds ... ok                          │    │
│  │                                                       │    │
│  │  running 4 tests (the tests/calculator_test.rs)      │    │
│  │  test it_adds ... ok                                 │    │
│  │                                                       │    │
│  │  running 2 tests (the tests/shared_test.rs)          │    │
│  │  test it_uses_the_shared ... ok                      │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
tests/The integration test directory
The separate crateEach .rs file
The public APIThe only access
tests/common/mod.rsThe shared utilities
cargo testRuns all the tests
cargo test --test nameThe specific file
cargo test -- --ignoredThe ignored tests
cargo test -- --test-threads=1The serial
The TestContextThe setup and the cleanup
The OnceThe one-time setup

Key takeaways:

  • The integration tests live in the tests/ directory, and each file is a separate crate. The test uses the library as the external consumer, and it can only access the public API. The integration tests verify the public contract, and the unit tests verify the internals .
  • The cargo test discovers the .rs files in the tests/ directory. The tests/common/mod.rs is the shared module, and the cargo test does not treat it as the separate test file. The cargo test --test name runs the specific file .
  • The integration tests use the same #[test] and assertions. The difference is the scope: the integration test sees the public API, and the unit test sees the internals. The use my_project::function imports the public function .
  • The setup and the teardown use the Once and the TestContext. The Once runs the one-time global setup, and the TestContext creates the per-test resource and cleans up with the Drop. The RAII pattern ensures the cleanup even when the test panics .
  • The #[ignore] skips the expensive tests. The cargo test skips them, and the cargo test -- --ignored runs them. The CI runs the ignored tests in the separate job .
  • The --test-threads=1 runs the tests serially. The integration tests that share the global state (the database, the file system) must run serially. The alternative is the separate resource per test .
  • The Result return allows the ? operator. The integration test can return the Result<(), E>, and the ? propagates the error. The test fails with the clear message when the Result is the Err .
  • The integration tests are the black-box, and the unit tests are the white-box. The two are the complements, and the cargo test runs both. The integration tests are the fewer and the public-API-focused, and the unit tests are the many and the internal-focused .

Remember: The integration tests live in the tests/ directory, and each file is a separate crate that depends on the library. The integration tests can only access the public API, and they verify the library as the consumer uses it. The cargo test discovers the files, and the cargo test --test name runs the specific one. The tests/common/mod.rs is the shared utilities, and the TestContext is the setup and the cleanup. The result is the verified public contract, which is the foundation of the reliable library.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!