Unit Testing Principles

To build robust, reliable simulations, we must move beyond manual observation and implement automated validation. This module covers the implementation of unit testing within the MineTwin framework, ensuring your code remains stable as the project scales.

1. Introduction to Unit Testing

The "Why": When writing complex simulation logic, manual testing is inefficient and prone to human error. We need automated ways to ensure our code behaves correctly—not just today, but months from now when other developers are modifying the codebase.

Unit tests act as evaluations of small, specific pieces of functionality. They serve as an automated "safety net."

Comparison of a real system’s interconnected units and isolated unit tests with mocks, using a puzzle analogy.

MineTwin comes loaded with a template testing class located at tests > com.~.tests > src > com.~.tests > Test.java.

This can help you get started quickly.

Java code snippet with JUnit test and methods for loading scenarios and running a model.

1.1. Basic Test: Counting Mine Nodes

Let’s start by adding a basic test that simply loads a scenario and counts the mine nodes to ensure the environment is initializing correctly.

import java.io.File;

@org.junit.jupiter.api.Test
void testNodeCount() {
    var scenario = loadScenario("scenarios" + File.seperatorChar + "Basic.xlsx");
    System.out.println(scenario.getMineNodes().size());
}

Why use File.separatorChar instead of an explicit file path?

Different operating systems use different characters to separate directories in a file path (Windows uses \, while Linux and macOS use /). By using File.separatorChar, your code automatically detects the correct separator for the machine it is running on, making your simulation project truly portable. Don’t forget to include the import java.io.File; statement at the top of your class!

To execute this, right-click on the test class in your side-bar/Project Explorer, navigate to Run As, and select JUnit Test.

Context menu showing \"Run As\" options with \"JUnit Test\" highlighted in green.

Check your Console view at the bottom of the screen. This basic test simply prints the output, so you should see the value 163 printed to the console.

Eclipse IDE console view showing terminated JUnit test output and a line with the number 163.

1.2. Advanced Test: Validating Outcomes

Now, let’s look at a proper unit test that actually checks results rather than just printing them. This test will run the model, retrieve values, and check them against predetermined expected values.

@org.junit.jupiter.api.Test
void testOre() {
    // Arrange
    var scenario = loadScenario(".\\scenarios\\Basic.xlsx");
    EYFTrainingModel model = new EYFTrainingModel(scenario, engine, new Mapping(), false, 0);

    // Act
    runModel();

    // Assert
    assertTrue(engine.getExceptions().isEmpty());
    assertEquals(1_800_000, model.getMineTwinResult().getOrThrow(MineTwinResult.PRODUCTION_TONS), 0.1);
}

Currently, we are checking production tons, but there are many possible parameters you can check.

Code editor with autocomplete suggestions for string variables in the MineTwinResult function.

Run both of these tests again with JUnit and monitor the results. After about 30 seconds (depending on your machine), you should see both tests pass with a green bar in the JUnit view!

JUnit test results: 2 runs, 0 errors, 0 failures. Test times: testOre 30.464s, testNodeCount 0.660s, total 31.274s.

2. Unit Testing Fundamentals

Proper unit testing relies on checking simulation results against predetermined expected values using assertions. In the MineTwin environment, we utilize the standard JUnit 5 framework.

2.1. Core Assertions

  • assertEquals(expected, actual, delta): Checks if two values match perfectly. The delta parameter defines the acceptable variance (useful for small rounding differences).

  • assertTrue(condition): Checks if a given boolean condition evaluates to true.

The Floating-Point Challenge: Assertions and Debugging

Because MineTwin simulates continuous variables and uses floating-point numbers, math operations often introduce tiny rounding discrepancies (e.g., 1.800000000001 vs 1.8).

To handle this, assertTrue is frequently wrapped around a custom Compare.equalTo function. This allows for tiny micro-tolerances in simulation rounding, ensuring your tests don’t fail due to insignificant mathematical artifacts.

Crucial Debugging Tip:

