跳到主要内容

Mock 测试详解

Mock 测试是一种通过创建模拟对象来替代真实依赖的测试技术。它使我们能够在隔离环境中测试代码,提高测试速度和可靠性。

为什么需要 Mock?

在实际开发中,代码往往依赖外部系统:数据库、网络服务、文件系统等。这些依赖会带来测试上的挑战:

问题影响
执行缓慢数据库查询、网络请求耗时较长
不稳定外部服务可能不可用或响应变化
难以控制无法控制外部系统的状态和返回值
副作用测试可能修改真实数据
环境复杂需要搭建完整的测试环境

Mock 通过模拟这些依赖,解决上述问题:

测试替身(Test Doubles)

Gerard Meszaros 提出了「测试替身」的概念,用来统称所有用于测试的替代对象。测试替身分为五种类型:

Dummy(哑对象)

Dummy 是最简单的测试替身,它只是用来填充参数列表,从不被实际使用。

# Python 示例
class DummyLogger:
"""Dummy Logger - 只是为了满足参数要求"""
pass

def test_create_user():
# DummyLogger 只是一个占位符,不会被实际调用
user_service = UserService(DummyLogger())
user = user_service.create("张三")
assert user.name == "张三"
// Java 示例
public class DummyLogger implements Logger {
// 所有方法都是空实现
@Override public void info(String msg) {}
@Override public void error(String msg) {}
}

@Test
void testCreateUser() {
UserService service = new UserService(new DummyLogger());
User user = service.create("张三");
assertEquals("张三", user.getName());
}
// JavaScript 示例
const dummyLogger = {}; // 空对象作为 Dummy

test('create user', () => {
const userService = new UserService(dummyLogger);
const user = userService.create('张三');
expect(user.name).toBe('张三');
});

Fake(伪造对象)

Fake 有真实的实现,但采取了某些捷径,不适合生产环境使用。最常见的例子是内存数据库。

# Python 示例
class FakeDatabase:
"""Fake 数据库 - 使用内存存储代替真实数据库"""

def __init__(self):
self._data = {}
self._next_id = 1

def save(self, entity):
if entity.id is None:
entity.id = self._next_id
self._next_id += 1
self._data[entity.id] = entity
return entity

def find_by_id(self, id):
return self._data.get(id)

def delete(self, id):
if id in self._data:
del self._data[id]
return True
return False

# 测试中使用 Fake
def test_user_repository():
fake_db = FakeDatabase()
repo = UserRepository(fake_db)

user = User(name="张三", email="[email protected]")
saved = repo.save(user)

assert saved.id is not None
assert repo.find_by_id(saved.id).name == "张三"
// Java 示例
public class FakeUserRepository implements UserRepository {
private final Map<Long, User> storage = new HashMap<>();
private long nextId = 1;

@Override
public User save(User user) {
if (user.getId() == null) {
user.setId(nextId++);
}
storage.put(user.getId(), user);
return user;
}

@Override
public Optional<User> findById(Long id) {
return Optional.ofNullable(storage.get(id));
}

@Override
public void delete(Long id) {
storage.remove(id);
}
}

// 测试
@Test
void testUserRepository() {
UserRepository repo = new FakeUserRepository();
User user = new User("张三", "[email protected]");

User saved = repo.save(user);
assertNotNull(saved.getId());
}

Stub(桩)

Stub 为测试中的调用提供预设的返回值,不会对测试外的调用做出响应。

# Python 示例
from unittest.mock import Mock

class PaymentServiceStub:
"""支付服务 Stub - 总是返回成功"""

def charge(self, amount, currency="USD"):
# 总是返回预设的成功响应
return {
"status": "success",
"transaction_id": "txn_stub_123",
"amount": amount,
"currency": currency
}

def test_create_order():
payment_stub = PaymentServiceStub()
order_service = OrderService(payment_stub)

order = order_service.create_order(
items=[{"product_id": 1, "price": 100}],
user_id=1
)

assert order.status == "paid"
assert order.transaction_id == "txn_stub_123"
# 使用 Mock 库创建 Stub
from unittest.mock import Mock

def test_with_mock_stub():
# 创建 Stub
email_stub = Mock()
email_stub.send.return_value = True # 预设返回值

notification_service = NotificationService(email_stub)
result = notification_service.notify_user(1, "Hello")

assert result is True
// Java 示例
public class PaymentServiceStub implements PaymentService {
@Override
public PaymentResult charge(BigDecimal amount, String currency) {
// 总是返回成功
return new PaymentResult(
"success",
"txn_stub_123",
amount,
currency
);
}
}

