Skip to content

11. Testing your components

Before trusting search results, check that moves preserve valid solutions and deltas agree with full evaluation. <easylocal/testing.hpp> supplies these checks so you can run them in your test suite. They share a fixture containing a small Input, a valid Solution and its SolutionManager:

namespace elt = easylocal::testing;
// Not the identity tour: on 0, 1, 2, 3, 4 position k holds city k, and a
// delta that mixes up positions and cities would still be right.
const elt::fixture<tutorial::TourManager> tsp{
    tutorial::five_cities(),
    tutorial::Tour{{0, 2, 4, 1, 3}},
};

Each check names the type under test and builds it from the fixture, as an app would: a component or a delta cost component from the Input, a NeighborhoodExplorer from the SolutionManager.

using namespace tutorial;
return elt::run_checks(
    elt::check_solution_manager(tsp),
    elt::check_cost_component<TourLength>(tsp),
    elt::check_neighborhood<SwapExplorer>(tsp),
    elt::check_neighborhood<TwoOptExplorer>(tsp),
    elt::check_delta_cost_component<TwoOptExplorer, TourLength, TwoOptLengthDelta>(
        tsp),
    // No delta cost component: the check uses the component's own delta_evaluate.
    elt::check_delta_cost_component<TwoOptExplorer, TourLengthWithDelta>(tsp));
Check Verifies
check_solution_manager(f) the fixture, initial and random solutions are valid
check_cost_component<Component>(f) evaluating the same solution twice gives equivalent values
check_neighborhood<NHE>(f) enumerated and sampled moves are valid, keep the solution valid and change it; random moves are enumerated ones, drawn from the generator given
check_delta_cost_component<NHE, Component, Delta>(f) value + delta equals the full re-evaluation after each valid move

Move checks use both the fixture's solution and random solutions followed by random walks. This helps catch deltas that work on one tour but fail on another.

The fixture deliberately uses a shuffled tour. On 0, 1, 2, 3, 4, city identifiers equal their positions, so a delta could incorrectly use distance[i][j] instead of distance[order[i]][order[j]] and still pass.

Failures identify the move, starting solution and disagreeing values. The report prints up to three failures per check. A hook that throws fails its check without stopping the remaining checks.

For a co-located delta (chapter 5) omit the third type, as for TourLengthWithDelta above: the check then uses the component's own delta_evaluate.

fixture<SM>{input} uses the initial solution when none is supplied. A third argument controls sampling, random walks and seed, for example {.random_samples = 16, .max_enumerated_moves = 256, .random_solutions = 4, .walk_length = 8, .seed = 7}.

Comparisons use testing::approximately, with relative and absolute floating-point tolerances of 1e-9 by default. This allows for rounding drift from incremental updates. Other values are compared exactly. Set {.tolerance = {.relative = 0, .absolute = 0}} for exact comparison, or provide a second fixture template argument to replace the comparison policy.

If a component needs more than the Input to construct it, pass an object: check_cost_component(f, TourLength{...}). Explorers and deltas support the same form. Include testing.hpp in your test executable; the Core umbrella provides the options and reports for check(app), but not these individual checks.

For bugs that appear only during a longer search, enable EASYLOCAL_VERIFY_DELTAS in a debug build. It compares incremental and full evaluation after every committed move.

For a whole composed problem, easylocal::check(app, input) runs the same checks against an app (chapter 15).

See also

Next steps

Chapter 12 packages the problem as an application.