Test Organization

The Oxide and Rust community thinks about tests in terms of two main categories: unit tests and integration tests. Unit tests are small and focused, testing one module in isolation and can test private interfaces. Integration tests are external to your library and use your code the same way external code would, using only the public interface.

Unit Tests

The purpose of unit tests is to test each unit of code in isolation to quickly pinpoint where code is or isn't working as expected. Unit tests go in the src directory in each file with the code they're testing.

The Tests Module and #[cfg(test)]

The #[cfg(test)] annotation tells Oxide to compile and run the test code only when you run cargo test, not when you run cargo build. This saves compile time and reduces the binary size.

public fn add(left: UIntSize, right: UIntSize): UIntSize {
    left + right
}

#[cfg(test)]
module tests {
    import super.*

    #[test]
    fn itWorks() {
        let result = add(2, 2)
        assertEq!(result, 4)
    }
}

The #[cfg(test)] attribute on the tests module means:

  • The module is only compiled during cargo test
  • All code inside is test-only, including helper functions
  • The module is stripped from release builds

Testing Private Functions

Unlike some languages, Oxide allows you to test private functions directly. Since tests are in the same file as the code, they have access to private items:

fn internalAdd(a: Int, b: Int): Int {
    a + b
}

public fn publicAdd(a: Int, b: Int): Int {
    internalAdd(a, b)
}

#[cfg(test)]
module tests {
    import super.*

    #[test]
    fn testInternalAdd() {
        // We can test the private function directly
        assertEq!(internalAdd(2, 3), 5)
    }

    #[test]
    fn testPublicAdd() {
        assertEq!(publicAdd(2, 3), 5)
    }
}

Whether you choose to test private functions is a matter of opinion. Oxide makes it possible, but you're not required to do so.

Organizing Unit Tests

For larger modules, you might organize tests into submodules:

public struct Calculator {
    value: Int,
}

extension Calculator {
    public static fn new(): Calculator {
        Calculator { value: 0 }
    }

    public fn add(amount: Int): Int {
        self.value + amount
    }

    public fn multiply(factor: Int): Int {
        self.value * factor
    }
}

#[cfg(test)]
module tests {
    import super.*

    module additionTests {
        import super.*

        #[test]
        fn addsPositiveNumbers() {
            let calc = Calculator { value: 5 }
            assertEq!(calc.add(3), 8)
        }

        #[test]
        fn addsNegativeNumbers() {
            let calc = Calculator { value: 5 }
            assertEq!(calc.add(-3), 2)
        }
    }

    module multiplicationTests {
        import super.*

        #[test]
        fn multipliesPositiveNumbers() {
            let calc = Calculator { value: 5 }
            assertEq!(calc.multiply(3), 15)
        }

        #[test]
        fn multipliesByZero() {
            let calc = Calculator { value: 5 }
            assertEq!(calc.multiply(0), 0)
        }
    }
}

Integration Tests

Integration tests are entirely external to your library. They use your library in the same way any other code would, which means they can only call public functions. Their purpose is to test that multiple parts of your library work together correctly.

The tests Directory

To create integration tests, create a tests directory at the top level of your project, next to src:

adder/
  Cargo.toml
  src/
    lib.ox
  tests/
    integration_test.ox

Let's create tests/integration_test.ox:

import adder

#[test]
fn itAddsTwo() {
    assertEq!(4, adder.add(2, 2))
}

Key differences from unit tests:

  1. No #[cfg(test)] needed - Cargo knows the tests directory contains tests
  2. External perspective - We import our crate with import adder
  3. Public API only - We can only use public functions and types

Run integration tests with:

cargo test --test integration_test

Or run all tests:

cargo test

Output:

running 1 test
test tests.itWorks ... ok

     Running tests/integration_test.ox (target/debug/deps/integration_test-...)

running 1 test
test itAddsTwo ... ok

test result: ok. 1 passed; 0 failed

Each file in tests is compiled as a separate crate.

Submodules in Integration Tests

As you add more integration tests, you might want to organize them. Each file in tests compiles as its own crate, so they don't share behavior like modules in src.

For shared helper code, create a subdirectory with a mod.ox file:

tests/
  common/
    mod.ox
  integration_test.ox

In tests/common/mod.ox:

public fn setup(): String {
    // Setup code that might be needed by multiple tests
    "test_database".toString()
}

public struct TestConfig {
    public name: String,
    public debug: Bool,
}

extension TestConfig {
    public static fn default(): TestConfig {
        TestConfig {
            name: "test".toString(),
            debug: true,
        }
    }
}

In tests/integration_test.ox:

external module common

import adder
import crate.common.{ setup, TestConfig }

#[test]
fn itAddsTwo() {
    let _config = TestConfig.default()
    let _db = setup()
    assertEq!(4, adder.add(2, 2))
}

#[test]
fn itAddsLargeNumbers() {
    assertEq!(1000000002, adder.add(1000000000, 2))
}

Files in subdirectories of tests don't get compiled as separate test crates. The common/mod.ox pattern prevents Cargo from treating common as a test file while allowing other tests to import it.

Integration Tests for Binary Crates

If your project only contains a src/main.ox and no src/lib.ox, you can't create integration tests in the tests directory and import functions with import cratename.

This is one reason Oxide projects with a binary have a straightforward src/main.ox that calls logic in src/lib.ox. The library can be tested with integration tests while the main file remains minimal.

Multiple Integration Test Files

For larger projects, organize integration tests by feature:

tests/
  common/
    mod.ox
  api_tests.ox
  database_tests.ox
  user_tests.ox

Each file runs as its own test suite. Run a specific one:

cargo test --test api_tests

Test Organization Best Practices

Unit Test Guidelines

  1. Keep tests close to code - Tests in the same file as the code they test
  2. Test one thing - Each test should verify a single behavior
  3. Use descriptive names - testAddWithNegativeNumbers not test1
  4. Arrange-Act-Assert - Structure tests clearly
#[test]
fn userCanChangeName() {
    // Arrange
    var user = User.new("Alice")

    // Act
    user.setName("Bob")

    // Assert
    assertEq!(user.name, "Bob")
}

Integration Test Guidelines

  1. Test the public interface - Don't try to access private internals
  2. Test realistic scenarios - Combine operations as users would
  3. Share setup code - Use the common module pattern
  4. One concern per file - Organize by feature or subsystem

When to Use Each

Test TypeUse For
Unit testsIndividual functions, edge cases, error handling
Integration testsFeature workflows, API contracts, module interactions

Documentation Tests

Oxide also runs code examples in documentation comments as tests:

/// Adds two numbers together.
///
/// # Examples
///
/// ```oxide
/// let result = adder.add(2, 2)
/// assertEq!(result, 4)
/// ```
public fn add(left: UIntSize, right: UIntSize): UIntSize {
    left + right
}

Run documentation tests with:

cargo test --doc

Documentation tests ensure your examples stay correct as code evolves.

Summary

Oxide's testing features help you write and organize tests effectively:

Unit Tests:

  • Placed in #[cfg(test)] modules alongside code
  • Can test private functions
  • Fast to compile and run
  • Use for isolated, focused tests

Integration Tests:

  • Placed in the tests directory
  • Use your library as an external consumer would
  • Test only the public API
  • Use for testing feature workflows

Best Practices:

  • Keep tests close to the code they test
  • Test one behavior per test
  • Use descriptive test names
  • Share setup code through common modules
  • Run tests frequently during development

Testing is a skill that improves with practice. Start with simple tests and grow your test suite as your codebase grows.