@Test
void testCreateOrder() {
PaymentService paymentStub = new PaymentServiceStub();
OrderService orderService = new OrderService(paymentStub);

Order order = orderService.createOrder(
List.of(new OrderItem(1L, new BigDecimal("100"))),
1L
);

assertEquals("paid", order.getStatus());
}
// JavaScript 示例
class PaymentServiceStub {
charge(amount, currency = 'USD') {
return {
status: 'success',
transaction_id: 'txn_stub_123',
amount,
currency
};
}
}

test('create order with stub', () => {
const paymentStub = new PaymentServiceStub();
const orderService = new OrderService(paymentStub);

const order = orderService.createOrder(
[{ product_id: 1, price: 100 }],
1
);

expect(order.status).toBe('paid');
});

Spy(间谍)

Spy 是一种特殊的 Stub,它会记录调用信息,供测试后续验证。

# Python 示例
class EmailServiceSpy:
"""邮件服务 Spy - 记录所有调用"""

def __init__(self):
self.sent_emails = []
self.call_count = 0

def send(self, to, subject, body):
self.call_count += 1
self.sent_emails.append({
"to": to,
"subject": subject,
"body": body
})
return True

def was_called_with(self, to, subject):
for email in self.sent_emails:
if email["to"] == to and email["subject"] == subject:
return True
return False

def test_notification_service():
email_spy = EmailServiceSpy()
notification = NotificationService(email_spy)

notification.notify_user("[email protected]", "欢迎", "欢迎注册!")

# 验证调用
assert email_spy.call_count == 1
assert email_spy.was_called_with("[email protected]", "欢迎")
assert email_spy.sent_emails[0]["body"] == "欢迎注册!"
// Java 示例
public class EmailServiceSpy implements EmailService {
private final List<Email> sentEmails = new ArrayList<>();

@Override
public boolean send(String to, String subject, String body) {
sentEmails.add(new Email(to, subject, body));
return true;
}

public int getCallCount() {
return sentEmails.size();
}

public boolean wasCalledWith(String to, String subject) {
return sentEmails.stream()
.anyMatch(e -> e.getTo().equals(to) && e.getSubject().equals(subject));
}

public List<Email> getSentEmails() {
return Collections.unmodifiableList(sentEmails);
}
}

@Test
void testNotificationService() {
EmailServiceSpy emailSpy = new EmailServiceSpy();
NotificationService notification = new NotificationService(emailSpy);

notification.notifyUser("[email protected]", "欢迎", "欢迎注册!");

assertEquals(1, emailSpy.getCallCount());
assertTrue(emailSpy.wasCalledWith("[email protected]", "欢迎"));
}

Mock(模拟对象)

Mock 是最复杂的测试替身。它预先设置期望,验证交互行为是否符合预期。与 Stub 不同,Mock 关注的是「行为验证」而非「状态验证」。

# Python unittest.mock 示例
from unittest.mock import Mock, call

def test_order_service_with_mock():
# 创建 Mock
payment_mock = Mock()
email_mock = Mock()

# 设置期望
payment_mock.charge.return_value = {
"status": "success",
"transaction_id": "txn_123"
}

# 创建被测对象
order_service = OrderService(payment_mock, email_mock)

# 执行操作
order_service.create_order(
user_id=1,
items=[{"product_id": 1, "price": 100}],
email="[email protected]"
)

# 验证交互
payment_mock.charge.assert_called_once_with(
amount=100,
currency="USD"
)

email_mock.send.assert_called_once_with(
to="[email protected]",
subject="订单确认",
body=mock.ANY # 不关心具体内容
)
# 使用 patch 装饰器
from unittest.mock import patch, MagicMock

@patch('module.requests.get')
def test_fetch_user_data(mock_get):
# 配置 Mock
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {"id": 1, "name": "张三"}
mock_get.return_value = mock_response

# 执行测试
result = fetch_user_data(1)

# 验证结果
assert result["name"] == "张三"

# 验证调用
mock_get.assert_called_once_with("https://api.example.com/users/1")
// Java Mockito 示例
import org.mockito.*;
import static org.mockito.Mockito.*;

@Test
void testOrderServiceWithMock() {
// 创建 Mock 对象
PaymentService paymentMock = mock(PaymentService.class);
EmailService emailMock = mock(EmailService.class);

// 设置期望
when(paymentMock.charge(any(BigDecimal.class), anyString()))
.thenReturn(new PaymentResult("success", "txn_123"));

// 创建被测对象
OrderService orderService = new OrderService(paymentMock, emailMock);

// 执行操作
orderService.createOrder(1L, List.of(new OrderItem(1L, new BigDecimal("100"))));

// 验证交互
verify(paymentMock).charge(eq(new BigDecimal("100")), eq("USD"));
verify(emailMock).send(eq("[email protected]"), eq("订单确认"), anyString());

// 验证调用次数
verify(paymentMock, times(1)).charge(any(), any());
verify(emailMock, never()).send(any(), eq("错误"), any());
}
// JavaScript Jest 示例
test('order service with mock', () => {
// 创建 Mock 函数
const paymentMock = {
charge: jest.fn().mockReturnValue({
status: 'success',
transaction_id: 'txn_123'
})
};

const emailMock = {
send: jest.fn().mockReturnValue(true)
};

const orderService = new OrderService(paymentMock, emailMock);

orderService.createOrder(1, [{ product_id: 1, price: 100 }], '[email protected]');

// 验证调用
expect(paymentMock.charge).toHaveBeenCalledWith(100, 'USD');
expect(paymentMock.charge).toHaveBeenCalledTimes(1);

expect(emailMock.send).toHaveBeenCalledWith(
'[email protected]',
'订单确认',
expect.any(String)
);
});

