userver: Unit Tests and Benchmarks
Loading...
Searching...
No Matches
Unit Tests and Benchmarks

Unit tests (gtest)

Getting started

userver test helpers live in userver::utest CMake target:

target_link_libraries(your-test-target PRIVATE userver::utest)

When being limited to userver::universal, you can use a subset of the helpers that don't require coroutine environment via userver::universal-utest CMake target:

target_link_libraries(your-test-target PRIVATE userver::universal-utest)

To include gtest and userver-specific macros, do:

As usual, gmock is available in <gmock/gmock.h>.

Unit tests with coroutine environment

utest.hpp header provides alternative gtest-like macros that run tests in a coroutine environment:

To test code that uses coroutine environment (e.g. creates tasks or uses synchronization primitives), use UTEST instead of TEST in the test header:

UTEST(Semaphore, PassAcrossCoroutines) {
std::shared_lock guard{s};
auto task = utils::Async("test", [guard = std::move(guard)] {});
task.WaitFor(utest::kMaxTestWaitTime);
EXPECT_TRUE(task.IsFinished());
}

There are U-versions for all gtest macros that declare tests and test groups: UTEST_F, UTEST_P, INSTANTIATE_UTEST_SUITE_P etc. Test fixture's constructor, destructor, SetUp and TearDown are executed in the same coroutine environment as the test body.

By default, a single-threaded TaskProcessor is used. It is usually enough, because userver engine enables concurrency even on 1 thread. However, some tests require actual parallelism, e.g. torture tests that search for potential data races. In such cases, use _MT macro versions:

UTEST_MT(SemaphoreLock, LockMoveCopyOwning, 2) {
ASSERT_TRUE(lock.OwnsLock());
engine::SemaphoreLock move_here{std::move(lock)};
// NOLINTNEXTLINE(bugprone-use-after-move,clang-analyzer-cplusplus.Move)
EXPECT_FALSE(lock.OwnsLock());
EXPECT_TRUE(move_here.OwnsLock());
}

The specified thread count is available in U-tests as GetThreadCount() method.

For DEATH-tests (when testing aborts or assertion fails) use UTEST_DEATH. It configures gtest-DEATH-checks to work in multithreaded environment. Also it disables using of ev_default_loop and catching of SIGCHLD signal to work with gtest's waitpid() calls.

Exception assertions

Standard gtest exception assertions provide poor error messages. Their equivalents with proper diagnostics are available in <userver/utest/assert_macros.hpp> (which gets pulled in by <userver/utest/utest.hpp> automatically):

Example usage:

void ThrowingFunction() { throw std::runtime_error("The message"); }
void NonThrowingFunction() {}
TEST(AssertMacros, Sample) {
UEXPECT_THROW_MSG(ThrowingFunction(), std::runtime_error, "message");
UEXPECT_THROW(ThrowingFunction(), std::runtime_error);
UEXPECT_THROW(ThrowingFunction(), std::exception);
UEXPECT_NO_THROW(NonThrowingFunction());
}

Exception assertions

Mocked time

class Timer final {
public:
// Starts the next loop and returns time elapsed on the previous loop
std::chrono::system_clock::duration NextLoop() {
const auto now = utils::datetime::Now();
const auto elapsed = now - loop_start_;
loop_start_ = now;
return elapsed;
}
private:
std::chrono::system_clock::time_point loop_start_{utils::datetime::Now()};
};
TEST(MockNow, Timer) {
utils::datetime::MockNowSet(Stringtime("2000-01-01T00:00:00+0000"));
Timer timer;
utils::datetime::MockSleep(9001s); // does NOT sleep, just sets the new time
EXPECT_EQ(timer.NextLoop(), 9001s);
utils::datetime::MockNowSet(Stringtime("2000-01-02T00:00:00+0000"));
EXPECT_EQ(timer.NextLoop(), 24h - 9001s);
}

Parametrized tests with custom names

See utest::PrintTestName for info on how to simplify writing parametrized tests and official gtest documentation.

Mocked dynamic config

You can fill dynamic config with custom config values using dynamic_config::StorageMock.

class DummyClient;
std::string DummyFunction(const dynamic_config::Snapshot& config) {
return config[kDummyConfig].bar;
}
UTEST(DynamicConfig, Snippet) {
// The 'StorageMock' will only contain the specified configs, and nothing more
{kDummyConfig, {42, "what"}},
{kIntConfig, 5},
};
EXPECT_EQ(DummyFunction(storage.GetSnapshot()), "what");
// 'DummyClient' stores 'dynamic_config::Source' for access to latest configs
DummyClient client{storage.GetSource()};
UEXPECT_NO_THROW(client.DoStuff());
storage.Extend({{kDummyConfig, {-10000, "invalid"}}});
UEXPECT_THROW(client.DoStuff(), std::runtime_error);
}

If you don't want to specify all configs used by the tested code, you can use default dynamic config.

To use default dynamic config values in tests, add DEFAULT_DYNAMIC_CONFIG_FILENAME preprocessor definition to your test CMake target, specifying the path of a YAML file with dynamic_config::DocsMap contents.

Default dynamic config values can be accessed using <dynamic_config/test_helpers.hpp>:

// Some production code
void MyHelper(const dynamic_config::Snapshot&);
class MyClient final {
public:
explicit MyClient(dynamic_config::Source);
// ...
};
// Tests
UTEST(Stuff, DefaultConfig) {
MyHelper(dynamic_config::GetDefaultSnapshot());
}
UTEST(Stuff, CustomConfig) {
const auto config_storage = dynamic_config::MakeDefaultStorage({
{kDummyConfig, {42, "what"}},
{kIntConfig, 5},
});
MyHelper(config_storage.GetSnapshot());
MyClient client{config_storage.GetSource()};
}

Testing userver logging

API for capturing userver logs can be found in userver/utest/default_logger_fixture.hpp

It can be used for testing that a certain piece of code produces logs with the given text (which is brittle, but sometimes needs to be done). It can also be used for testing logging::LogHelper serialization functions.

Benchmarks (google-benchmark)

Getting started

All userver benchmark helpers live in userver::ubench CMake target:

target_link_libraries(your-bench-target PRIVATE userver::ubench)

As usual, google-benchmark is available in <benchmark/benchmark.h>.

See also official google-benchmark documentation.

Coroutine environment

Use engine::RunStandalone to run parts of your benchmark in a coroutine environment:

void semaphore_lock(benchmark::State& state) {
std::size_t i = 0;
engine::Semaphore sem{std::numeric_limits<std::size_t>::max()};
for ([[maybe_unused]] auto _ : state) {
sem.lock_shared();
++i;
}
for (std::size_t j = 0; j < i; ++j) {
sem.unlock_shared();
}
});
}
BENCHMARK(semaphore_lock);

Mocked dynamic config

See the equivalent utest section.

Default dynamic configs are available in in <userver/dynamic_config/test_helpers.hpp>.