백엔드 테스트를 위한 Mock, Stub, Fake 실용적인 전략
Ethan Miller
Product Engineer · Leapcell

소개
백엔드 개발의 복잡한 세계에서 애플리케이션의 신뢰성과 견고성을 보장하는 것이 무엇보다 중요합니다. 이는 종종 비즈니스 로직부터 데이터베이스 상호 작용, 외부 API 호출에 이르기까지 다양한 구성 요소를 철저하게 테스트하는 것으로 이어집니다. 그러나 의존하는 서비스가 없거나, 설정하기 비싸거나, 비결정성을 유발하는 경우 일반적인 단위 테스트 또는 통합 테스트는 번거롭거나 느리거나 심지어 불가능할 수 있습니다. 여기서 테스트 대역(Test Double)인 Mock, Stub, Fake를 전략적으로 적용하는 것이 매우 유용해집니다. 실제 의존성을 제어 가능한 대체물로 지능적으로 대체함으로써 진정한 단위 격리를 달성하고, 테스트 실행 속도를 높이며, 예측 가능한 테스트 환경을 만들 수 있습니다. 이 글에서는 백엔드 테스트 프로세스를 간소화하기 위한 이러한 강력한 기법의 효과적인 활용법을 자세히 살펴봅니다.
핵심 개념 설명
적용 방법을 살펴보기 전에 Mock, Stub, Fake의 고유한 역할를 이해하는 것이 중요합니다. 종종 혼용되지만 테스트 환경 내에서 다른 목적을 수행합니다.
-
Stubs: Stub은 테스트 중에 발생하는 메서드 호출에 대한 미리 정의된 응답을 보유하는 객체입니다. 기본적으로 메서드 호출에 대한 미리 정의된 응답을 제공하여 테스트가 외부 시스템에 의존하지 않도록 합니다. Stub은 주로 테스트 대상 시스템(SUT, System Under Test)에 데이터를 제공하는 데 중점을 둡니다. Stub 자체에 대해 단언하는 것이 아니라 SUT의 동작에 대해 단언합니다.
-
Mocks: Mock은 더 정교한 유형의 테스트 대역입니다. Stub과 마찬가지로 미리 정의된 값을 반환할 수 있습니다. 그러나 Mock은 또한 상호 작용을 검증할 수 있도록 합니다. 특정 메서드가 어떻게 호출되었는지, 몇 번 호출되었는지, 어떤 인수로 호출되었는지 단언할 수 있습니다. Mock은 객체 간의 상호 작용을 테스트하고 명령 호출을 검증하는 데 중요합니다.
-
Fakes: Fake는 테스트에 종종 사용되는 인터페이스 또는 클래스의 경량 구현입니다. 실제 작동하는 동작을 가지고 있지만 프로덕션 환경에는 적합하지 않습니다. 일반적인 예는 실제 데이터베이스 서버 대신 테스트에 사용되는 인메모리 데이터베이스입니다. Fake는 실제와는 단순화된 버전이라 할지라도 실제로 무언가를 수행합니다.
효과적인 구현 및 적용
일반적인 백엔드 시나리오인 데이터베이스 및 외부 이메일 서비스와 상호 작용하는 사용자 서비스에 중점을 두고 실제 코드를 통해 이러한 개념을 설명해 보겠습니다.
사용자를 생성하고 환영 이메일을 보내야 하는 Python의 간단한 UserService
를 고려해보세요.
# user_service.py class User: def __init__(self, user_id, name, email): self.user_id = user_id self.name = name self.email = email class Database: def save_user(self, user): print(f"Saving user {user.name} to database...") # 실제 데이터베이스 상호 작용 시뮬레이션 pass class EmailService: def send_email(self, recipient, subject, body): print(f"Sending email to {recipient} with subject '{subject}'...") # 실제 이메일 전송 시뮬레이션 pass class UserService: def __init__(self, db: Database, email_service: EmailService): self.db = db self.email_service = email_service def create_user(self, user_id, name, email): user = User(user_id, name, email) self.db.save_user(user) self.email_service.send_email(email, "Welcome!", f"Hello {name}, welcome to our service!") return user
제어된 데이터에 Stub 사용
create_user
를 테스트할 때 실제 데이터베이스나 이메일 전송 메커니즘에 관계없이 UserService
가 사용자 데이터를 올바르게 처리하는지 확인하고 싶습니다. Database
에 대한 Stub은 제어된 환경을 제공할 수 있습니다.
# test_user_service_stub.py import unittest from unittest.mock import MagicMock from user_service import UserService, User, Database, EmailService class TestUserServiceWithStubs(unittest.TestCase): def test_create_user_saves_and_sends_welcome_email(self): # .save_user가 실제 DB와 상호 작용하지 않도록 데이터베이스 Stub 처리 # Stub의 경우, SUT가 의존하는 반환 값이 없는 한 반환 값을 구성하지 않고 MagicMock을 사용할 수 있습니다. mock_db = MagicMock(spec=Database) mock_email_service = MagicMock(spec=EmailService) # 여기서는 Stub 역할을 하며, 아직 호출을 검증하지 않습니다. user_service = UserService(mock_db, mock_email_service) user = user_service.create_user("123", "John Doe", "john@example.com") self.assertIsInstance(user, User) self.assertEqual(user.name, "John Doe") # Stub 테스트에서는 일반적으로 Stub 자체와의 상호 작용을 검증하지 않지만, # SUT의 출력이나 상태 변화에 중점을 둡니다. # 그러나 시연을 위해 MagicMock을 Stub으로 사용하는 방법을 보여줍니다. # 일반적으로 UserService의 반환 값이나 내부 상태(있는 경우)를 검증할 것입니다. # mock_db와 mock_email_service를 순전히 Stub으로 취급할 때, # 우리는 주로 UserService 자체의 동작에 관심이 있습니다. # create_user가 User 객체를 반환한다는 사실이 우리의 주요 단언입니다.
이 Stub 예제에서 MagicMock(spec=Database)
는 mock_db
가 Database
객체처럼 동작하도록 보장하지만 실제 데이터베이스에 연결하지는 않습니다. 주로 UserService
의 반환 값(user
객체)에 중점을 두어 mock_db
와 mock_email_service
가 복잡한 동작 없이 종속성을 충족하는 Stub 역할을 합니다.
상호 작용 검증을 위한 Mock 사용
이제 Mock을 사용하여 데이터베이스의 save_user
와 이메일 서비스의 send_email
이 올바른 인수로 실제로 호출되었는지 확인해 보겠습니다.
# test_user_service_mock.py import unittest from unittest.mock import MagicMock from user_service import UserService, User, Database, EmailService class TestUserServiceWithMocks(unittest.TestCase): def test_create_user_interacts_with_dependencies_correctly(self): mock_db = MagicMock(spec=Database) mock_email_service = MagicMock(spec=EmailService) user_service = UserService(mock_db, mock_email_service) user = user_service.create_user("123", "John Doe", "john@example.com") # Mock과의 상호 작용에 대한 단언: mock_db.save_user.assert_called_once_with(user) mock_email_service.send_email.assert_called_once_with( "john@example.com", "Welcome!", "Hello John Doe, welcome to our service!" ) self.assertIsInstance(user, User) self.assertEqual(user.name, "John Doe")
여기서 mock_db
와 mock_email_service
는 Mock으로 사용됩니다. UserService
가 소비하는 반환 값이 없기 때문에 그들의 메서드(save_user
, send_email
)에 대한 반환 값을 구성하지 않았습니다. 주요 차이점은 assert_called_once_with
메서드로, 상호 작용과 해당 상호 작용에 전달된 인수를 명시적으로 검증합니다. 이는 UserService
가 종속성에 대한 호출을 올바르게 조정하는지 확인하는 데 중요합니다.
단순화된 동작을 위한 Fake 사용
Database
클래스에 더 복잡한 find_user
메서드가 있다고 가정하고 데이터베이스에서 읽는 UserService
메서드를 테스트하고 싶다고 가정해 봅시다. Fake 데이터베이스는 간단한 인메모리 구현을 제공할 수 있습니다.
# user_service.py (추가 메서드) # ... (기존 클래스) class UserService: # ... (기존 __init__ 및 create_user) def get_user_by_id(self, user_id): return self.db.find_user(user_id)
# test_user_service_fake.py import unittest from user_service import UserService, User, Database, EmailService # Fake Database 구현 class FakeDatabase(Database): def __init__(self): self.users = {} # 인메모리 저장소 def save_user(self, user): self.users[user.user_id] = user print(f"Fake DB: Saved user {user.name}") def find_user(self, user_id): print(f"Fake DB: Finding user {user_id}") return self.users.get(user_id) class TestUserServiceWithFakes(unittest.TestCase): def test_get_user_by_id_retrieves_user_from_fake_db(self): fake_db = FakeDatabase() mock_email_service = MagicMock(spec=EmailService) # 이메일 서비스는 여전히 Mock/Stub일 수 있습니다. user_service = UserService(fake_db, mock_email_service) # 먼저, FakeDatabase의 save_user를 사용하여 사용자를 생성합니다. user_service.create_user("456", "Jane Doe", "jane@example.com") # 이제 FakeDatabase에 저장된 데이터를 사용하여 get_user_by_id 메서드를 테스트합니다. retrieved_user = user_service.get_user_by_id("456") self.assertIsNotNone(retrieved_user) self.assertEqual(retrieved_user.name, "Jane Doe") self.assertEqual(retrieved_user.email, "jane@example.com") self.assertEqual(retrieved_user.user_id, "456")
이 시나리오에서 FakeDatabase
는 Fake입니다. Database
인터페이스를 구현하지만 실제 데이터베이스 연결 대신 인메모리 사전을 사용하여 사용자를 저장합니다. 이를 통해 실제 데이터베이스 연결의 오버헤드나 복잡성 없이 일부 단순화된 데이터베이스와 유사한 동작을 포함하여 get_user_by_id
를 테스트할 수 있습니다. Fake는 여전히 빠르고 제어되어야 하는 통합 테스트에 훌륭합니다.
결론
Mock, Stub, Fake를 효과적으로 사용하는 것은 견고한 백엔드 테스트의 초석입니다. Stub은 단순화된 데이터를 제공하고, Mock은 상호 작용을 검증하며, Fake는 경량의 작동하는 구현을 제공하며, 각 기능은 테스트 격리 및 효율성 달성에 고유한 역할을 수행합니다. 이러한 테스트 대역을 마스터하면 개발자는 자신의 코드를 신뢰할 수 있습니다. 각 단위가 통제되고 예측 가능한 조건에서 안정적으로 작업을 수행한다는 것을 알기 때문입니다. 이러한 도구는 백엔드 구성 요소의 빠르고 안정적인 검증을 가능하게 합니다.