各类型对比

类型用途验证方式适用场景
Dummy填充参数不验证只需要满足参数要求
Fake简化实现状态验证需要实际工作行为,如内存数据库
Stub预设返回值状态验证只需要特定返回值
Spy记录调用状态验证需要检查调用信息
Mock验证行为行为验证需要验证交互是否正确

Mock 最佳实践

1. 只 Mock 你拥有的接口

不要 Mock 你无法控制的外部库,而是封装它们然后 Mock 你的封装。

# ❌ 不好:直接 Mock requests 库
@patch('requests.get')
def test_fetch_data(mock_get):
mock_get.return_value.json.return_value = {"data": "test"}
result = fetch_data()
assert result == {"data": "test"}

# ✅ 好:Mock 自己封装的 HTTP 客户端
def test_fetch_data():
http_client_mock = Mock()
http_client_mock.get.return_value = {"data": "test"}

service = DataService(http_client_mock)
result = service.fetch_data()

assert result == {"data": "test"}
http_client_mock.get.assert_called_once_with("/api/data")

2. 验证行为而非实现细节

# ❌ 不好:验证内部实现
def test_send_notification():
email_service = Mock()
notification = NotificationService(email_service)

notification.notify_user(1, "Hello")

# 验证内部调用了私有方法
email_service._format_message.assert_called() # 过度关注实现

# ✅ 好:验证公开行为
def test_send_notification():
email_service = Mock()
notification = NotificationService(email_service)

notification.notify_user("[email protected]", "Hello")

# 验证最终行为
email_service.send.assert_called_once_with(
to="[email protected]",
subject="Notification",
body="Hello"
)

3. 避免过度 Mock

# ❌ 不好:Mock 太多,测试变得脆弱
@patch('module.UserService')
@patch('module.EmailService')
@patch('module.CacheService')
@patch('module.Database')
def test_create_user(db_mock, cache_mock, email_mock, user_mock):
# 测试变得难以理解和维护
pass

# ✅ 好:只 Mock 外部边界
def test_create_user():
email_mock = Mock()

service = UserService(email_mock) # 只 Mock 邮件服务
user = service.create("张三", "[email protected]")

assert user.name == "张三"
email_mock.send_welcome.assert_called_once()

4. 使用 spec 确保接口兼容

from unittest.mock import create_autospec, Mock

# ❌ 不好:Mock 可能与真实接口不一致
payment_mock = Mock()
payment_mock.charge("invalid arguments") # 不会报错

# ✅ 好:使用 autospec 确保接口一致
from payment import PaymentService

payment_mock = create_autospec(PaymentService)
payment_mock.charge("invalid") # 会报错:参数不匹配

5. 合理使用 patch

from unittest.mock import patch, MagicMock

# 方式1:装饰器(推荐)
@patch('module.external_api')
def test_with_decorator(mock_api):
mock_api.return_value = {"data": "test"}
result = my_function()
assert result is not None

# 方式2:上下文管理器
def test_with_context():
with patch('module.external_api') as mock_api:
mock_api.return_value = {"data": "test"}
result = my_function()
assert result is not None

# 方式3:手动启动/停止
def test_manual():
patcher = patch('module.external_api')
mock_api = patcher.start()
mock_api.return_value = {"data": "test"}

try:
result = my_function()
assert result is not None
finally:
patcher.stop()

各语言 Mock 工具

Python

# unittest.mock - 标准库
from unittest.mock import Mock, patch, MagicMock, call

# pytest-mock - pytest 插件
def test_with_pytest_mock(mocker):
mock_db = mocker.patch('module.Database')
mock_db.return_value.query.return_value = [{"id": 1}]

result = get_users()
assert len(result) == 1

# pytest-mock 的好处:自动清理 Mock

Java

// Mockito
import static org.mockito.Mockito.*;

@Test
void testWithMockito() {
// 创建 Mock
List<String> mockList = mock(List.class);

// 设置行为
when(mockList.get(0)).thenReturn("first");
when(mockList.size()).thenReturn(100);

// 使用 Mock
assertEquals("first", mockList.get(0));
assertEquals(100, mockList.size());

// 验证
verify(mockList).get(0);
verify(mockList, times(1)).get(anyInt());
}

