unit test 란

 

 

 

mocking 이란

mocking은 유닛 테스트에서 사용되는 기술 중 하나로, 의존성 있는 코드를 테스트할 때, 해당 의존성을 가짜 객체로 대체하여 테스트하는 것을 의미함

즉, 의존성 있는 코드를 호출할 때, 그 코드가 호출하는 객체를 대신해서 가짜 객체를 사용하여 호출하는 것

이 가짜 객체는 실제 객체와 동일한 인터페이스를 제공하지만, 미리 정의된 테스트 케이스에 따라서 적절한 결과를 반환

이렇게 하면 의존성이 있는 코드의 결과를 예측 가능하게 만들어

또한, 의존성이 있는 코드를 모의(mock) 객체로 대체하면, 해당 객체가 실제 시스템에 영향을 미치지 않고도 코드를 테스트할 수 있음

 

 

Queue라는 클래스가 있다고 가정하고, Queue가 의존하는 클래스인 Data라는 클래스를 mocking하는 예제

// Queue.h
#include "Data.h"

class Queue {
public:
    Queue(Data* data);
    int size();
private:
    Data* data_;
};

// Queue.cpp
#include "Queue.h"

Queue::Queue(Data* data) : data_(data) {}

int Queue::size() {
    return data_->getSize();
}

// MockData.h
#include "Data.h"
#include "gmock/gmock.h"

class MockData : public Data {
public:
    MOCK_METHOD(int, getSize, (), (override));
};

// QueueTest.cpp
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "Queue.h"
#include "MockData.h"

using ::testing::Return;

TEST(QueueTest, SizeTest) {
    MockData mock_data;
    Queue q(&mock_data);
    EXPECT_CALL(mock_data, getSize()).WillOnce(Return(3));
    ASSERT_EQ(q.size(), 3);
}

 

이 예제에서는 Queue 클래스가 Data 클래스에 의존하고 있음

그리고 QueueTest에서는 Queue의 size() 메소드를 테스트하고자 함

하지만, size() 메소드는 Data의 getSize() 메소드를 호출하기 때문에, Data 클래스에 의존하고 있는 상황

이를 해결하기 위해 MockData 클래스를 만들어 Data 클래스를 mocking

 

Queue 클래스에서는 생성자에서 Data 객체를 받아서 멤버 변수로 저장

size() 메소드에서는 Data 객체의 getSize() 메소드를 호출하여 반환값을 리턴

QueueTest에서는 MockData 객체를 생성하고, Queue 객체를 생성할 때 이를 전달

그리고 EXPECT_CALL 매크로를 사용하여, MockData 객체의 getSize() 메소드가 호출될 때 3을 반환하도록 지정

마지막으로 ASSERT_EQ 매크로를 사용하여 Queue 객체의 size() 메소드가 3을 반환하는지 확인

이렇게 Mocking을 통해, Queue 클래스가 Data 클래스에 의존하는 상황에서도 테스트를 수행할 수 있게됨

 

 

mocking을 사용하여 실제 데이터베이스에 접근하지 않고 데이터베이스를 대체하는 예제

 

예를 들어, 데이터베이스에서 사용자 정보를 가져와 로그인 인증을 수행하는 함수가 있는데,

이 함수는 데이터베이스에 직접 연결하여 쿼리를 실행하므로 테스트를 수행하는 데 시간이 많이 소요된다고 가정

이런 경우 mocking을 사용하여 데이터베이스에 연결하지 않고도 테스트를 수행할 수 있음

 

먼저 사용자 정보를 나타내는 User 클래스는 아래 처럼 디자인 해볼 수 있음

class User {
public:
    User(const std::string& name, const std::string& password) : name_(name), password_(password) {}
    const std::string& GetName() const { return name_; }
    const std::string& GetPassword() const { return password_; }
private:
    std::string name_;
    std::string password_;
};

 

데이터베이스와 상호작용하는 UserRepository 클래스

class UserRepository {
public:
    std::shared_ptr<User> GetUserByName(const std::string& name) {
        // 데이터베이스에 연결하여 쿼리 실행
        // ...
        // 결과를 User 객체로 변환하여 반환
        return std::make_shared<User>(name, "password");
    }
};
 
UserRepository를 사용하여 로그인 인증을 수행하는 LoginService 클래스
 
