rdbo / libmem

Advanced Game Hacking Library for C, Modern C++, Rust and Python (Windows/Linux/FreeBSD) (Process/Memory Hacking) (Hooking/Detouring) (Cross Platform) (x86/x64/ARM/ARM64) (DLL/SO Injection) (Internal/External) (Assembler/Disassembler)
GNU Affero General Public License v3.0
764 stars 92 forks source link

Better testing #131

Closed illegal-instruction-co closed 8 months ago

illegal-instruction-co commented 1 year ago

Tests must be classificated as these 2 ( 3 ) parts:

  1. Unit tests
  2. Integration tests
  3. End to end tests ( optional )
rdbo commented 10 months ago

Unit tests WIP right now I think they will be enough, no need for the rest. The unit tests kind of will have integration tests anyways, can't really separate because unit testing some methods require other methods to succeed.

illegal-instruction-co commented 10 months ago

Unit tests WIP right now I think they will be enough, no need for the rest. The unit tests kind of will have integration tests anyways, can't really separate because unit testing some methods require other methods to succeed.

Integration tests are also needed. To cover all the code, at least 1 or 2 integration test should be enought.

rdbo commented 10 months ago

I can't unit test without doing integration anyways. Example:

char *test_LM_GetThreadProcess()
{
    lm_thread_t curthread;
    lm_process_t curprocess;
    lm_process_t process;

    mu_assert("failed to get current thread", LM_GetThread(&curthread) == LM_TRUE);
    mu_assert("retrieved invalid thread", CHECK_THREAD(&curthread));
    mu_assert("failed to get current process", LM_GetProcess(&curprocess) == LM_TRUE);
    mu_assert("retrieved current process is invalid", CHECK_PROCESS(&curprocess));
    mu_assert("failed to get thread process", LM_GetThreadProcess(&curthread, &process) == LM_TRUE);
    mu_assert("retrieved process is invalid", CHECK_PROCESS(&process));
    mu_assert("processes don't match", EQUAL_PROCESSES(&curprocess, &process));
    mu_assert("function attempted to run with bad arguments (invalid thread)", LM_GetThreadProcess(LM_NULLPTR, &process) == LM_FALSE);
    mu_assert("function attempted to run with bad arguments (invalid proc)", LM_GetThreadProcess(&curthread, LM_NULLPTR) == LM_FALSE);

    return NULL;
}

In this case, I'm unit testing the function LM_GetThreadProcess. But to test it, I have to get some stuff to check against, in this case I need to get a process and a thread. So in this unit test, I'm testing LM_GetThreadProcess while also requiring LM_GetProcess and LM_GetThread to work. Both of them are also tested before this function is tested.

illegal-instruction-co commented 10 months ago

Thats why we need to use dependency injection. If we are writing unit test, target fn must undepended.

rdbo commented 10 months ago

Thats why we need to use dependency injection. If we are writing unit test, target fn must undepended.

Hmm... I never wrote deep tests before TBH. What do you think of the current state of the tests: https://github.com/rdbo/libmem/tree/master/tests/unit

Any advices? Better to get things done right early on then have to change everything later.

rdbo commented 10 months ago

Ok, I'm doing proper dependency injection now - I'm storing the values after a successful test, and they are retrieved from the functions that need it.

illegal-instruction-co commented 10 months ago

I can make proper example tonight with pr. And another advice, i dont know how you think about using external libraries but googletest is best in this case.

rdbo commented 10 months ago

I can make proper example tonight with pr. And another advice, i dont know how you think about using external libraries but googletest is best in this case.

I'm using minunit because it's really small. In general I try avoiding using separate libraries, but if it's necessary, then why not.

rdbo commented 10 months ago

I can't unit test without doing integration anyways. Example:

char *test_LM_GetThreadProcess()
{
  lm_thread_t curthread;
  lm_process_t curprocess;
  lm_process_t process;

  mu_assert("failed to get current thread", LM_GetThread(&curthread) == LM_TRUE);
  mu_assert("retrieved invalid thread", CHECK_THREAD(&curthread));
  mu_assert("failed to get current process", LM_GetProcess(&curprocess) == LM_TRUE);
  mu_assert("retrieved current process is invalid", CHECK_PROCESS(&curprocess));
  mu_assert("failed to get thread process", LM_GetThreadProcess(&curthread, &process) == LM_TRUE);
  mu_assert("retrieved process is invalid", CHECK_PROCESS(&process));
  mu_assert("processes don't match", EQUAL_PROCESSES(&curprocess, &process));
  mu_assert("function attempted to run with bad arguments (invalid thread)", LM_GetThreadProcess(LM_NULLPTR, &process) == LM_FALSE);
  mu_assert("function attempted to run with bad arguments (invalid proc)", LM_GetThreadProcess(&curthread, LM_NULLPTR) == LM_FALSE);

  return NULL;
}

