Unit test conventions

Table of Contents

ORE Studio's Catch2 unit tests follow consistent file structure, naming, and test-case shape. The code-add-tests skill drives their authoring; this doc is the durable reference for the conventions every reviewer also checks (see Code review checklist§"Unit tests"). For database-integration specifics see Unit Test Database Integration Guide inside ores.testing. Return to Knowledge.

File organisation

Test files live under projects/<component>/tests/ with the naming pattern <layer>_<class>_tests.cpp. Common layers:

Layer Example file
domain domain_account_tests.cpp
repository repository_account_repository_tests.cpp
service service_account_service_tests.cpp
security security_session_tests.cpp
messaging messaging_account_protocol_tests.cpp
generators generators_account_generator_tests.cpp

Standard file structure

Every test file opens with the GPL header, includes the system under test, includes Catch2, declares the suite + tag constants in an anonymous namespace, then defines TEST_CASE blocks:

/* GPL copyright header */

#include "COMPONENT/LAYER/CLASS.hpp"

#include <catch2/catch_test_macros.hpp>
// Additional includes as needed

namespace {

const std::string test_suite("COMPONENT.tests");
const std::string tags("[LAYER]");

}

TEST_CASE("name_describing_behaviour", tags) {
    // ...
}

Includes by test category

Category Required additions
Domain <faker-cxx/faker.h>, ores.logging/make_logger.hpp,
  <component>/domain/<class>_json_io.hpp
Repository <boost/uuid/uuid_io.hpp>, ores.logging/make_logger.hpp,
  <component>/generators/<class>_generator.hpp,
  ores.testing/database_helper.hpp
Messaging ores.logging/make_logger.hpp,
  <component>/messaging/<protocol>_protocol.hpp, generators
Generators ores.logging/make_logger.hpp,
  <component>/generators/<class>_generator.hpp

Test-case naming

Use descriptive snake_case names following these verb patterns:

Verb pattern Use for
create_<type>_with_valid_fields Basic construction.
<type>_serialization_to_json JSON serialization.
create_<type>_with_faker Faker-generated data.
write_single_<type> Repository write.
read_latest_<type> Repository read (current).
read_<type>_by_<field> Repository filtered read.
<type>_serialize_deserialize Protocol roundtrip.

Test-case shape

Every test case shares the same skeleton:

TEST_CASE("name", tags) {
    auto lg(make_logger(test_suite));

    // Setup (database_helper, scoped_database_helper, etc.)

    // Build system under test
    type sut;
    // ... initialise fields, or use a generator

    BOOST_LOG_SEV(lg, info) << "Description: " << sut;

    CHECK(sut.field == expected);
    CHECK_NOTHROW(operation());
}

Tags

Tag When to use
[domain] Domain entity tests.
[repository] Database / repository tests.
[service] Service-layer tests.
[security] Security-related tests.
[messaging] Protocol serialize / deserialize tests.
[generators] Synthetic data generator tests.

Generators

When a generator exists for a type, use it instead of hand-built construction:

#include "<component>/generators/<class>_generator.hpp"
using namespace ores::<component>::generators;

auto entity = generate_synthetic_<type>();          // single
auto entities = generate_synthetic_<types>(5);      // batch

Test that generators themselves produce valid entities under the [generators] tag.

Database helpers

Two helpers in ores.testing:

Helper When to use
database_helper Original pattern. Some components (DQ, IAM) pass
  h.context() to the repository constructor.
scoped_database_helper Newer per-test tenant-isolated pattern; no table
  truncation needed. Refdata uses this.
// database_helper style
database_helper h;
type_repository repo(h.context());

// scoped_database_helper style
scoped_database_helper h;
type_repository repo;
repo.write(h.context(), entity);

Best practices

  • Log every test caseBOOST_LOG_SEV(lg, info) << sut; helps diagnose failures from logs alone.
  • CHECK vs REQUIRECHECK for non-fatal assertions, REQUIRE when the rest of the case cannot run if this fails.
  • Test both happy and edge paths — but as separate TEST_CASE blocks.
  • No Catch2 SECTION — separate TEST_CASE for each scenario instead. SECTIONs hide which sub-case failed in the logs.
  • str.contains() over find() ! npos= (C++23).

See also

Emacs 29.1 (Org mode 9.6.6)