diff --git a/src/utils/ExecutionStatManager.cpp b/src/utils/ExecutionStatManager.cpp
index 06f7942cf37955c7d715fded89b464da74586eb5..6422d08c026ddf2df2169f503a585df936108ad9 100644
--- a/src/utils/ExecutionStatManager.cpp
+++ b/src/utils/ExecutionStatManager.cpp
@@ -44,7 +44,7 @@ ExecutionStatManager::_prettyPrintTime(double time_in_seconds) const
 }
 
 void
-ExecutionStatManager::_printMaxResidentMemory() const
+ExecutionStatManager::_printMaxResidentMemory(std::ostream& os) const
 {
   class Memory
   {
@@ -85,42 +85,39 @@ ExecutionStatManager::_printMaxResidentMemory() const
   };
 
   Memory memory;
-  std::cout << "Memory: " << rang::style::bold << Memory{parallel::allReduceSum(memory.value())}.prettyPrint()
-            << rang::style::reset;
-  if (parallel::size() > 1) {
-    std::cout << " (over " << parallel::size() << " processes)";
-    std::cout << " Avg: " << rang::style::bold
-              << Memory{parallel::allReduceSum(memory.value()) / parallel::size()}.prettyPrint() << rang::style::reset;
-    std::cout << " Min: " << rang::style::bold << Memory{parallel::allReduceMin(memory.value())}.prettyPrint()
-              << rang::style::reset;
-    std::cout << " Max: " << rang::style::bold << Memory{parallel::allReduceMax(memory.value())}.prettyPrint()
-              << rang::style::reset;
-  }
-  std::cout << '\n';
+  os << "Memory: " << rang::style::bold << Memory{parallel::allReduceSum(memory.value())}.prettyPrint()
+     << rang::style::reset;
+  os << " (over " << parallel::size() << " processes)";
+  os << " Avg: " << rang::style::bold << Memory{parallel::allReduceSum(memory.value()) / parallel::size()}.prettyPrint()
+     << rang::style::reset;
+  os << " Min: " << rang::style::bold << Memory{parallel::allReduceMin(memory.value())}.prettyPrint()
+     << rang::style::reset;
+  os << " Max: " << rang::style::bold << Memory{parallel::allReduceMax(memory.value())}.prettyPrint()
+     << rang::style::reset;
+  os << '\n';
 }
 
 void
-ExecutionStatManager::_printElapseTime() const
+ExecutionStatManager::_printElapseTime(std::ostream& os) const
 {
   const double elapse_time = m_instance->m_elapse_time.seconds();
-  std::cout << "Execution: " << rang::style::bold << elapse_time << 's' << rang::style::reset;
+  os << "Execution: " << rang::style::bold << elapse_time << 's' << rang::style::reset;
   if (elapse_time > 60) {
-    std::cout << " [" << rang::style::bold << this->_prettyPrintTime(elapse_time) << rang::style::reset << ']';
+    os << " [" << rang::style::bold << this->_prettyPrintTime(elapse_time) << rang::style::reset << ']';
   }
   if (m_run_number > 1) {
     const double cumulative_elapse_time = elapse_time + m_previous_cumulative_elapse_time;
-    std::cout << " (Run number " << m_run_number << ").\n - Cumulative execution time: " << rang::style::bold
-              << cumulative_elapse_time << 's' << rang::style::reset;
+    os << " (Run number " << m_run_number << ").\n - Cumulative execution time: " << rang::style::bold
+       << cumulative_elapse_time << 's' << rang::style::reset;
     if (cumulative_elapse_time > 60) {
-      std::cout << " [" << rang::style::bold << this->_prettyPrintTime(cumulative_elapse_time) << rang::style::reset
-                << ']';
+      os << " [" << rang::style::bold << this->_prettyPrintTime(cumulative_elapse_time) << rang::style::reset << ']';
     }
   }
-  std::cout << '\n';
+  os << '\n';
 }
 
 void