// 使用注解
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;

@InjectMocks
private UserService userService;

@Test
void testFindUser() {
when(userRepository.findById(1L)).thenReturn(Optional.of(new User("张三")));

User user = userService.findById(1L);

assertEquals("张三", user.getName());
}
}

JavaScript/TypeScript

// Jest
test('mock examples', () => {
// Mock 函数
const mockFn = jest.fn();
mockFn.mockReturnValue('default');
mockFn.mockReturnValueOnce('first call');

expect(mockFn()).toBe('first call');
expect(mockFn()).toBe('default');

// Mock 模块
jest.mock('./api');
const api = require('./api');
api.getUser.mockResolvedValue({ id: 1, name: '张三' });

// Mock 定时器
jest.useFakeTimers();
const callback = jest.fn();
setTimeout(callback, 1000);
jest.advanceTimersByTime(1000);
expect(callback).toHaveBeenCalled();
});

// Mock 类
jest.mock('./PaymentService');
import PaymentService from './PaymentService';

test('order payment', () => {
PaymentService.mockImplementation(() => ({
charge: jest.fn().mockResolvedValue({ status: 'success' })
}));

const orderService = new OrderService(new PaymentService());
// ...
});

Go

// 使用接口和手动 Mock
type UserRepository interface {
FindByID(id int64) (*User, error)
Save(user *User) error
}

// 手动实现 Mock
type MockUserRepository struct {
users map[int64]*User
}

func (m *MockUserRepository) FindByID(id int64) (*User, error) {
if user, ok := m.users[id]; ok {
return user, nil
}
return nil, errors.New("user not found")
}

func (m *MockUserRepository) Save(user *User) error {
m.users[user.ID] = user
return nil
}

// 测试
func TestUserService(t *testing.T) {
mockRepo := &MockUserRepository{
users: map[int64]*User{
1: {ID: 1, Name: "张三"},
},
}

service := NewUserService(mockRepo)
user, err := service.GetByID(1)

if err != nil {
t.Errorf("unexpected error: %v", err)
}
if user.Name != "张三" {
t.Errorf("expected 张三, got %s", user.Name)
}
}

// 使用 testify/mock
import "github.com/stretchr/testify/mock"

type MockUserRepository struct {
mock.Mock
}

func (m *MockUserRepository) FindByID(id int64) (*User, error) {
args := m.Called(id)
return args.Get(0).(*User), args.Error(1)
}

func TestUserService(t *testing.T) {
mockRepo := new(MockUserRepository)
mockRepo.On("FindByID", int64(1)).Return(&User{ID: 1, Name: "张三"}, nil)

service := NewUserService(mockRepo)
user, err := service.GetByID(1)

assert.NoError(t, err)
assert.Equal(t, "张三", user.Name)
mockRepo.AssertExpectations(t)
}

Mock vs Stub 选择指南

选择原则

  1. 只需填充参数 → Dummy
  2. 需要工作实现但不适合生产 → Fake(如内存数据库)
  3. 只需要预设返回值 → Stub
  4. 需要验证方法是否被调用 → Mock
  5. 需要检查调用细节 → Spy

常见陷阱

1. Mock 返回 Mock

# ❌ 不好:Mock 链式调用
mock = Mock()
mock.get_user().get_profile().get_name().return_value = "张三" # 难以理解

# ✅ 好:分步设置
user_mock = Mock()
user_mock.get_profile.return_value.get_name.return_value = "张三"
mock.get_user.return_value = user_mock

# 或者更好的方式:使用真实对象或 Fake

2. 验证次数过多

# ❌ 不好:过度验证
verify(mock).method(any(), any(), any())
verify(mock, times(1)).method(any(), any(), any())
verify(mock, atLeastOnce()).method(any(), any(), any())

# ✅ 好:只验证关键行为
verify(mock).send_email(eq("[email protected]"), any())

3. 忘记重置 Mock

# ❌ 不好:Mock 状态在测试间共享
mock = Mock()

def test_one():
mock.foo()
assert mock.foo.called # 通过

def test_two():
# mock 仍然记录着上一个测试的调用!
assert not mock.foo.called # 失败

# ✅ 好:每个测试创建新的 Mock
def test_one():
mock = Mock()
mock.foo()
assert mock.foo.called

def test_two():
mock = Mock() # 新的 Mock
assert not mock.called

总结

Mock 测试是现代软件开发中不可或缺的技术。关键要点:

  • 理解类型:Dummy、Fake、Stub、Spy、Mock 各有用途
  • 适度使用:Mock 是手段,不是目的
  • 验证行为:关注外部可见的行为,而非内部实现
  • 保持简单:避免过度复杂的 Mock 设置
  • 接口优先:只 Mock 你拥有的接口

参考资源