diff --git a/test/Graphics/Color.cpp b/test/Graphics/Color.cpp
index c2fd0ad33..9cf115a56 100644
--- a/test/Graphics/Color.cpp
+++ b/test/Graphics/Color.cpp
@@ -1,6 +1,8 @@
 #include <SFML/Graphics/Color.hpp>
 #include "GraphicsUtil.hpp"
 
+#include <doctest.h>
+
 TEST_CASE("sf::Color class - [graphics]")
 {
     SUBCASE("Construction")
diff --git a/test/Graphics/Rect.cpp b/test/Graphics/Rect.cpp
index 6aa54c453..834681c5a 100644
--- a/test/Graphics/Rect.cpp
+++ b/test/Graphics/Rect.cpp
@@ -2,6 +2,8 @@
 #include <SFML/System/Vector2.hpp>
 #include "GraphicsUtil.hpp"
 
+#include <doctest.h>
+
 TEST_CASE("sf::Rect class template - [graphics]")
 {
     SUBCASE("Construction")
diff --git a/test/Graphics/Vertex.cpp b/test/Graphics/Vertex.cpp
index 5eb3ed317..b6ea904c3 100644
--- a/test/Graphics/Vertex.cpp
+++ b/test/Graphics/Vertex.cpp
@@ -1,6 +1,8 @@
 #include <SFML/Graphics/Vertex.hpp>
 #include "GraphicsUtil.hpp"
 
+#include <doctest.h>
+
 TEST_CASE("sf::Vertex class - [graphics]")
 {
     SUBCASE("Construction")
diff --git a/test/System/FileInputStream.cpp b/test/System/FileInputStream.cpp
index 22f29f5f8..4bbd069dd 100644
--- a/test/System/FileInputStream.cpp
+++ b/test/System/FileInputStream.cpp
@@ -3,6 +3,8 @@
 #include <string_view>
 #include <utility>
 
+#include <doctest.h>
+
 TEST_CASE("sf::FileInputStream class - [system]")
 {
     SUBCASE("Empty stream")
diff --git a/test/System/Time.cpp b/test/System/Time.cpp
index d32f83412..aaed19d9e 100644
--- a/test/System/Time.cpp
+++ b/test/System/Time.cpp
@@ -1,6 +1,8 @@
 #include <SFML/System/Time.hpp>
 #include "SystemUtil.hpp"
 
+#include <doctest.h>
+
 using doctest::Approx;
 
 TEST_CASE("sf::Time class - [system]")
diff --git a/test/System/Vector2.cpp b/test/System/Vector2.cpp
index 1795d10b1..dd1ebe812 100644
--- a/test/System/Vector2.cpp
+++ b/test/System/Vector2.cpp
@@ -2,6 +2,8 @@
 #include "SystemUtil.hpp"
 #include <type_traits>
 
+#include <doctest.h>
+
 // Use sf::Vector2i for tests. Test coverage is given, as there are no template specializations.
 
 TEST_CASE("sf::Vector2 class template - [system]")
diff --git a/test/System/Vector3.cpp b/test/System/Vector3.cpp
index 55bb08a36..dd96e3c45 100644
--- a/test/System/Vector3.cpp
+++ b/test/System/Vector3.cpp
@@ -2,6 +2,8 @@
 #include "SystemUtil.hpp"
 #include <type_traits>
 
+#include <doctest.h>
+
 // Use sf::Vector3i for tests. Test coverage is given, as there are no template specializations.
 
 TEST_CASE("sf::Vector3 class template - [system]")
diff --git a/test/TestUtilities/GraphicsUtil.cpp b/test/TestUtilities/GraphicsUtil.cpp
index 45879fce2..ebff0532a 100644
--- a/test/TestUtilities/GraphicsUtil.cpp
+++ b/test/TestUtilities/GraphicsUtil.cpp
@@ -1,19 +1,17 @@
 // Note: No need to increase compile time by including TestUtilities/Graphics.hpp
 #include <SFML/Graphics/Color.hpp>
-#include <sstream>
-#include <doctest.h>
+#include <ostream>
 
 namespace sf
 {
-    doctest::String toString(const sf::Color& color)
+    std::ostream& operator <<(std::ostream& os, const sf::Color& color)
     {
-        std::ostringstream stream;
-        stream << "0x" << std::hex << color.toInteger() << std::dec
-               << " (r=" << static_cast<int>(color.r)
-               << ", g=" << static_cast<int>(color.g)
-               << ", b=" << static_cast<int>(color.b)
-               << ", a=" << static_cast<int>(color.a) << ")";
+        os << "0x" << std::hex << color.toInteger() << std::dec
+           << " (r=" << static_cast<int>(color.r)
+           << ", g=" << static_cast<int>(color.g)
+           << ", b=" << static_cast<int>(color.b)
+           << ", a=" << static_cast<int>(color.a) << ")";
 
-        return stream.str().c_str();
+        return os;
     }
 }
diff --git a/test/TestUtilities/GraphicsUtil.hpp b/test/TestUtilities/GraphicsUtil.hpp
index 287c72514..9547c1503 100644
--- a/test/TestUtilities/GraphicsUtil.hpp
+++ b/test/TestUtilities/GraphicsUtil.hpp
@@ -1,6 +1,6 @@
 // Header for SFML unit tests.
 //
-// For a new graphics module test case, include this header and not <doctest.h> directly.
+// For a new graphics module test case, include this header.
 // This ensures that string conversions are visible and can be used by doctest for debug output.
 
 #ifndef SFML_TESTUTILITIES_GRAPHICS_HPP
@@ -8,15 +8,10 @@
 
 #include "WindowUtil.hpp"
 
-namespace doctest
-{
-    class String;
-}
-
 namespace sf
 {
     class Color;
-    doctest::String toString(const Color& color);
+    std::ostream& operator <<(std::ostream& os, const Color& color);
 }
 
 #endif // SFML_TESTUTILITIES_GRAPHICS_HPP
diff --git a/test/TestUtilities/SystemUtil.cpp b/test/TestUtilities/SystemUtil.cpp
index 7306e1a98..c1c14ea44 100644
--- a/test/TestUtilities/SystemUtil.cpp
+++ b/test/TestUtilities/SystemUtil.cpp
@@ -9,23 +9,22 @@
 #endif // !defined(__GNUC__) || (__GNUC__ >= 9)
 
 #include <fstream>
+#include <ostream>
 #include <sstream>
 #include <cassert>
 
-#include <doctest.h>
-
 namespace sf
 {
-    doctest::String toString(const sf::String& string)
+    std::ostream& operator <<(std::ostream& os, const sf::String& string)
     {
-        return string.toAnsiString().c_str();
+        os << string.toAnsiString();
+        return os;
     }
 
-    doctest::String toString(sf::Time time)
+    std::ostream& operator <<(std::ostream& os, sf::Time time)
     {
-        std::ostringstream stream;
-        stream << time.asMicroseconds() << "us";
-        return stream.str().c_str();
+        os << time.asMicroseconds() << "us";
+        return os;
     }
 }
 
diff --git a/test/TestUtilities/SystemUtil.hpp b/test/TestUtilities/SystemUtil.hpp
index c5eb989f4..1c0c1b685 100644
--- a/test/TestUtilities/SystemUtil.hpp
+++ b/test/TestUtilities/SystemUtil.hpp
@@ -1,15 +1,15 @@
 // Header for SFML unit tests.
 //
-// For a new system module test case, include this header and not <doctest.h> directly.
+// For a new system module test case, include this header.
 // This ensures that string conversions are visible and can be used by doctest for debug output.
 
 #ifndef SFML_TESTUTILITIES_SYSTEM_HPP
 #define SFML_TESTUTILITIES_SYSTEM_HPP
 
-#include <doctest.h>
-
 #include <SFML/System/Vector2.hpp>
 #include <SFML/System/Vector3.hpp>
+
+#include <ostream>
 #include <sstream>
 #include <string>
 
@@ -19,23 +19,21 @@ namespace sf
     class String;
     class Time;
 
-    doctest::String toString(const sf::String& string);
-    doctest::String toString(sf::Time time);
+    std::ostream& operator <<(std::ostream& os, const sf::String& string);
+    std::ostream& operator <<(std::ostream& os, sf::Time time);
 
     template <typename T>
-    doctest::String toString(const sf::Vector2<T>& vector)
+    std::ostream& operator <<(std::ostream& os, const sf::Vector2<T>& vector)
     {
-        std::ostringstream stream;
-        stream << "(" << vector.x << ", " << vector.y << ")";
-        return stream.str().c_str();
+        os << "(" << vector.x << ", " << vector.y << ")";
+        return os;
     }
 
     template <typename T>
-    doctest::String toString(const sf::Vector3<T>& vector)
+    std::ostream& operator <<(std::ostream& os, const sf::Vector3<T>& vector)
     {
-        std::ostringstream stream;
-        stream << "(" << vector.x << ", " << vector.y << ", " << vector.z << ")";
-        return stream.str().c_str();
+        os << "(" << vector.x << ", " << vector.y << ", " << vector.z << ")";
+        return os;
     }
 }
 
diff --git a/test/TestUtilities/WindowUtil.cpp b/test/TestUtilities/WindowUtil.cpp
index b280f8553..637e328c1 100644
--- a/test/TestUtilities/WindowUtil.cpp
+++ b/test/TestUtilities/WindowUtil.cpp
@@ -1,15 +1,12 @@
 // Note: No need to increase compile time by including TestUtilities/Window.hpp
 #include <SFML/Window/VideoMode.hpp>
-#include <sstream>
-
-#include <doctest.h>
+#include <ostream>
 
 namespace sf
 {
-    doctest::String toString(const sf::VideoMode& videoMode)
+    std::ostream& operator <<(std::ostream& os, const sf::VideoMode& videoMode)
     {
-        std::ostringstream stream;
-        stream << videoMode.width << "x" << videoMode.height << "x" << videoMode.bitsPerPixel;
-        return stream.str().c_str();
+        os << videoMode.width << "x" << videoMode.height << "x" << videoMode.bitsPerPixel;
+        return os;
     }
 }
diff --git a/test/TestUtilities/WindowUtil.hpp b/test/TestUtilities/WindowUtil.hpp
index 586ee39cd..ae5630afa 100644
--- a/test/TestUtilities/WindowUtil.hpp
+++ b/test/TestUtilities/WindowUtil.hpp
@@ -1,6 +1,6 @@
 // Header for SFML unit tests.
 //
-// For a new window module test case, include this header and not <doctest.h> directly.
+// For a new window module test case, include this header.
 // This ensures that string conversions are visible and can be used by doctest for debug output.
 
 #ifndef SFML_TESTUTILITIES_WINDOW_HPP
@@ -10,21 +10,18 @@
 
 #include <SFML/Graphics/Rect.hpp>
 
-#include <doctest.h>
-
 // String conversions for doctest framework
 namespace sf
 {
     class VideoMode;
 
-    doctest::String toString(const sf::VideoMode& videoMode);
+    std::ostream& operator <<(std::ostream& os, const sf::VideoMode& videoMode);
 
     template <typename T>
-    doctest::String toString(const sf::Rect<T>& rect)
+    std::ostream& operator <<(std::ostream& os, const sf::Rect<T>& rect)
     {
-        std::ostringstream stream;
-        stream << "(left=" << rect.left << ", top=" << rect.top << ", width=" << rect.width << ", height=" << rect.height << ")";
-        return stream.str().c_str();
+        os << "(left=" << rect.left << ", top=" << rect.top << ", width=" << rect.width << ", height=" << rect.height << ")";
+        return os;
     }
 }
 
diff --git a/test/Window/VideoMode.cpp b/test/Window/VideoMode.cpp
index 2f6bab7a0..6f5e05121 100644
--- a/test/Window/VideoMode.cpp
+++ b/test/Window/VideoMode.cpp
@@ -1,6 +1,8 @@
 #include <SFML/Window/VideoMode.hpp>
 #include "WindowUtil.hpp"
 
+#include <doctest.h>
+
 TEST_CASE("sf::VideoMode class - [window]")
 {
     SUBCASE("Construction")