-ExecutionStatManager::_printTotalCPUTime() const
+ExecutionStatManager::_printTotalCPUTime(std::ostream& os) const
 {
   rusage u;
   getrusage(RUSAGE_SELF, &u);
@@ -128,36 +125,33 @@ ExecutionStatManager::_printTotalCPUTime() const
   const double total_cpu_time =
     parallel::allReduceSum(u.ru_utime.tv_sec + u.ru_stime.tv_sec + (u.ru_utime.tv_usec + u.ru_stime.tv_usec) * 1E-6);
 
-  std::cout << "Total CPU: " << rang::style::bold << total_cpu_time << 's' << rang::style::reset;
-  std::cout << " (" << parallel::allReduceSum(Kokkos::DefaultHostExecutionSpace::concurrency()) << " threads over "
-            << parallel::size() << " processes)";
+  os << "Total CPU: " << rang::style::bold << total_cpu_time << 's' << rang::style::reset;
+  os << " (" << parallel::allReduceSum(Kokkos::DefaultHostExecutionSpace::concurrency()) << " threads over "
+     << parallel::size() << " processes)";
   if (total_cpu_time > 60) {
-    std::cout << " [" << _prettyPrintTime(total_cpu_time) << ']';
+    os << " [" << _prettyPrintTime(total_cpu_time) << ']';
   }
 
   if (m_run_number > 1) {
     const double cumulative_total_cpu_time = total_cpu_time + m_previous_cumulative_total_cpu_time;
-    std::cout << "\n - Cumulative total CPU: " << rang::style::bold << cumulative_total_cpu_time << 's'
-              << rang::style::reset;
+    os << "\n - Cumulative total CPU: " << rang::style::bold << cumulative_total_cpu_time << 's' << rang::style::reset;
     if (cumulative_total_cpu_time > 60) {
-      std::cout << " [" << rang::style::bold << this->_prettyPrintTime(cumulative_total_cpu_time) << rang::style::reset
-                << ']';
+      os << " [" << rang::style::bold << this->_prettyPrintTime(cumulative_total_cpu_time) << rang::style::reset << ']';
     }
   }
 
-  std::cout << '\n';
+  os << '\n';
 }
 
 void
-ExecutionStatManager::printInfo()
+ExecutionStatManager::printInfo(std::ostream& os)
 {
   if (ExecutionStatManager::getInstance().doPrint()) {
-    std::cout << "----------------- " << rang::fg::green << "pugs exec stats" << rang::fg::reset
-              << " ---------------------\n";
+    os << "----------------- " << rang::fg::green << "pugs exec stats" << rang::fg::reset << " ---------------------\n";
 
-    ExecutionStatManager::getInstance()._printElapseTime();
-    ExecutionStatManager::getInstance()._printTotalCPUTime();
-    ExecutionStatManager::getInstance()._printMaxResidentMemory();
+    ExecutionStatManager::getInstance()._printElapseTime(os);
+    ExecutionStatManager::getInstance()._printTotalCPUTime(os);
+    ExecutionStatManager::getInstance()._printMaxResidentMemory(os);
   }
 }
 
diff --git a/src/utils/ExecutionStatManager.hpp b/src/utils/ExecutionStatManager.hpp
index f07099eecf4be81a7cea41b1456f826be5c06cbf..a99f6e08c239d0b832feb32748d17c0bb61f023c 100644
--- a/src/utils/ExecutionStatManager.hpp
+++ b/src/utils/ExecutionStatManager.hpp
@@ -4,9 +4,14 @@
 #include <utils/PugsAssert.hpp>
 #include <utils/Timer.hpp>
 
+#include <iostream>
+
 class ExecutionStatManager
 {
  private:
+  // For unit tests only
+  friend class ExecutionStatManagerTester;
+
   static ExecutionStatManager* m_instance;
 
   Timer m_elapse_time;
@@ -19,9 +24,9 @@ class ExecutionStatManager
 
   std::string _prettyPrintTime(double seconds) const;
 
-  void _printMaxResidentMemory() const;
-  void _printElapseTime() const;
-  void _printTotalCPUTime() const;
+  void _printMaxResidentMemory(std::ostream& os) const;
+  void _printElapseTime(std::ostream& os) const;
+  void _printTotalCPUTime(std::ostream& os) const;
 
   explicit ExecutionStatManager()                   = default;
   ExecutionStatManager(ExecutionStatManager&&)      = delete;
@@ -99,7 +104,7 @@ class ExecutionStatManager
     return *m_instance;
   }
 