In this case, I'm unit testing the function LM_GetThreadProcess. But to test it, I have to get some stuff to check against, in this case I need to get a process and a thread. So in this unit test, I'm testing LM_GetThreadProcess while also requiring LM_GetProcess and LM_GetThread to work. Both of them are also tested before this function is tested.

I'm wondering how could this be done using GTest and Dependency Injection... My DI method is very basic, I just make a global variable that is assigned on a previous test, then used by whatever needs it.

illegal-instruction-co commented 10 months ago

First of all needed seperated test cases for each target fn.

#include <gtest/gtest.h>

extern "C" {
#include "your_code.h"  // your C code header file
}

// Define your CHECK_THREAD, CHECK_PROCESS, and EQUAL_PROCESSES macros or functions here

class LMThreadTest : public ::testing::Test {
protected:
    void SetUp() override {
        // Setup code, if needed, for LM_GetThread tests
    }

    void TearDown() override {
        // Tear-down code, if needed, for LM_GetThread tests
    }
};

class LMProcessTest : public ::testing::Test {
protected:
    void SetUp() override {
        // Setup code, if needed, for LM_GetProcess tests
    }

    void TearDown() override {
        // Tear-down code, if needed, for LM_GetProcess tests
    }
};

class LMThreadProcessTest : public ::testing::Test {
protected:
    void SetUp() override {
        // Setup code, if needed, for LM_GetThreadProcess tests
    }

    void TearDown() override {
        // Tear-down code, if needed, for LM_GetThreadProcess tests
    }
};

TEST_F(LMThreadTest, GetCurrentThread) {
    lm_thread_t curthread;
    ASSERT_EQ(LM_GetThread(&curthread), LM_TRUE);
    ASSERT_TRUE(CHECK_THREAD(&curthread));
}

TEST_F(LMProcessTest, GetCurrentProcess) {
    lm_process_t curprocess;
    ASSERT_EQ(LM_GetProcess(&curprocess), LM_TRUE);
    ASSERT_TRUE(CHECK_PROCESS(&curprocess));
}

TEST_F(LMThreadProcessTest, GetThreadProcess) {
    lm_thread_t curthread;
    lm_process_t curprocess;
    ASSERT_EQ(LM_GetThread(&curthread), LM_TRUE);
    ASSERT_TRUE(CHECK_THREAD(&curthread));

    ASSERT_EQ(LM_GetProcess(&curprocess), LM_TRUE);
    ASSERT_TRUE(CHECK_PROCESS(&curprocess));

    lm_process_t process;
    ASSERT_EQ(LM_GetThreadProcess(&curthread, &process), LM_TRUE);
    ASSERT_TRUE(CHECK_PROCESS(&process));
    ASSERT_TRUE(EQUAL_PROCESSES(&curprocess, &process));
}

int main(int argc, char** argv) {
    ::testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}

and link fake dependencies into test object instead of original ones. For example, your LM_GetThreadProcess depended to LM_ATHING method. Create a static library for LM_ATHING with std::function callbacks.

struct yourfakecallback final {
    static inline std::function<void(int> LM_ATHING;
};
#include "yourfakecallback.h"
void LM_ATHING(int thing) {
    return yourfakecallback::LM_ATHING(thing);
}

in your test case

TEST_F(LMThreadProcessTest, GetThreadProcess) {
    yourfakecallback::LM_ATHING = [](int thing) { 
        thing = 5;
    };

    lm_thread_t curthread;
    lm_process_t curprocess;
    ASSERT_EQ(LM_GetThread(&curthread), LM_TRUE);
    ASSERT_TRUE(CHECK_THREAD(&curthread));

    ASSERT_EQ(LM_GetProcess(&curprocess), LM_TRUE);
    ASSERT_TRUE(CHECK_PROCESS(&curprocess));

    lm_process_t process;
    ASSERT_EQ(LM_GetThreadProcess(&curthread, &process), LM_TRUE);
    ASSERT_TRUE(CHECK_PROCESS(&process));
    ASSERT_TRUE(EQUAL_PROCESSES(&curprocess, &process));
}
rdbo commented 8 months ago

For now, I think tests are good enough. They are covering most of libmem's C library functionality and they have passed on Linux and Windows. I will close this for now.