단위 테스트 란 무엇입니까
모의 이란
모킹(Mocking)은 단위 테스트에서 사용되는 기법 중 하나로, 의존하는 코드를 테스트할 때 종속성을 모킹된 객체로 대체하여 테스트하는 것을 의미합니다.
즉, 종속 코드가 호출될 때 코드는 호출 개체 대신 가짜 개체를 사용하여 호출합니다.
이 가짜 개체는 실제 개체와 동일한 인터페이스를 제공하지만 미리 정의된 테스트 사례에 따라 적절한 결과를 반환합니다.
이렇게 하면 종속 코드의 결과를 예측할 수 있습니다.
또한 종속 코드를 모의 개체로 대체하여 개체가 실제 시스템에 영향을 주지 않고 코드를 테스트할 수 있습니다.
Queue라는 클래스가 있다고 가정하면 Queue가 의존하는 클래스인 Data라는 클래스를 조롱한 예입니다.
// 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 클래스를 모의합니다.
Queue 클래스에서 Data 개체는 생성자에 의해 수신되고 멤버 변수로 저장됩니다.
size() 메서드는 Data 객체의 getSize() 메서드를 호출하여 반환 값을 반환합니다.
QueueTest에서 MockData 객체를 생성하고 Queue 객체를 생성할 때 전달합니다.
EXPECT_CALL 매크로를 사용하여 MockData 객체의 getSize() 메서드가 호출될 때 3을 반환하도록 지정합니다.
마지막으로 ASSERT_EQ 매크로를 사용하여 Queue 객체의 size() 메서드가 3을 반환하는지 확인합니다.
이러한 방식으로 모킹을 사용하면 Queue 클래스가 Data 클래스에 의존하더라도 테스트를 실행할 수 있습니다.
실제 데이터베이스에 접근하지 않고 데이터베이스를 대체하기 위해 모킹을 사용하는 예
예를 들어, 데이터베이스에서 사용자 정보를 검색하고 로그인 인증을 수행하는 기능이 있습니다.
이 함수가 데이터베이스에 직접 연결하고 쿼리를 실행하므로 테스트에 시간이 많이 걸린다고 가정합니다.
이 경우 모의를 사용하여 데이터베이스에 연결하지 않고 테스트를 실행할 수 있습니다.
먼저 사용자 정보를 나타내는 사용자 클래스는 다음과 같이 설계할 수 있다.
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");
}
};
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의 인증 기능을 테스트하려면 UserRepository 클래스의 GetUserByName 기능을 호출해야 합니다.
그러나 이 기능을 테스트하는 것은 데이터베이스에 직접 연결되기 때문에 시간이 많이 걸립니다.
따라서 조롱을 사용하여 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 함수를 대체할 모의 함수를 만듭니다.
이제 LoginService 클래스의 Authenticate 함수를 테스트하기 위해 MockUserRepository 클래스의 GetUserByName 함수를 호출합니다.
TEST(LoginServiceTest, TestAuthenticate) {
// Mocking을 사용하여 UserRepository 대체
MockUserRepository repository;
LoginService service;
// mocking 함수 설정
EXPECT_CALL(repository, GetUserByName("Alice"))
- 여러 종속성을 사용하여 모의하기(여러 종속성이 있는 클래스를 모의하는 예)
다음 코드에서 두 클래스 ClassA와 ClassB는 ClassC의 종속성으로 사용됩니다.
ClassC를 테스트하기 위해 모의 ClassA 및 ClassB
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. 비가상 인터페이스로 모킹(가상 함수 대신 인터페이스를 사용하는 클래스를 모킹하는 예)
아래 코드에서 ClassD는 ClassE를 종속성으로 사용하고 ClassD는 조롱됩니다.
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. 부분 목킹(부분 목킹으로 클래스의 일부 함수만 목킹한 예)
아래 코드에서 ClassF에는 funcF 및 funcG 함수가 있으며 funcF는 모킹됩니다.
funcG 함수가 실제로 호출되고 ClassF의 인스턴스에 부분 모킹이 적용됩니다.
class ClassF {
public:
virtual void funcF() {
// implementation
}
void funcG() {
// implementation
}
};
TEST(ClassFTest, TestFuncF) {
+ QNX에서 WillOnce를 사용할 수 없습니다.
이 경우 대신 통화 함수를 사용하여 함수가 호출될 때 특정 값을 반환합니다.
class MyClass {
public:
virtual int GetValue() const { return 0; }
};
class MyMock : public MyClass {
public:
MOCK_CONST_METHOD0(GetValue, int());
};
마이모크 클래스 값 가져오기() 함수를 모의로 설정하고,
윌원스 함수를 사용하여 값을 반환하는 대신,
통화 함수를 사용하여 특정 값을 반환하도록 설정할 수 있습니다.
즉, 다음과 같이 설정할 수 있습니다.
MyMock mock;
EXPECT_CALL(mock, GetValue())
.WillOnce(Invoke((&)() {
// OnCall 대신에 직접 처리하면 됩니다.
return 42;
}));
지금 마이모크~에서 값 가져오기() 호출되면 함수는 42를 반환하도록 설정됩니다.
이 방법 윌원스 기능을 사용할 수 없는 환경에서 모킹을 현명하게 구현할 수도 있습니다.
예 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 객체를 매개변수로 받아 데이터를 처리합니다.
위의 예에서 MockDataReaderWrapper 클래스가 생성되어 WillOnce 함수 대신 사용됩니다.
MockDataReaderWrapper 클래스는 IDataReader 인터페이스를 구현하고 MockDataReader 개체를 내부적으로 참조합니다. DataProcessor 클래스는 std::shared_ptr 유형의 MockDataReaderWrapper 객체를 수신하고 처리합니다.
마지막으로 EXPECT_CALL 매크로를 사용하여 MockDataReader 클래스의 ReadData 함수를 호출할 때 반환 값을 지정합니다.
이때 MockDataReaderWrapper 클래스를 통해 ReadData 함수가 호출됩니다.
이 예에서 MockDataReaderWrapper 클래스는 QNX 환경에서 WillOnce 함수를 대체하는 데 사용할 수 있습니다.