Testing files and time

A test that reads the wall clock or a fixed file path passes on one machine and fails on another. The cure is the same in both cases: the test creates what it needs, passes it in, and removes it afterwards, so nothing outside the test decides the result.

The harness of lesson 085 checks pure functions. This lesson adds the three things a test of real code needs: a directory of its own, a clock it controls, and a random engine with a seed it chose.

A test that does not control its inputs is a test of the machine, not of the code. The four sections are the four inputs a test usually has to control: the file system, the clock, the random engine and the cleanup.

A directory per test

The program builds a path under the system's temporary directory, creates it, writes a file and removes the whole tree. Nothing outside the test can see the path, and nothing is left after it.

The output is the size of the file and then 0 for the directory that no longer exists. The braces around the writing part matter: the ofstream closes before the size is read.

A test suite gives every test its own directory name, usually with the test's name in it, so that two tests running at once do not collide. Lesson 069 introduced the calls; here they are used for isolation.

#include <filesystem> #include <fstream> #include <iostream> namespace fs = std::filesystem; int main() { fs::path dir = fs::temp_directory_path() / "cpp-course-test"; fs::create_directories(dir); { std::ofstream out{dir / "data.txt"}; out << "12345"; } std::cout << fs::file_size(dir / "data.txt") << '\n'; fs::remove_all(dir); std::cout << fs::exists(dir) << '\n'; }

g++ -std=c++23 -Wall -Wextra temp_dir.cpp -o temp_dir
./temp_dir

5
0

A clock the test controls

The code under test takes a clock interface instead of calling the system clock itself. The test passes a fake implementation and moves its time by hand, so a rule about deadlines is checked in microseconds and at any date it likes.

The program asks whether a deadline has expired, advances the fake clock by 60 seconds and asks again. The answers are 0 and 1, and no real time passed.

The interface costs one virtual call in production, which is the price of testability here. Lesson 036 showed the injection pattern; this is its most common use.

#include <iostream> class time_source { public: virtual ~time_source() = default; virtual int now() const = 0; }; class fake_clock : public time_source { public: explicit fake_clock(int start) : time_(start) {} int now() const override { return time_; } void advance(int seconds) { time_ += seconds; } private: int time_; }; int is_expired(const time_source& source, int deadline) { return source.now() > deadline; } int main() { fake_clock clock{100}; std::cout << is_expired(clock, 150) << ' '; clock.advance(60); std::cout << is_expired(clock, 150) << '\n'; }

g++ -std=c++23 -Wall -Wextra fake_clock.cpp -o fake_clock
./fake_clock

0 1

A test that reads what it wrote

The test writes two lines to a temporary file, reads them back and checks both. The check function is the CHECK of the previous lesson with a message instead of a line number, which is enough here.

The output is 0 failures. The file is removed at the end, and the braces around the writer close the stream before the reader opens the same path.

A test like this catches the mistakes that pure functions cannot: a wrong separator, a missing newline, a stream that was not flushed. Those are the bugs that appear when the program meets the file system.

#include <filesystem> #include <fstream> #include <iostream> #include <string> namespace fs = std::filesystem; int failures = 0; void check(bool condition, const char* what) { if (!condition) { std::cout << "FAIL " << what << '\n'; ++failures; } } void test_round_trip() { fs::path file = fs::temp_directory_path() / "cpp-course-round-trip.txt"; { std::ofstream out{file}; out << "alpha\nbeta\n"; } std::ifstream in{file}; std::string line; std::getline(in, line); check(line == "alpha", "first line"); std::getline(in, line); check(line == "beta", "second line"); fs::remove(file); } int main() { test_round_trip(); std::cout << failures << " failures\n"; }

g++ -std=c++23 -Wall -Wextra file_test.cpp -o file_test
./file_test

0 failures

A seed the test chose

A test that needs random numbers takes the engine as a parameter instead of creating one. The test passes an engine with a fixed seed and knows the sequence that follows.

The program draws three numbers from two engines with the same seed and prints both rows. They are identical, which is what makes a test that depends on chance reproducible.

Lesson 074 showed the seed and the distribution; here the point is where the engine lives. A function that creates its own engine cannot be tested for a specific sequence.