When choosing an assertion, consider how it reports failures:

  • assertTrue(Compare.equalTo(…​)): This simply tells you that the condition was false. It does not show you the specific values that caused the failure.

  • assertEquals(expected, actual, delta): This is more informative. If this fails, JUnit will explicitly show you the discrepancy—for example, it will clearly state that it expected 20 but received 10.

Use assertEquals whenever possible to make your test failures easier to debug!

3. Practical Exercises

Each of the following exercises follows the exact same structure (load the scenario, run the engine, assert the final state) but targets a different component of the MineTwin model.

For these exercises, you should run the model first to find the expected values in the UI, and then put those values into your assertions.

Note: While you wouldn’t always do this in real-world test-driven development, it is the easiest way to learn the syntax right now.

Exercise Task Description Notes

Option 1: Dump Area Amount Test

Check that all dump areas have the correct amount of material at the end of the simulation run.

- Use the standard scenario: loadScenario(".\\scenarios\\Basic.xlsx")

- Test for the amounts at the end of the simulation in each dump area.

- Hint: You will need to import the DumpArea classes.

Option 2: Truck Stats Test

Verify specific statistics for "Truck 1".

- Use the standard scenario: loadScenario(".\\scenarios\\Basic.xlsx")

- Test for the Scheduled time, Utilization, and Total Hauled Amount

- Hint: You need to import the haulage truck type and use the Truck class, not the generic Equipment class.

Option 3: Mine Area Test

Verify that restricting trucks to a specific mine area successfully prevents material from reaching incorrect dump areas.

- Open the app, load the standard scenario, and manually set all trucks to use the "Purple" Mine area. (You can remove all but 4 trucks for convenience).

- Save this modified scenario as a new scenario file under tests > com.~.tests > scenarios named MineAreaTest.xlsx.

- Use this new scenario for your unit test.

- Test for the amounts at the end of the simulation in each dump area (checking that the restricted areas received nothing).

Click here to view the Solution Code
Option 1: Dump Area Amount Test
import com.amalgamasimulation.minetwin.openpit.simulation.DumpArea;

@org.junit.jupiter.api.Test
void testDumpAreaAmount() {
    var scenario = loadScenario(".\\scenarios\\Basic.xlsx");

    EYFTrainingModel model = new EYFTrainingModel(scenario, engine, new Mapping(), false, 0);
    runModel();

    assertTrue(engine.getExceptions().isEmpty());
    assertTrue(Compare.equalTo(model.getDumpAreas().get(0).getBunker().getContentsAmount(), 2400000));
    assertTrue(Compare.equalTo(model.getDumpAreas().get(1).getBunker().getContentsAmount(), 1600140));
    assertTrue(Compare.equalTo(model.getDumpAreas().get(2).getBunker().getContentsAmount(), 1800000));
}
Option 2: Truck Stats Test
import com.amalgamasimulation.minetwin.openpit.simulation.haulage.Truck;

@org.junit.jupiter.api.Test
void testTruckStats() {
    var scenario = loadScenario(".\\scenarios\\Basic.xlsx");

    EYFTrainingModel model = new EYFTrainingModel(scenario, engine, new Mapping(), false, 0);
    runModel();

    Truck truck = model.getTrucks().get(0);

    assertTrue(engine.getExceptions().isEmpty());
    assertEquals(620, truck.scheduledTime(), 1);
    assertEquals(0.91, truck.utilization(), 0.5);
    assertEquals(175740, truck.getBunker().getTotalOutflowAmount(), 10);
}
Option 3: Mine Area Test
import com.amalgamasimulation.minetwin.openpit.simulation.DumpArea;

@org.junit.jupiter.api.Test
void testMineAreas() {
    var scenario = loadScenario(".\\scenarios\\MineAreaTest.xlsx");

    EYFTrainingModel model = new EYFTrainingModel(scenario, engine, new Mapping(), false, 0);
    runModel();

    assertTrue(engine.getExceptions().isEmpty());
    assertEquals(781800, model.getDumpAreas().get(0).getBunker().getContentsAmount(), 100);
    assertEquals(0, model.getDumpAreas().get(1).getBunker().getContentsAmount(), 0.1);
    assertEquals(0, model.getDumpAreas().get(2).getBunker().getContentsAmount(), 0.1);
}