MockingandStubbing mocking-stubbing

模拟和存根技术用于在测试中隔离代码单元,通过替换外部依赖项为受控的测试替身,以实现快速、可靠的单元测试。

测试 0 次安装 0 次浏览 更新于 3/4/2026

模拟和存根

概述

模拟和存根是隔离代码单元进行测试的基本技术,通过替换依赖项为受控的测试替身。这使得单元测试快速、可靠、专注,不依赖于外部系统,如数据库、API或文件系统。

何时使用

  • 从外部依赖项中隔离单元测试
  • 测试依赖于慢速操作的代码(数据库、网络)
  • 模拟错误条件和边缘情况
  • 验证对象之间的交互
  • 测试具有非确定性行为的代码(时间、随机性)
  • 避免在测试中进行昂贵的操作
  • 测试错误处理而不触发真实故障

测试替身类型

  • 存根:返回预定义值,不验证行为
  • 模拟:验证交互(方法调用、参数)
  • 间谍:包装真实对象,允许部分模拟
  • :工作实现,但简化(内存数据库)
  • 虚拟:传递但从未使用(填充参数列表)

指令

1. Jest 模拟(JavaScript/TypeScript)

基本模拟

// services/UserService.ts
import { UserRepository } from './UserRepository';
import { EmailService } from './EmailService';

export class UserService {
  constructor(
    private userRepository: UserRepository,
    private emailService: EmailService
  ) {}

  async createUser(userData: CreateUserDto) {
    const user = await this.userRepository.create(userData);
    await this.emailService.sendWelcomeEmail(user.email, user.name);
    return user;
  }

  async getUserStats(userId: string) {
    const user = await this.userRepository.findById(userId);
    if (!user) throw new Error('User not found');

    const orderCount = await this.userRepository.getOrderCount(userId);
    return { ...user, orderCount };
  }
}

// __tests__/UserService.test.ts
import { UserService } from '../UserService';
import { UserRepository } from '../UserRepository';
import { EmailService } from '../EmailService';

// 模拟依赖项
jest.mock('../UserRepository');
jest.mock('../EmailService');

describe('UserService', () => {
  let userService: UserService;
  let mockUserRepository: jest.Mocked<UserRepository>;
  let mockEmailService: jest.Mocked<EmailService>;

  beforeEach(() => {
    // 每次测试前清除所有模拟
    jest.clearAllMocks();

    // 创建模拟实例
    mockUserRepository = new UserRepository() as jest.Mocked<UserRepository>;
    mockEmailService = new EmailService() as jest.Mocked<EmailService>;

    userService = new UserService(mockUserRepository, mockEmailService);
  });

  describe('createUser', () => {
    it('should create user and send welcome email', async () => {
      // 安排
      const userData = {
        email: 'test@example.com',
        name: 'Test User',
        password: 'password123'
      };

      const createdUser = {
        id: '123',
        ...userData,
        createdAt: new Date()
      };

      mockUserRepository.create.mockResolvedValue(createdUser);
      mockEmailService.sendWelcomeEmail.mockResolvedValue(undefined);

      // 行动
      const result = await userService.createUser(userData);

      // 断言
      expect(result).toEqual(createdUser);
      expect(mockUserRepository.create).toHaveBeenCalledWith(userData);
      expect(mockUserRepository.create).toHaveBeenCalledTimes(1);
      expect(mockEmailService.sendWelcomeEmail).toHaveBeenCalledWith(
        userData.email,
        userData.name
      );
    });

    it('should not send email if user creation fails', async () => {
      // 安排
      mockUserRepository.create.mockRejectedValue(
        new Error('Database error')
      );

      // 行动 & 断言
      await expect(
        userService.createUser({ email: 'test@example.com' })
      ).rejects.toThrow('Database error');

      expect(mockEmailService.sendWelcomeEmail).not.toHaveBeenCalled();
    });
  });

  describe('getUserStats', () => {
    it('should return user with order count', async () => {
      // 安排
      const userId = '123';
      const user = { id: userId, name: 'Test User' };

      mockUserRepository.findById.mockResolvedValue(user);
      mockUserRepository.getOrderCount.mockResolvedValue(5);

      // 行动
      const result = await userService.getUserStats(userId);

      // 断言
      expect(result).toEqual({ ...user, orderCount: 5 });
      expect(mockUserRepository.findById).toHaveBeenCalledWith(userId);
      expect(mockUserRepository.getOrderCount).toHaveBeenCalledWith(userId);
    });

    it('should throw error if user not found', async () => {
      // 安排
      mockUserRepository.findById.mockResolvedValue(null);

      // 行动 & 断言
      await expect(userService.getUserStats('999')).rejects.toThrow(
        'User not found'
      );

      expect(mockUserRepository.getOrderCount).not.toHaveBeenCalled();
    });
  });
});