#include <iostream> #include <random> #include <vector> std::vector<int> draw(std::mt19937& engine, int count) { std::uniform_int_distribution<int> dice{1, 6}; std::vector<int> values; for (int i = 0; i < count; ++i) { values.push_back(dice(engine)); } return values; } int main() { std::mt19937 first{7}; std::mt19937 second{7}; for (int value : draw(first, 3)) { std::cout << value << ' '; } std::cout << '\n'; for (int value : draw(second, 3)) { std::cout << value << ' '; } std::cout << '\n'; }

g++ -std=c++23 -Wall -Wextra seeded.cpp -o seeded
./seeded

1 2 5
1 2 5

Cleanup that runs on every path

A test that creates a file should remove it even when a check fails or an exception leaves the function. The guard below runs a function in its destructor, so the removal happens on every path.

The program prints whether the file exists inside the block and after it: 1 and 0, with the guard's message between them. The guard is the pattern of lesson 027 with a lambda as the action.

The same shape cleans a database row, a temporary environment variable or a server started for one test. The alternative, a cleanup call at every return, is the version that forgets one.

#include <filesystem> #include <fstream> #include <functional> #include <iostream> namespace fs = std::filesystem; class cleanup { public: explicit cleanup(std::function<void()> action) : action_(std::move(action)) {} ~cleanup() { action_(); } private: std::function<void()> action_; }; int main() { fs::path file = fs::temp_directory_path() / "cpp-course-guard.txt"; std::ofstream{file} << "data"; { cleanup guard{[file] { fs::remove(file); std::cout << "removed\n"; }}; std::cout << fs::exists(file) << ' '; } std::cout << fs::exists(file) << '\n'; }

g++ -std=c++23 -Wall -Wextra guard.cpp -o guard
./guard

1 removed
0

Exercises

  1. Write a test that creates a file, checks its size and removes it, and run it twice in a row.
  2. Add a second method to the fake clock that moves time backwards and check a rule that should refuse it.
  3. Replace the fixed seed in the seeded program with a seed taken from the command line and run it with the same seed twice.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. Why does a test create its files in a temporary directory instead of the source tree?

    Answer

    The test stays isolated and leaves nothing behind for the next run

    A test that cleans up after itself can run in any order and on any machine; a file left in the source tree changes the next run.

  2. What does fs::temp_directory_path() return?

    Answer

    A path to the system's directory for temporary files

    It reports where the system keeps temporary files, /tmp on this machine; the program still has to create anything inside it.

  3. What does this program print? std::cout << fs::exists(file) << ' '; inside a block whose guard removes file at the end, then the same line after the block?

    Answer

    1 0

    The file exists inside the block and the guard removes it when the block ends, so the second check prints 0.

  4. Why does the code under test take a clock interface instead of calling the system clock?

    Answer

    A test can pass a fake clock and control time exactly

    Injection replaces the real clock with a fake one, so a test checks a deadline without waiting for it.

  5. Two std::mt19937 engines are constructed with the seed 7 and each draws three values from uniform_int_distribution<int>{1, 6}. What is true?

    Answer

    The two rows of three values are identical

    An engine is deterministic: the same seed gives the same sequence, which is why a test can rely on it.

  6. A test that draws random numbers without a fixed seed fails now and then. What is the usual cause?

    Answer

    The engine was seeded from the wall clock or random_device

    A seed from the system makes the sequence different on every run, so a test that depends on it is not reproducible.

  7. What does the guard in the lesson do when the block ends?

    Answer

    It calls the function it stored in the constructor

    The destructor runs the stored action, which is the RAII pattern with a lambda as the cleanup.

  8. What does this program print? fs::path dir = fs::temp_directory_path() / "x"; fs::create_directories(dir); fs::remove_all(dir); std::cout << fs::exists(dir);

    Answer

    0, because remove_all deleted the directory

    remove_all deletes the directory and its contents, so exists returns false and the stream prints 0.

  9. A test writes a file and checks its size immediately, without closing the stream. What is the likely result?

    Answer

    The size may be 0 because the stream has not flushed yet

    Output is buffered until the stream is flushed or closed, so the reader can see an empty or partial file.

  10. Which failure does a round-trip test of a file catch that a test of a pure function cannot?

    Answer

    A missing newline or a wrong separator in the written text

    The file layer adds separators, buffering and encodings; only a test that writes and reads the text sees them.

The code on this page was built with g++ 15.2.0 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.

Article author: Arthur Isaev