TDD/단위 테스트

[단위 테스트] 목 활용하기

로파이 2022. 12. 24. 20:27

테스트 대상 시스템과 외부 의존성간 상호작용을 검증하기 위해 사용되는 목 활용 모범 사례를 알아본다.

 

CRM 예제에서 목의 사용

TEST(TestUserController, 통합_테스트_이메일_변경_사내도메인에서_외부도메인으로) {
	// given
	auto db = new Database(string("connection"));
	auto insertUser = User::CreateUser("user@mycorp.com", UserType::Employee);
	auto insertCompany = Company::CreateCompany("mycorp.com", 1);

	db->SaveUser(insertUser);
	db->SaveCompany(insertCompany);

	auto messageBusMock = new MockMessageBus();
	auto sut = UserController(db, messageBusMock);

	EXPECT_CALL(*messageBusMock, SendEmailChangedMessage(insertUser._userId, "new@gmail.com")).Times(1);
    
    //...
}

외부 비관리 의존성에 해당하는 메시지 버스를 위해 목으로 대체하여 테스트를 진행하였다.

 

더 나은 목 설계

 

실제 API와 가까운 외부 의존성을 선택하는 목

만약 IMessageBus 인터페이스에서 IBus의 구현체를 사용한다면,

class IMessageBus {
protected:
	IBus* _bus;

public:
	virtual void SendEmailChangedMessage(int, std::string) abstract;
};
void MessageBus::SendEmailChangedMessage(int userId, std::string emailAddress)
{
	_bus->Send(Format("user id %d changed email address to %s", userId, emailAddress.c_str()));
}

실제 목을 작성할 때도 IMessageBus 대신 IBus에 대한 목 클래스를 정의하여 테스트에 사용하는 것이 좋다.

IBus는 실제 API와 가장 가까운 외부 의존성에 대한 인터페이스이며 가장 일반화된 함수로 회귀 방지를 극대화 할 수 있다.

 

목을 스파이로 대체하기

목과 마찬가지로 테스트를 수행하는 대역이다. 목은 목 라이브러리를 사용하여 정의하지만 스파이는 수기로 내용을 정의해야한다.

스파이는 테스트를 위한 코드를 작성하는 것으로 테스트 코드가 더 보기 쉽고 편리하도록 하기 위해 작성되어질 수 있다.

class BusSpy : public IBus {
private:
	vector<string> _sentMessages;

public:
	void Send(string message) {
		_sentMessages.push_back(message);
	}

	BusSpy& ShouldSendNumberOfMessages(int number) {
		int msgCount = (int)_sentMessages.size();
		// ASSERT_EQ(msgCount, number)
		return *this;
	}

	BusSpy& WithEmailSendMessage(int userId, string newEmail) {
		// ASSERT_EQ("~", ); 메시지 검증
		return *this;
	}
};

- google test에서 사용자 클래스내 ASSERT_* 구문 정의는 지원되지 않는다.

 

목은 통합 테스트에서 사용되어야 한다.

도메인과 컨트롤러 역할이 잘 분리된 설계는 비관리 의존성이 포함된 컨트롤러를 위한 테스트로 통합 테스트로 간주되어 진다.

 

테스트당 목이 하나일 필요는 없다.

외부 의존성의 개수에 따라 여러 개가 될 수 있다.

목을 통해 검증할 상호작용이 모두 테스트 되어지는 지 확인한다.

 

외부 의존성에 대한 직접적인 API를 감싸는 어댑터(래퍼 클래스)를 만들고 그 어댑터에 대한 목을 작성하여라

IBus 인터페이스 처럼 실제 어플리케이션 서비스 계층에서 사용되는 명세에 대해서만 목을 작성해야한다.

IBus와 같은 인터페이스가 없다면 외부 라이브러리 혹은 API에 대한 어댑터를 만들고 그에 대한 목을 작성한다.

 

결론

  • 시스템의 끝단, API와 가장 가까운 인터페이스를 통해 목을 설계하여라
  • 스파이는 목을 직접 작성한 예이다. 보기 쉬운 테스트 코드를 위한 클래스이다.
  • 통합 테스트에서 컨트롤러를 테스트할 목적으로 목을 사용해야한다.
  • 실제 어플리케이션 계층에서 관리되는 타입만 목으로 작성하여라

참고
Unit Testing 단위 테스트, 블라디미르 코리코프 지음
https://www.sandordargo.com/blog/2019/04/24/parameterized-testing-with-gtest#parameterizedtests
https://github.com/jinsnowy/UnitTest_Study