间谍功能

// services/PaymentService.js
const stripe = require('stripe');

class PaymentService {
  async processPayment(amount, currency, customerId) {
    const charge = await stripe.charges.create({
      amount: amount * 100,
      currency,
      customer: customerId,
    });

    this.logPayment(charge.id, amount);
    return charge;
  }

  logPayment(chargeId, amount) {
    console.log(`Payment processed: ${chargeId} for $${amount}`);
  }
}

// __tests__/PaymentService.test.js
describe('PaymentService', () => {
  let paymentService;
  let stripeMock;

  beforeEach(() => {
    // 模拟Stripe模块
    stripeMock = {
      charges: {
        create: jest.fn(),
      },
    };
    jest.mock('stripe', () => jest.fn(() => stripeMock));

    paymentService = new PaymentService();
  });

  it('should process payment and log', async () => {
    // 安排
    const mockCharge = { id: 'ch_123', amount: 5000 };
    stripeMock.charges.create.mockResolvedValue(mockCharge);

    // 间谍内部方法
    const logSpy = jest.spyOn(paymentService, 'logPayment');

    // 行动
    await paymentService.processPayment(50, 'usd', 'cus_123');

    // 断言
    expect(stripeMock.charges.create).toHaveBeenCalledWith({
      amount: 5000,
      currency: 'usd',
      customer: 'cus_123',
    });
    expect(logSpy).toHaveBeenCalledWith('ch_123', 50);

    logSpy.mockRestore();
  });
});

2. Python模拟unittest.mock

# services/order_service.py
from typing import Optional
from repositories.order_repository import OrderRepository
from services.payment_service import PaymentService
from services.notification_service import NotificationService

class OrderService:
    def __init__(
        self,
        order_repository: OrderRepository,
        payment_service: PaymentService,
        notification_service: NotificationService
    ):
        self.order_repository = order_repository
        self.payment_service = payment_service
        self.notification_service = notification_service

    def create_order(self, user_id: str, items: list) -> Order:
        """Create and process a new order."""
        order = self.order_repository.create({
            'user_id': user_id,
            'items': items,
            'status': 'pending'
        })

        try:
            payment = self.payment_service.process_payment(
                order.id,
                order.total
            )
            order.status = 'paid'
            order.payment_id = payment.id
            self.order_repository.update(order)

            self.notification_service.send_order_confirmation(
                order.user_id,
                order.id
            )
        except PaymentError as e:
            order.status = 'failed'
            self.order_repository.update(order)
            raise

        return order

# tests/test_order_service.py
import pytest
from unittest.mock import Mock, MagicMock, patch, call
from services.order_service import OrderService
from exceptions import PaymentError