-  static void printInfo();
+  static void printInfo(std::ostream& os = std::cout);
   static void create();
   static void destroy();
 };
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index af385092149149d41fbf41f70172759e29320573..bfde508503511a8e6ef6f0c0a82f9e4cf650a05d 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -224,6 +224,7 @@ add_executable (mpi_unit_tests
   test_EmbeddedDiscreteFunctionOperators1D.cpp
   test_EmbeddedDiscreteFunctionOperators2D.cpp
   test_EmbeddedDiscreteFunctionOperators3D.cpp
+  test_ExecutionStatManager.cpp
   test_InterpolateItemArray.cpp
   test_InterpolateItemValue.cpp
   test_ItemArray.cpp
diff --git a/tests/test_ExecutionStatManager.cpp b/tests/test_ExecutionStatManager.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..434b1eaf1cf9460049cc923de3263b52d5a88509
--- /dev/null
+++ b/tests/test_ExecutionStatManager.cpp
@@ -0,0 +1,89 @@
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/matchers/catch_matchers_all.hpp>
+
+#include <utils/ExecutionStatManager.hpp>
+#include <utils/Stringify.hpp>
+#include <utils/pugs_config.hpp>
+
+#include <sstream>
+
+class ExecutionStatManagerTester
+{
+ public:
+  double
+  getPreviousCumulativeElapseTime() const
+  {
+    return ExecutionStatManager::getInstance().m_previous_cumulative_elapse_time;
+  }
+
+  double
+  getPreviousCumulativeTotalCpuTime() const
+  {
+    return ExecutionStatManager::getInstance().m_previous_cumulative_total_cpu_time;
+  }
+
+  void
+  setElapseTime(const Timer& timer) const
+  {
+    ExecutionStatManager::getInstance().m_elapse_time = timer;
+  }
+
+  Timer&
+  getElapseTime() const
+  {
+    return ExecutionStatManager::getInstance().m_elapse_time;
+  }
+
+  ExecutionStatManagerTester()  = default;
+  ~ExecutionStatManagerTester() = default;
+};
+
+// clazy:excludeall=non-pod-global-static
+
+TEST_CASE("ExecutionStatManager", "[utils]")
+{
+  REQUIRE_NOTHROW(ExecutionStatManager::create());
+  REQUIRE_THROWS_WITH(ExecutionStatManager::create(), "unexpected error: ExecutionStatManager already created");
+
+  ExecutionStatManagerTester esm_tester;
+
+  REQUIRE(ExecutionStatManager::getInstance().runNumber() == 1);
+  REQUIRE(ExecutionStatManager::getInstance().doPrint() == true);
+  REQUIRE(ExecutionStatManager::getInstance().exitCode() == 0);
+
+  REQUIRE(esm_tester.getPreviousCumulativeElapseTime() == 0);
+  REQUIRE(esm_tester.getPreviousCumulativeTotalCpuTime() == 0);
+
+  REQUIRE_NOTHROW(ExecutionStatManager::getInstance().setRunNumber(2));
+  REQUIRE(ExecutionStatManager::getInstance().runNumber() == 2);
+
+  REQUIRE_NOTHROW(ExecutionStatManager::getInstance().setPreviousCumulativeElapseTime(100));
+  REQUIRE_NOTHROW(ExecutionStatManager::getInstance().setPreviousCumulativeTotalCPUTime(200));
+
+  REQUIRE(esm_tester.getPreviousCumulativeElapseTime() == 100);
+  REQUIRE(esm_tester.getPreviousCumulativeTotalCpuTime() == 200);
+
+  using namespace std::chrono_literals;
+  Timer t{std::chrono::high_resolution_clock::now() - (2 * 21h + 267s)};
+  t.pause();
+  esm_tester.setElapseTime(t);
+
+  REQUIRE(ExecutionStatManager::getInstance().getElapseTime() == t.seconds());
+  REQUIRE(ExecutionStatManager::getInstance().getCumulativeElapseTime() == 100 + t.seconds());
+  REQUIRE(ExecutionStatManager::getInstance().getCumulativeTotalCPUTime() >= 200);
+
+  std::ostringstream os;
+  // One just call the function not test is performed. It is quite
+  // boring to check the result and not that useful.
+  ExecutionStatManager::getInstance().printInfo(os);
+
+  ExecutionStatManager::getInstance().setPrint(false);
+  REQUIRE(ExecutionStatManager::getInstance().doPrint() == false);
+
+  ExecutionStatManager::getInstance().setExitCode(1);
+  REQUIRE(ExecutionStatManager::getInstance().exitCode() == 1);
+
+  REQUIRE_NOTHROW(ExecutionStatManager::destroy());
+  // One allows multiple destruction to handle unexpected code exit
+  REQUIRE_NOTHROW(ExecutionStatManager::destroy());
+}