class LoginService {
public:
    bool Authenticate(const std::string& name, const std::string& password) {
        UserRepository repository;
        std::shared_ptr<User> user = repository.GetUserByName(name);
        if (!user) {
            return false;
        }
        return user->GetPassword() == password;
    }
};

 

이제 LoginService의 Authenticate 함수를 테스트하려면 UserRepository 클래스의 GetUserByName 함수를 호출해야 함

그러나 이 함수는 데이터베이스에 직접 연결하기 때문에 테스트하는 데 시간이 많이 소요됨

따라서 mocking을 사용하여 UserRepository 클래스의 GetUserByName 함수를 대체할 수 있음

 

이를 위해 UserRepository 클래스를 인터페이스로 추상화하고, 이 인터페이스를 상속하는 MockUserRepository 클래스를 작성

class IUserRepository {
public:
    virtual std::shared_ptr<User> GetUserByName(const std::string& name) = 0;
};

class MockUserRepository : public IUserRepository {
public:
    MOCK_METHOD1(GetUserByName, std::shared_ptr<User>(const std::string& name));
};

 

여기서 MOCK_METHOD1 매크로는 IUserRepository 클래스의 GetUserByName 함수를 대체하기 위한 mocking 함수를 생성

이제 LoginService 클래스의 Authenticate 함수를 테스트하려면 MockUserRepository 클래스의 GetUserByName 함수를 호출

 