class TestOrderService:
    @pytest.fixture
    def mock_dependencies(self):
        """Create mock dependencies."""
        return {
            'order_repository': Mock(),
            'payment_service': Mock(),
            'notification_service': Mock()
        }

    @pytest.fixture
    def order_service(self, mock_dependencies):
        """Create OrderService with mocked dependencies."""
        return OrderService(**mock_dependencies)

    def test_create_order_success(self, order_service, mock_dependencies):
        """Test successful order creation and payment."""
        # 安排
        user_id = 'user-123'
        items = [{'product_id': 'p1', 'quantity': 2}]

        mock_order = Mock(
            id='order-123',
            total=99.99,
            status='pending',
            user_id=user_id
        )
        mock_payment = Mock(id='payment-123')

        mock_dependencies['order_repository'].create.return_value = mock_order
        mock_dependencies['payment_service'].process_payment.return_value = mock_payment

        # 行动
        result = order_service.create_order(user_id, items)

        # 断言
        assert result.status == 'paid'
        assert result.payment_id == 'payment-123'

        mock_dependencies['order_repository'].create.assert_called_once_with({
            'user_id': user_id,
            'items': items,
            'status': 'pending'
        })

        mock_dependencies['payment_service'].process_payment.assert_called_once_with(
            'order-123',
            99.99
        )

        mock_dependencies['notification_service'].send_order_confirmation.assert_called_once_with(
            user_id,
            'order-123'
        )

        assert mock_dependencies['order_repository'].update.call_count == 1

    def test_create_order_payment_failure(self, order_service, mock_dependencies):
        """Test order creation when payment fails."""
        # 安排
        mock_order = Mock(id='order-123', total=99.99, status='pending')
        mock_dependencies['order_repository'].create.return_value = mock_order
        mock_dependencies['payment_service'].process_payment.side_effect = PaymentError('Card declined')

        # 行动 & 断言
        with pytest.raises(PaymentError):
            order_service.create_order('user-123', [])

        # 验证订单状态更新为失败
        assert mock_order.status == 'failed'
        mock_dependencies['order_repository'].update.assert_called()

        # 通知不应发送
        mock_dependencies['notification_service'].send_order_confirmation.assert_not_called()

    @patch('services.order_service.datetime')
    def test_order_timestamp(self, mock_datetime, order_service, mock_dependencies):
        """Test order creation with mocked time."""
        # 安排
        fixed_time = datetime(2024, 1, 1, 12, 0, 0)
        mock_datetime.now.return_value = fixed_time

        mock_order = Mock(id='order-123', created_at=fixed_time)
        mock_dependencies['order_repository'].create.return_value = mock_order

        # 行动
        result = order_service.create_order('user-123', [])

        # 断言
        assert result.created_at == fixed_time

3. Mockito for Java

// service/UserService.java
public class UserService {
    private final UserRepository userRepository;
    private final EmailService emailService;
    private final AuditLogger auditLogger;

    public UserService(
        UserRepository userRepository,
        EmailService emailService,
        AuditLogger auditLogger
    ) {
        this.userRepository = userRepository;
        this.emailService = emailService;
        this.auditLogger = auditLogger;
    }

    public User createUser(UserDto userDto) {
        User user = userRepository.save(mapToUser(userDto));
        emailService.sendWelcomeEmail(user.getEmail());
        auditLogger.log("User created: " + user.getId());
        return user;
    }

    public Optional<User> getUserWithOrders(Long userId) {
        return userRepository.findByIdWithOrders(userId);
    }
}

// test/UserServiceTest.java
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;

@ExtendWith(MockitoExtension.class)
class UserServiceTest {

    @Mock
    private UserRepository userRepository;

    @Mock
    private EmailService emailService;

    @Mock
    private AuditLogger auditLogger;

    @InjectMocks
    private UserService userService;

    @Test
    void createUser_shouldSaveAndSendEmail() {
        // 安排
        UserDto userDto = new UserDto("test@example.com", "Test User");
        User savedUser = new User(1L, "test@example.com", "Test User");

        when(userRepository.save(any(User.class))).thenReturn(savedUser);
        doNothing().when(emailService).sendWelcomeEmail(anyString());

        // 行动
        User result = userService.createUser(userDto);

        // 断言
        assertNotNull(result);
        assertEquals(1L, result.getId());

        verify(userRepository, times(1)).save(any(User.class));
        verify(emailService, times(1)).sendWelcomeEmail("test@example.com");
        verify(auditLogger, times(1)).log(contains("User created"));
    }

    @Test
    void createUser_shouldThrowExceptionWhenEmailFails() {
        // 安排
        UserDto userDto = new UserDto("test@example.com", "Test User");
        User savedUser = new User(1L, "test@example.com", "Test User");

        when(userRepository.save(any(User.class))).thenReturn(savedUser);
        doThrow(new EmailException("SMTP error"))
            .when(emailService)
            .sendWelcomeEmail(anyString());

        // 行动 & 断言
        assertThrows(EmailException.class, () -> {
            userService.createUser(userDto);
        });

        verify(userRepository).save(any(User.class));
        verify(emailService).sendWelcomeEmail("test@example.com");
    }

