diff --git a/CMakeLists.txt b/CMakeLists.txt index 540975a..31c3e54 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,7 +24,7 @@ set(PROJECT_NAME_TEST ${PROJECT_NAME}_unit_test) # name for the unit-test execut # ---------------------------------------------------------------------------------------- # Compiler and language configuration # ---------------------------------------------------------------------------------------- -# Require C++17 for GoogleTest and modern C++ features +# Require at least C++17 for GoogleTest and modern C++ features set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -124,6 +124,7 @@ set(APP_SOURCES "src/core/filehandle/IOStream.cpp" "src/core/filehandle/StringStream.cpp" "src/core/filehandle/FileIO.cpp" + "src/core/filehandle/Directory.cpp" ## Container "src/core/datatypes/container/sequence/Array.cpp" "src/core/datatypes/container/sequence/Vector.cpp" @@ -175,9 +176,10 @@ target_include_directories(${PROJECT_NAME_TEST} PRIVATE ${APP_HEADERS} # PRIVATE for internal use within a target, ) -# Link GoogleTest to the test executable +# Link GoogleTest, GoogleMock to the test executable target_link_libraries(${PROJECT_NAME_TEST} GTest::gtest_main + GTest::gmock_main ) # Register tests with CTest diff --git a/src/core/filehandle/Directory.cpp b/src/core/filehandle/Directory.cpp new file mode 100644 index 0000000..aa34ddf --- /dev/null +++ b/src/core/filehandle/Directory.cpp @@ -0,0 +1,45 @@ +#include // for creating dir +#include +#include +namespace { +void run() { + std::filesystem::path path1{"p1"}; + std::filesystem::path path2{"p1/p2"}; + + std::cout << "[1] Try create_directory(p1/p2)\n"; + try { + if (!std::filesystem::create_directory(path2)) { + std::cout << " Failed: parent directory does not exist\n"; + } + } catch (const std::filesystem::filesystem_error& e) { + std::cout << " Exception: " << e.what() << '\n'; + } + + std::cout << "[2] Create parent directories: p1\n"; + std::filesystem::create_directories(path1); + + std::cout << "[3] Try create_directory(p1/p2) again\n"; + if (std::filesystem::create_directory(path2)) { + std::cout << " Success\n"; + } else { + std::cout << " Failed: directory already exists\n"; + } + + std::cout << "[4] Cleanup\n"; + if (std::filesystem::exists(path2)) { + std::filesystem::remove(path2); + std::cout << " Removed p1/p2\n"; + } + + if (std::filesystem::exists(path1)) { + std::filesystem::remove(path1); + std::cout << " Removed p1\n"; + } +} +} // namespace + +struct DirectoryRunner { + DirectoryRunner() { run(); } +}; + +static DirectoryRunner autoRunner; \ No newline at end of file diff --git a/src/leetcode/arrays/3sum/Solution.cpp b/src/leetcode/arrays/3sum/Solution.cpp new file mode 100644 index 0000000..e69de29 diff --git a/src/leetcode/arrays/3sum/Solution.h b/src/leetcode/arrays/3sum/Solution.h new file mode 100644 index 0000000..e69de29 diff --git a/tests/DeleteMeTest.cpp b/tests/DeleteMeTest.cpp index 77870d4..7cdfa91 100644 --- a/tests/DeleteMeTest.cpp +++ b/tests/DeleteMeTest.cpp @@ -2,6 +2,10 @@ #include +/** + * ASSERT_* versions generate fatal failures when they fail, and abort the current function + * EXPECT_* are preferred, as they allow more than one failure to be reported in a test. + */ TEST(AddTest, ValidNumbers) { DeleteMe calc = DeleteMe(); @@ -44,6 +48,51 @@ TEST(DivTest, InvalidNumbers) { ASSERT_EQ(0, calc.get_last_result()); } +TEST(DivTest, FailureMessage) { + // ASSERT_FALSE(true) << "FailureMessage: ASSERT_FALSE(true)"; +} + +#include +#include "mock/MockTurtle.h" +#include "mock/Painter.h" +TEST(MockPainterTest, DrawSquare_CallsCorrectTurtleOperations) { + MockTurtle turtle; + Painter painter{turtle}; + + EXPECT_CALL(turtle, PenDown()).Times(1); + + EXPECT_CALL(turtle, Forward(100)).Times(4); + EXPECT_CALL(turtle, Turn(90)).Times(4); + + EXPECT_CALL(turtle, PenUp()).Times(1); + + painter.DrawSquare(100); +} + +#include "mock/FakeTurtle.h" +TEST(FakePainterTest, DrawSquare_ProducesSquarePath) { + FakeTurtle turtle; + Painter painter{turtle}; + + painter.DrawSquare(10); + + const auto& path = turtle.Path(); + + ASSERT_EQ(path.size(), 8); // 4 lines → 8 points (start/end) + + EXPECT_EQ(path[0], std::make_pair(0, 0)); + EXPECT_EQ(path[1], std::make_pair(0, 10)); + + EXPECT_EQ(path[2], std::make_pair(0, 10)); + EXPECT_EQ(path[3], std::make_pair(10, 10)); + + EXPECT_EQ(path[4], std::make_pair(10, 10)); + EXPECT_EQ(path[5], std::make_pair(10, 0)); + + EXPECT_EQ(path[6], std::make_pair(10, 0)); + EXPECT_EQ(path[7], std::make_pair(0, 0)); +} + int main(int argc, char** argv) { testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..429cb2d --- /dev/null +++ b/tests/README.md @@ -0,0 +1,97 @@ +# GoogleTest +- GoogleTest helps you write better C++ tests. +## 1. Basic Concepts +- **assertions:** the statememts that check whether a condition is true. The result can be: + - success + - nonfatal failure + - fatal failure +- **test suite:** that contains one or many tests. +- **text fixture:** the class contain test suite used for multiple tests to share common objects and subroutines. + +## 2. Test Fixtures: Using the Same Data Configuration for Multiple Tests +- To create a fixture class: + - derive a class from `testing::Test` with protected body + - declare used objects + - write a default constructor or `Setup()` function to prepare the object for each test. + - write a default destructor or `TearDown()` function to release resources. + - use `TEST_F(TestFixtureClassName, TestName)` instead of `TEST()` + +- e.g. +```cpp +#include "this/package/foo.h" + +#include + +namespace my { +namespace project { +namespace { + +// The fixture for testing class Foo. +class FooTest : public testing::Test { + protected: + // You can remove any or all of the following functions if their bodies would + // be empty. + + FooTest() { + // You can do set-up work for each test here. + } + + ~FooTest() override { + // You can do clean-up work that doesn't throw exceptions here. + } + + // If the constructor and destructor are not enough for setting up + // and cleaning up each test, you can define the following methods: + + void SetUp() override { + // Code here will be called immediately after the constructor (right + // before each test). + } + + void TearDown() override { + // Code here will be called immediately after each test (right + // before the destructor). + } + + // Class members declared here can be used by all tests in the test suite + // for Foo. +}; + +// Tests that the Foo::Bar() method does Abc. +TEST_F(FooTest, MethodBarDoesAbc) { + const std::string input_filepath = "this/package/testdata/myinputfile.dat"; + const std::string output_filepath = "this/package/testdata/myoutputfile.dat"; + Foo f; + EXPECT_EQ(f.Bar(input_filepath, output_filepath), 0); +} + +// Tests that Foo does Xyz. +TEST_F(FooTest, DoesXyz) { + // Exercises the Xyz feature of Foo. +} + +} // namespace +} // namespace project +} // namespace my + +int main(int argc, char **argv) { + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} +``` + +## 3. gMock +- It's not feasible or wide to rely on real objects entirely when testing. +- A mocks object implements the same interface as a real object, but lets you specify at run time how it will be used and what it should do (which methods will be called? in which order? how many times? with what arguments? what will they return? etc). +- `Fake objects` vs `Mock objects`: + - Fake objects have working implementations, but usually take some shortcut. e.g. return a static or in-memory value instead of the actual computation.“what happened?” + + - Mock objects are objects pre-programmed with expectations, which form a specification of the calls they are expected to receive. “how did it happen?” + +- **gMock** is a library (sometimes we also call it a “framework” to make it sound cool) for creating mock classes and using them. It does to C++ what jMock/EasyMock does to Java (well, more or less). + +>When using gMock, ++) first, you use some simple macros to describe the interface you want to mock, and they will expand to the implementation of your mock class; ++) next, you create some mock objects and specify its expectations and behavior using an intuitive syntax; ++) then you exercise code that uses the mock objects. gMock will catch any violation to the expectations as soon as it arises. +TBD https://google.github.io/googletest/gmock_for_dummies.html diff --git a/tests/mock/FakeTurtle.h b/tests/mock/FakeTurtle.h new file mode 100644 index 0000000..5653466 --- /dev/null +++ b/tests/mock/FakeTurtle.h @@ -0,0 +1,59 @@ +#pragma once + +#include +#include +#include "Turtle.h" + +class FakeTurtle : public Turtle { + public: + FakeTurtle() = default; + + void PenUp() override { penDown_ = false; } + + void PenDown() override { penDown_ = true; } + + void Forward(int distance) override { + if (penDown_) { + path_.emplace_back(x_, y_); + } + + switch (direction_) { + case 0: + y_ += distance; + break; // north + case 90: + x_ += distance; + break; // east + case 180: + y_ -= distance; + break; // south + case 270: + x_ -= distance; + break; // west + } + + if (penDown_) { + path_.emplace_back(x_, y_); + } + } + + void Turn(int degrees) override { direction_ = (direction_ + degrees) % 360; } + + void GoTo(int x, int y) override { + x_ = x; + y_ = y; + } + + int GetX() const override { return x_; } + int GetY() const override { return y_; } + + const std::vector>& Path() const { return path_; } + + private: + int x_{0}; + int y_{0}; + int direction_{0}; // 0=north, 90=east, ... + bool penDown_{false}; + + std::vector> path_; +}; diff --git a/tests/mock/MockTurtle.h b/tests/mock/MockTurtle.h new file mode 100644 index 0000000..707979a --- /dev/null +++ b/tests/mock/MockTurtle.h @@ -0,0 +1,13 @@ +#include "Turtle.h" // mock test for this class +#include // Brings in gMock. + +class MockTurtle : public Turtle { + public: + MOCK_METHOD(void, PenUp, (), (override)); // the MOCK_METHOD macro will generate the definitions for APIs + MOCK_METHOD(void, PenDown, (), (override)); + MOCK_METHOD(void, Forward, (int distance), (override)); + MOCK_METHOD(void, Turn, (int degrees), (override)); + MOCK_METHOD(void, GoTo, (int x, int y), (override)); + MOCK_METHOD(int, GetX, (), (const, override)); + MOCK_METHOD(int, GetY, (), (const, override)); +}; \ No newline at end of file diff --git a/tests/mock/Painter.h b/tests/mock/Painter.h new file mode 100644 index 0000000..96e44d4 --- /dev/null +++ b/tests/mock/Painter.h @@ -0,0 +1,19 @@ +#pragma once +#include "Turtle.h" + +class Painter { +public: + explicit Painter(Turtle& turtle) : turtle_(turtle) {} + + void DrawSquare(int size) { + turtle_.PenDown(); + for (int i = 0; i < 4; ++i) { + turtle_.Forward(size); + turtle_.Turn(90); + } + turtle_.PenUp(); + } + +private: + Turtle& turtle_; +}; diff --git a/tests/mock/Turtle.h b/tests/mock/Turtle.h new file mode 100644 index 0000000..cf5f345 --- /dev/null +++ b/tests/mock/Turtle.h @@ -0,0 +1,13 @@ +#pragma once + +class Turtle { + public: + virtual ~Turtle() {} + virtual void PenUp() = 0; + virtual void PenDown() = 0; + virtual void Forward(int distance) = 0; + virtual void Turn(int degrees) = 0; + virtual void GoTo(int x, int y) = 0; + virtual int GetX() const = 0; + virtual int GetY() const = 0; +}; \ No newline at end of file