TEST(LoginServiceTest, TestAuthenticate) {
    // Mocking을 사용하여 UserRepository 대체
    MockUserRepository repository;
    LoginService service;

    // mocking 함수 설정
    EXPECT_CALL(repository, GetUserByName("Alice"))

 

 

  1. Mocking with multiple dependencies (다수의 의존성을 가진 클래스에 대해 mocking을 수행하는 예제)

아래 코드에서는 ClassA와 ClassB 두 개의 클래스가 ClassC에서 의존성으로 사용되며,

ClassC의 테스트를 위해 ClassA와 ClassB를 mocking 

class ClassA {
public:
  virtual void funcA() = 0;
};

class ClassB {
public:
  virtual void funcB() = 0;
};

class ClassC {
public:
  ClassC(std::shared_ptr<ClassA> a, std::shared_ptr<ClassB> b) 
    : a_(a), b_(b) {}

  void funcC() {
    a_->funcA();
    b_->funcB();
  }

private:
  std::shared_ptr<ClassA> a_;
  std::shared_ptr<ClassB> b_;
};

TEST(ClassCTest, TestFuncC) {
  // Mock ClassA and ClassB
  auto mockA = std::make_shared<MockClassA>();
  auto mockB = std::make_shared<MockClassB>();

  // Set expectations
  EXPECT_CALL(*mockA, funcA());
  EXPECT_CALL(*mockB, funcB());

  // Inject mocks into ClassC
  ClassC c(mockA, mockB);

  // Test ClassC's funcC
  c.funcC();
}

 

 

2. Mocking with non-virtual interface (가상 함수가 아닌 인터페이스를 사용하는 클래스에 대해 mocking을 수행하는 예제)

 

아래 코드에서는 ClassD가 ClassE를 의존성으로 사용하며, ClassD를 mocking

ClassD는 인터페이스인 ID를 구현하며, ClassE는 ID를 통해 ClassD와 상호작용함

class ID {
public:
  virtual void funcD() = 0;
};

class ClassD : public ID {
public:
  void funcD() override {
    funcE();
  }

  virtual void funcE() {
    // implementation
  }
};

class ClassE {
public:
  ClassE(ID& d) : d_(d) {}

  void funcE() {
    d_.funcD();
  }

private:
  ID& d_;
};

class MockClassD : public ID {
public:
  MOCK_METHOD(void, funcD, (), (override));
};

TEST(ClassETest, TestFuncE) {
  // Mock ClassD
  MockClassD mockD;

  // Set expectations
  EXPECT_CALL(mockD, funcD());

  // Create ClassE with mockD
  ClassE e(mockD);

  // Test ClassE's funcE
  e.funcE();
}

 

 

3. Partial Mocking (Partial Mocking을 사용하여 클래스의 일부 함수만 mocking 하는 예제)

아래 코드에서는 ClassF가 funcF와 funcG 함수를 가지며, funcF를 mocking 

funcG 함수는 실제로 호출되며, ClassF의 인스턴스는 Partial Mocking이 적용

 

class ClassF {
public:
  virtual void funcF() {
    // implementation
  }

  void funcG() {
    // implementation
  }
};

TEST(ClassFTest, TestFuncF) {

 

 

+ QNX 에서  WillOnce 사용 불가.

 

이 경우에는 대신 OnCall 함수를 사용하여 함수 호출 시점에서 특정 값을 반환하도록 설정

 

class MyClass {
public:
    virtual int GetValue() const { return 0; }
};

class MyMock : public MyClass {
public:
    MOCK_CONST_METHOD0(GetValue, int());
};

 

MyMock 클래스에서 GetValue() 함수를 mock으로 설정하고,

WillOnce 함수를 사용하여 값을 반환하는 것 대신,

OnCall 함수를 사용하여 특정 값을 반환하도록 설정할 수 있습니다.

 

즉  다음과 같은 방법으로 설정할 수 있음

MyMock mock;
EXPECT_CALL(mock, GetValue())
    .WillOnce(Invoke([&]() {
        // OnCall 대신에 직접 처리하면 됩니다.
        return 42;
    }));

이제 MyMockGetValue() 함수가 호출되면, 42를 반환하도록 설정되어 있음

이렇게 하면 WillOnce 함수를 사용할 수 없는 환경에서도 유용한 방법으로 mocking을 구현할 수 있음

 

 

예제2

#include <iostream>
#include <memory>

class IDataReader {
public:
    virtual ~IDataReader() = default;
    virtual int ReadData(char* buffer, int size) = 0;
};

class MockDataReader : public IDataReader {
public:
    MOCK_METHOD(int, ReadData, (char* buffer, int size), (override));
};

class DataProcessor {
public:
    explicit DataProcessor(std::shared_ptr<IDataReader> reader) : reader_(std::move(reader)) {}

    bool ProcessData() {
        char buffer[1024];
        int bytes_read = reader_->ReadData(buffer, sizeof(buffer));

        if (bytes_read < 0) {
            std::cerr << "Error reading data" << std::endl;
            return false;
        }

        // process the data...

        return true;
    }

private:
    std::shared_ptr<IDataReader> reader_;
};

class MockDataReaderWrapper : public IDataReader {
public:
    MockDataReaderWrapper(MockDataReader& mock_reader) : mock_reader_(mock_reader) {}

    int ReadData(char* buffer, int size) override {
        return mock_reader_.ReadData(buffer, size);
    }

private:
    MockDataReader& mock_reader_;
};

TEST(DataProcessorTest, ProcessDataTest) {
    MockDataReader mock_reader;
    MockDataReaderWrapper mock_reader_wrapper(mock_reader);
    DataProcessor processor(std::make_shared<MockDataReaderWrapper>(mock_reader_wrapper));

    EXPECT_CALL(mock_reader, ReadData(_, _)).WillOnce(Return(10));

    ASSERT_TRUE(processor.ProcessData());
}

이 예제에서는 IDataReader라는 인터페이스를 정의하고, 이를 구현하는 MockDataReader 클래스를 만들었음

그리고 DataProcessor 클래스는 IDataReader 객체를 인자로 받아 데이터를 처리함

 

위 예제에서는 WillOnce 함수 대신 MockDataReaderWrapper 클래스를 만들어 사용

MockDataReaderWrapper 클래스는 IDataReader 인터페이스를 구현하면서, 내부적으로 MockDataReader 객체를 참조 DataProcessor 클래스에서는 std::shared_ptr<IDataReader> 타입으로 MockDataReaderWrapper 객체를 받아 처리

마지막으로 EXPECT_CALL 매크로를 사용하여 MockDataReader 클래스의 ReadData 함수가 호출될 때 반환 값을 지정

이 때 ReadData 함수는 MockDataReaderWrapper 클래스를 통해 호출됨

이 예제에서는 MockDataReaderWrapper 클래스를 사용하여 QNX 환경에서 WillOnce 함수를 대체할 수 있음

 

 

'유닛 테스트(unit test)' 카테고리의 다른 글

[C++] google test - gmock #2  (0) 2023.03.25

+ Recent posts