    @Test
    void getUserWithOrders_shouldReturnUserWhenExists() {
        // 安排
        User user = new User(1L, "test@example.com", "Test User");
        when(userRepository.findByIdWithOrders(1L))
            .thenReturn(Optional.of(user));

        // 行动
        Optional<User> result = userService.getUserWithOrders(1L);

        // 断言
        assertTrue(result.isPresent());
        assertEquals(user, result.get());

        verify(userRepository).findByIdWithOrders(1L);
    }

    @Test
    void getUserWithOrders_shouldReturnEmptyWhenNotExists() {
        // 安排
        when(userRepository.findByIdWithOrders(999L))
            .thenReturn(Optional.empty());

        // 行动
        Optional<User> result = userService.getUserWithOrders(999L);

        // 断言
        assertFalse(result.isPresent());
    }

    @Captor
    private ArgumentCaptor<User> userCaptor;

    @Test
    void createUser_shouldSaveUserWithCorrectData() {
        // 安排
        UserDto userDto = new UserDto("test@example.com", "Test User");
        when(userRepository.save(any(User.class)))
            .thenReturn(new User(1L, "test@example.com", "Test User"));

        // 行动
        userService.createUser(userDto);

        // 断言 - 捕获并验证保存的用户
        verify(userRepository).save(userCaptor.capture());
        User capturedUser = userCaptor.getValue();

        assertEquals("test@example.com", capturedUser.getEmail());
        assertEquals("Test User", capturedUser.getName());
    }
}

4. 高级模拟模式

// 模拟计时器
describe('Scheduled Tasks', () => {
  beforeEach(() => {
    jest.useFakeTimers();
  });

  afterEach(() => {
    jest.useRealTimers();
  });

  it('should execute task after delay', () => {
    const callback = jest.fn();
    const scheduler = new TaskScheduler();

    scheduler.scheduleTask(callback, 5000);

    expect(callback).not.toHaveBeenCalled();

    jest.advanceTimersByTime(5000);

    expect(callback).toHaveBeenCalledTimes(1);
  });
});

// 部分模拟
describe('UserService with partial mocking', () => {
  it('should use real method for validation, mock for DB', async () => {
    const userService = new UserService();

    // 间谍真实对象
    const saveSpy = jest
      .spyOn(userService.repository, 'save')
      .mockResolvedValue({ id: '123' });

    // 真实验证方法被使用
    await expect(
      userService.createUser({ email: 'invalid' })
    ).rejects.toThrow('Invalid email');

    expect(saveSpy).not.toHaveBeenCalled();

    // 有效数据使用模拟保存
    await userService.createUser({ email: 'valid@example.com' });
    expect(saveSpy).toHaveBeenCalled();
  });
});

最佳实践

✅ DO

  • 模拟外部依赖项(数据库、API、文件系统)
  • 使用依赖注入以便于模拟
  • 验证与模拟的重要交互
  • 测试之间重置模拟
  • 在边界(存储库、服务)处模拟
  • 如有需要,使用间谍进行部分模拟
  • 创建可重用的模拟工厂
  • 测试成功和失败两种场景

❌ DON’T

  • 模拟一切(不要模拟你自己的东西)
  • 过度指定模拟交互
  • 在集成测试中使用模拟
  • 模拟简单的实用程序函数
  • 创建复杂的模拟层次结构
  • 忘记验证模拟调用
  • 在测试之间共享模拟
  • 仅仅为了使测试通过而模拟

工具和库

  • JavaScript/TypeScript:Jest, Sinon.js, ts-mockito
  • Python:unittest.mock, pytest-mock, responses
  • Java:Mockito, EasyMock, PowerMock, JMockit
  • C#:Moq, NSubstitute, FakeItEasy

示例

另见:integration-testing, test-data-generation, test-automation-framework 以获得完整的测试模式。