unit-testing-framework

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Unit Testing Framework

单元测试框架

Overview

概述

Write effective unit tests that are fast, isolated, readable, and maintainable following industry best practices and AAA (Arrange-Act-Assert) pattern.
遵循行业最佳实践和AAA(Arrange-Act-Assert,准备-执行-断言)模式,编写快速、独立、可读且可维护的高效单元测试。

When to Use

适用场景

  • Writing tests for new code
  • Improving test coverage
  • Establishing testing standards
  • Refactoring with test safety
  • Implementing TDD (Test-Driven Development)
  • Creating test utilities and mocks
  • 为新代码编写测试
  • 提升测试覆盖率
  • 建立测试标准
  • 借助测试安全地重构代码
  • 实现测试驱动开发(TDD)
  • 创建测试工具与模拟对象

Instructions

操作指南

1. Test Structure (AAA Pattern)

1. 测试结构(AAA模式)

javascript
// Jest/JavaScript example
describe('UserService', () => {
  describe('createUser', () => {
    it('should create user with valid data', async () => {
      // Arrange - Set up test data and dependencies
      const userData = {
        email: 'john@example.com',
        firstName: 'John',
        lastName: 'Doe'
      };
      const mockDatabase = createMockDatabase();
      const service = new UserService(mockDatabase);

      // Act - Execute the function being tested
      const result = await service.createUser(userData);

      // Assert - Verify the outcome
      expect(result.id).toBeDefined();
      expect(result.email).toBe('john@example.com');
      expect(mockDatabase.save).toHaveBeenCalledWith(
        expect.objectContaining(userData)
      );
    });
  });
});
javascript
// Jest/JavaScript example
describe('UserService', () => {
  describe('createUser', () => {
    it('should create user with valid data', async () => {
      // Arrange - Set up test data and dependencies
      const userData = {
        email: 'john@example.com',
        firstName: 'John',
        lastName: 'Doe'
      };
      const mockDatabase = createMockDatabase();
      const service = new UserService(mockDatabase);

      // Act - Execute the function being tested
      const result = await service.createUser(userData);

      // Assert - Verify the outcome
      expect(result.id).toBeDefined();
      expect(result.email).toBe('john@example.com');
      expect(mockDatabase.save).toHaveBeenCalledWith(
        expect.objectContaining(userData)
      );
    });
  });
});

2. Test Cases by Language

2. 各语言测试示例

JavaScript/TypeScript (Jest)

JavaScript/TypeScript (Jest)

typescript
import { Calculator } from './calculator';

describe('Calculator', () => {
  let calculator: Calculator;

  beforeEach(() => {
    calculator = new Calculator();
  });

  describe('add', () => {
    it('should add two positive numbers', () => {
      expect(calculator.add(2, 3)).toBe(5);
    });

    it('should handle negative numbers', () => {
      expect(calculator.add(-2, 3)).toBe(1);
      expect(calculator.add(-2, -3)).toBe(-5);
    });

    it('should handle zero', () => {
      expect(calculator.add(0, 5)).toBe(5);
      expect(calculator.add(5, 0)).toBe(5);
    });
  });

  describe('divide', () => {
    it('should divide numbers correctly', () => {
      expect(calculator.divide(10, 2)).toBe(5);
    });

    it('should throw error when dividing by zero', () => {
      expect(() => calculator.divide(10, 0)).toThrow('Division by zero');
    });

    it('should handle decimal results', () => {
      expect(calculator.divide(10, 3)).toBeCloseTo(3.333, 2);
    });
  });
});
typescript
import { Calculator } from './calculator';

describe('Calculator', () => {
  let calculator: Calculator;

  beforeEach(() => {
    calculator = new Calculator();
  });

  describe('add', () => {
    it('should add two positive numbers', () => {
      expect(calculator.add(2, 3)).toBe(5);
    });

    it('should handle negative numbers', () => {
      expect(calculator.add(-2, 3)).toBe(1);
      expect(calculator.add(-2, -3)).toBe(-5);
    });

    it('should handle zero', () => {
      expect(calculator.add(0, 5)).toBe(5);
      expect(calculator.add(5, 0)).toBe(5);
    });
  });

  describe('divide', () => {
    it('should divide numbers correctly', () => {
      expect(calculator.divide(10, 2)).toBe(5);
    });

    it('should throw error when dividing by zero', () => {
      expect(() => calculator.divide(10, 0)).toThrow('Division by zero');
    });

    it('should handle decimal results', () => {
      expect(calculator.divide(10, 3)).toBeCloseTo(3.333, 2);
    });
  });
});

Python (pytest)

Python (pytest)

python
import pytest
from user_service import UserService, ValidationError

class TestUserService:
    @pytest.fixture
    def service(self, mock_database):
        """Fixture to create UserService instance"""
        return UserService(mock_database)

    @pytest.fixture
    def valid_user_data(self):
        return {
            'email': 'john@example.com',
            'first_name': 'John',
            'last_name': 'Doe'
        }

    def test_create_user_with_valid_data(self, service, valid_user_data):
        """Should create user with valid input"""
        # Act
        user = service.create_user(valid_user_data)

        # Assert
        assert user.id is not None
        assert user.email == 'john@example.com'
        assert user.first_name == 'John'

    def test_create_user_with_invalid_email(self, service):
        """Should raise ValidationError for invalid email"""
        invalid_data = {'email': 'invalid', 'first_name': 'John'}

        with pytest.raises(ValidationError) as exc_info:
            service.create_user(invalid_data)

        assert 'email' in str(exc_info.value)

    @pytest.mark.parametrize('email,expected', [
        ('user@example.com', True),
        ('invalid', False),
        ('', False),
        (None, False),
    ])
    def test_email_validation(self, service, email, expected):
        """Should validate email formats correctly"""
        assert service.validate_email(email) == expected
python
import pytest
from user_service import UserService, ValidationError

class TestUserService:
    @pytest.fixture
    def service(self, mock_database):
        """Fixture to create UserService instance"""
        return UserService(mock_database)

    @pytest.fixture
    def valid_user_data(self):
        return {
            'email': 'john@example.com',
            'first_name': 'John',
            'last_name': 'Doe'
        }

    def test_create_user_with_valid_data(self, service, valid_user_data):
        """Should create user with valid input"""
        # Act
        user = service.create_user(valid_user_data)

        # Assert
        assert user.id is not None
        assert user.email == 'john@example.com'
        assert user.first_name == 'John'

    def test_create_user_with_invalid_email(self, service):
        """Should raise ValidationError for invalid email"""
        invalid_data = {'email': 'invalid', 'first_name': 'John'}

        with pytest.raises(ValidationError) as exc_info:
            service.create_user(invalid_data)

        assert 'email' in str(exc_info.value)

    @pytest.mark.parametrize('email,expected', [
        ('user@example.com', True),
        ('invalid', False),
        ('', False),
        (None, False),
    ])
    def test_email_validation(self, service, email, expected):
        """Should validate email formats correctly"""
        assert service.validate_email(email) == expected

Java (JUnit 5)

Java (JUnit 5)

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

class UserServiceTest {
    private UserService userService;
    private UserRepository mockRepository;

    @BeforeEach
    void setUp() {
        mockRepository = mock(UserRepository.class);
        userService = new UserService(mockRepository);
    }

    @Test
    @DisplayName("Should create user with valid data")
    void testCreateUserWithValidData() {
        // Arrange
        UserDto userDto = new UserDto("john@example.com", "John", "Doe");
        User savedUser = new User(1L, "john@example.com", "John", "Doe");
        when(mockRepository.save(any(User.class))).thenReturn(savedUser);

        // Act
        User result = userService.createUser(userDto);

        // Assert
        assertNotNull(result.getId());
        assertEquals("john@example.com", result.getEmail());
        verify(mockRepository, times(1)).save(any(User.class));
    }

    @Test
    @DisplayName("Should throw ValidationException for invalid email")
    void testCreateUserWithInvalidEmail() {
        UserDto userDto = new UserDto("invalid", "John", "Doe");

        ValidationException exception = assertThrows(
            ValidationException.class,
            () -> userService.createUser(userDto)
        );

        assertTrue(exception.getMessage().contains("email"));
    }

    @ParameterizedTest
    @ValueSource(strings = {"user@example.com", "test@domain.co.uk"})
    @DisplayName("Should validate correct email formats")
    void testValidEmailFormats(String email) {
        assertTrue(userService.validateEmail(email));
    }

    @ParameterizedTest
    @ValueSource(strings = {"invalid", "", "no-at-sign.com"})
    @DisplayName("Should reject invalid email formats")
    void testInvalidEmailFormats(String email) {
        assertFalse(userService.validateEmail(email));
    }
}
java
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

class UserServiceTest {
    private UserService userService;
    private UserRepository mockRepository;

    @BeforeEach
    void setUp() {
        mockRepository = mock(UserRepository.class);
        userService = new UserService(mockRepository);
    }

    @Test
    @DisplayName("Should create user with valid data")
    void testCreateUserWithValidData() {
        // Arrange
        UserDto userDto = new UserDto("john@example.com", "John", "Doe");
        User savedUser = new User(1L, "john@example.com", "John", "Doe");
        when(mockRepository.save(any(User.class))).thenReturn(savedUser);

        // Act
        User result = userService.createUser(userDto);

        // Assert
        assertNotNull(result.getId());
        assertEquals("john@example.com", result.getEmail());
        verify(mockRepository, times(1)).save(any(User.class));
    }

    @Test
    @DisplayName("Should throw ValidationException for invalid email")
    void testCreateUserWithInvalidEmail() {
        UserDto userDto = new UserDto("invalid", "John", "Doe");

        ValidationException exception = assertThrows(
            ValidationException.class,
            () -> userService.createUser(userDto)
        );

        assertTrue(exception.getMessage().contains("email"));
    }

    @ParameterizedTest
    @ValueSource(strings = {"user@example.com", "test@domain.co.uk"})
    @DisplayName("Should validate correct email formats")
    void testValidEmailFormats(String email) {
        assertTrue(userService.validateEmail(email));
    }

    @ParameterizedTest
    @ValueSource(strings = {"invalid", "", "no-at-sign.com"})
    @DisplayName("Should reject invalid email formats")
    void testInvalidEmailFormats(String email) {
        assertFalse(userService.validateEmail(email));
    }
}

3. Mocking & Test Doubles

3. 模拟与测试替身

Mock External Dependencies

模拟外部依赖

javascript
// Mock database
const mockDatabase = {
  save: jest.fn().mockResolvedValue({ id: '123' }),
  findById: jest.fn().mockResolvedValue({ id: '123', name: 'John' }),
  delete: jest.fn().mockResolvedValue(true)
};

// Mock HTTP client
jest.mock('axios');
axios.get.mockResolvedValue({ data: { users: [] } });

// Spy on methods
const spy = jest.spyOn(userService, 'sendEmail');
expect(spy).toHaveBeenCalledWith('john@example.com', 'Welcome');
javascript
// Mock database
const mockDatabase = {
  save: jest.fn().mockResolvedValue({ id: '123' }),
  findById: jest.fn().mockResolvedValue({ id: '123', name: 'John' }),
  delete: jest.fn().mockResolvedValue(true)
};

// Mock HTTP client
jest.mock('axios');
axios.get.mockResolvedValue({ data: { users: [] } });

// Spy on methods
const spy = jest.spyOn(userService, 'sendEmail');
expect(spy).toHaveBeenCalledWith('john@example.com', 'Welcome');

Python Mocking

Python 模拟示例

python
from unittest.mock import Mock, patch, MagicMock

def test_send_email(mocker):
    """Test email sending with mocked SMTP"""
    # Mock the SMTP client
    mock_smtp = mocker.patch('smtplib.SMTP')
    service = EmailService()

    # Act
    service.send_email('test@example.com', 'Subject', 'Body')

    # Assert
    mock_smtp.return_value.send_message.assert_called_once()

@patch('requests.get')
def test_fetch_user_data(mock_get):
    """Test API call with mocked requests"""
    mock_get.return_value.json.return_value = {'id': 1, 'name': 'John'}

    user = fetch_user_data(1)

    assert user['name'] == 'John'
    mock_get.assert_called_with('https://api.example.com/users/1')
python
from unittest.mock import Mock, patch, MagicMock

def test_send_email(mocker):
    """Test email sending with mocked SMTP"""
    # Mock the SMTP client
    mock_smtp = mocker.patch('smtplib.SMTP')
    service = EmailService()

    # Act
    service.send_email('test@example.com', 'Subject', 'Body')

    # Assert
    mock_smtp.return_value.send_message.assert_called_once()

@patch('requests.get')
def test_fetch_user_data(mock_get):
    """Test API call with mocked requests"""
    mock_get.return_value.json.return_value = {'id': 1, 'name': 'John'}

    user = fetch_user_data(1)

    assert user['name'] == 'John'
    mock_get.assert_called_with('https://api.example.com/users/1')

4. Testing Async Code

4. 异步代码测试

javascript
// Jest async/await
it('should fetch user data', async () => {
  const user = await fetchUser('123');
  expect(user.id).toBe('123');
});

// Testing promises
it('should resolve with user data', () => {
  return fetchUser('123').then(user => {
    expect(user.id).toBe('123');
  });
});

// Testing rejection
it('should reject with error for invalid ID', async () => {
  await expect(fetchUser('invalid')).rejects.toThrow('User not found');
});
javascript
// Jest async/await
it('should fetch user data', async () => {
  const user = await fetchUser('123');
  expect(user.id).toBe('123');
});

// Testing promises
it('should resolve with user data', () => {
  return fetchUser('123').then(user => {
    expect(user.id).toBe('123');
  });
});

// Testing rejection
it('should reject with error for invalid ID', async () => {
  await expect(fetchUser('invalid')).rejects.toThrow('User not found');
});

5. Test Coverage

5. 测试覆盖率

bash
undefined
bash
undefined

JavaScript (Jest)

JavaScript (Jest)

npm test -- --coverage
npm test -- --coverage

Python (pytest with coverage)

Python (pytest with coverage)

pytest --cov=src --cov-report=html
pytest --cov=src --cov-report=html

Java (Maven)

Java (Maven)

mvn test jacoco:report

**Coverage Goals:**
- **Statements**: 80%+ covered
- **Branches**: 75%+ covered
- **Functions**: 85%+ covered
- **Lines**: 80%+ covered
mvn test jacoco:report

**覆盖率目标:**
- **语句覆盖率**:80%以上
- **分支覆盖率**:75%以上
- **函数覆盖率**:85%以上
- **行覆盖率**:80%以上

6. Testing Edge Cases

6. 边界场景测试

javascript
describe('Edge Cases', () => {
  it('should handle null input', () => {
    expect(processData(null)).toBeNull();
  });

  it('should handle undefined input', () => {
    expect(processData(undefined)).toBeUndefined();
  });

  it('should handle empty string', () => {
    expect(processData('')).toBe('');
  });

  it('should handle empty array', () => {
    expect(processData([])).toEqual([]);
  });

  it('should handle large numbers', () => {
    expect(calculate(Number.MAX_SAFE_INTEGER)).toBeDefined();
  });

  it('should handle special characters', () => {
    expect(sanitize('<script>alert("xss")</script>'))
      .toBe('&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;');
  });
});
javascript
describe('Edge Cases', () => {
  it('should handle null input', () => {
    expect(processData(null)).toBeNull();
  });

  it('should handle undefined input', () => {
    expect(processData(undefined)).toBeUndefined();
  });

  it('should handle empty string', () => {
    expect(processData('')).toBe('');
  });

  it('should handle empty array', () => {
    expect(processData([])).toEqual([]);
  });

  it('should handle large numbers', () => {
    expect(calculate(Number.MAX_SAFE_INTEGER)).toBeDefined();
  });

  it('should handle special characters', () => {
    expect(sanitize('<script>alert("xss")</script>'))
      .toBe('&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;');
  });
});

Best Practices

最佳实践

✅ DO

✅ 应该做

  • Write tests before or alongside code (TDD)
  • Test one thing per test
  • Use descriptive test names
  • Follow AAA pattern
  • Test edge cases and error conditions
  • Keep tests isolated and independent
  • Use setup/teardown appropriately
  • Mock external dependencies
  • Aim for high coverage on critical paths
  • Make tests fast (< 10ms each)
  • Use parameterized tests for similar cases
  • Test public interfaces, not implementation
  • 在编写代码之前或同时编写测试(TDD)
  • 每个测试只验证一个点
  • 使用描述性的测试名称
  • 遵循AAA模式
  • 测试边界情况和错误场景
  • 保持测试独立且隔离
  • 合理使用初始化/清理操作
  • 模拟外部依赖
  • 针对关键路径追求高覆盖率
  • 确保测试快速(每个测试<10毫秒)
  • 对相似场景使用参数化测试
  • 测试公共接口,而非实现细节

❌ DON'T

❌ 不应该做

  • Test implementation details
  • Write tests that depend on each other
  • Ignore failing tests
  • Test third-party library code
  • Use real databases/APIs in unit tests
  • Make tests too complex
  • Skip edge cases
  • Forget to clean up resources
  • Test everything (focus on business logic)
  • Write flaky tests
  • 测试实现细节
  • 编写相互依赖的测试
  • 忽略失败的测试
  • 测试第三方库代码
  • 在单元测试中使用真实数据库/API
  • 让测试过于复杂
  • 忽略边界情况
  • 忘记清理资源
  • 测试所有内容(专注于业务逻辑)
  • 编写不稳定的测试

Test Organization

测试组织架构

src/
├── components/
│   ├── UserProfile.tsx
│   └── __tests__/
│       └── UserProfile.test.tsx
├── services/
│   ├── UserService.ts
│   └── __tests__/
│       ├── UserService.test.ts
│       └── fixtures/
│           └── users.json
└── utils/
    ├── validation.ts
    └── __tests__/
        └── validation.test.ts
src/
├── components/
│   ├── UserProfile.tsx
│   └── __tests__/
│       └── UserProfile.test.tsx
├── services/
│   ├── UserService.ts
│   └── __tests__/
│       ├── UserService.test.ts
│       └── fixtures/
│           └── users.json
└── utils/
    ├── validation.ts
    └── __tests__/
        └── validation.test.ts

Common Assertions

常用断言

Jest

Jest

javascript
expect(value).toBe(expected);              // Strict equality
expect(value).toEqual(expected);           // Deep equality
expect(value).toBeTruthy();                // Truthy check
expect(value).toBeDefined();               // Not undefined
expect(value).toBeNull();                  // Null check
expect(value).toContain(item);             // Array/string contains
expect(value).toMatch(/pattern/);          // Regex match
expect(fn).toThrow(Error);                 // Throws error
expect(fn).toHaveBeenCalled();             // Mock called
expect(fn).toHaveBeenCalledWith(arg);      // Mock called with args
javascript
expect(value).toBe(expected);              // Strict equality
expect(value).toEqual(expected);           // Deep equality
expect(value).toBeTruthy();                // Truthy check
expect(value).toBeDefined();               // Not undefined
expect(value).toBeNull();                  // Null check
expect(value).toContain(item);             // Array/string contains
expect(value).toMatch(/pattern/);          // Regex match
expect(fn).toThrow(Error);                 // Throws error
expect(fn).toHaveBeenCalled();             // Mock called
expect(fn).toHaveBeenCalledWith(arg);      // Mock called with args

pytest

pytest

python
assert value == expected
assert value is True
assert value is not None
assert item in collection
assert pattern in string
with pytest.raises(Exception):
    risky_function()
assert mock.called
assert mock.call_count == 2
python
assert value == expected
assert value is True
assert value is not None
assert item in collection
assert pattern in string
with pytest.raises(Exception):
    risky_function()
assert mock.called
assert mock.call_count == 2

Example: Complete Test Suite

示例:完整测试套件

typescript
// user-service.test.ts
import { UserService } from './user-service';
import { Database } from './database';
import { EmailService } from './email-service';

// Mock dependencies
jest.mock('./database');
jest.mock('./email-service');

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

  beforeEach(() => {
    mockDatabase = new Database() as jest.Mocked<Database>;
    mockEmailService = new EmailService() as jest.Mocked<EmailService>;
    userService = new UserService(mockDatabase, mockEmailService);
  });

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

  describe('createUser', () => {
    const validUserData = {
      email: 'john@example.com',
      firstName: 'John',
      lastName: 'Doe'
    };

    it('should create user successfully', async () => {
      // Arrange
      const savedUser = { id: '123', ...validUserData };
      mockDatabase.save.mockResolvedValue(savedUser);

      // Act
      const result = await userService.createUser(validUserData);

      // Assert
      expect(result).toEqual(savedUser);
      expect(mockDatabase.save).toHaveBeenCalledWith(
        expect.objectContaining(validUserData)
      );
      expect(mockEmailService.sendWelcomeEmail).toHaveBeenCalledWith(
        validUserData.email
      );
    });

    it('should throw ValidationError for invalid email', async () => {
      const invalidData = { ...validUserData, email: 'invalid' };

      await expect(userService.createUser(invalidData))
        .rejects
        .toThrow('Invalid email format');

      expect(mockDatabase.save).not.toHaveBeenCalled();
    });

    it('should handle database errors', async () => {
      mockDatabase.save.mockRejectedValue(new Error('DB Error'));

      await expect(userService.createUser(validUserData))
        .rejects
        .toThrow('Failed to create user');
    });

    it('should continue even if welcome email fails', async () => {
      const savedUser = { id: '123', ...validUserData };
      mockDatabase.save.mockResolvedValue(savedUser);
      mockEmailService.sendWelcomeEmail.mockRejectedValue(
        new Error('Email failed')
      );

      const result = await userService.createUser(validUserData);

      expect(result).toEqual(savedUser);
      // User still created even though email failed
    });
  });

  describe('getUserById', () => {
    it('should return user when found', async () => {
      const user = { id: '123', email: 'john@example.com' };
      mockDatabase.findById.mockResolvedValue(user);

      const result = await userService.getUserById('123');

      expect(result).toEqual(user);
    });

    it('should throw NotFoundError when user not found', async () => {
      mockDatabase.findById.mockResolvedValue(null);

      await expect(userService.getUserById('999'))
        .rejects
        .toThrow('User not found');
    });
  });
});
typescript
// user-service.test.ts
import { UserService } from './user-service';
import { Database } from './database';
import { EmailService } from './email-service';

// Mock dependencies
jest.mock('./database');
jest.mock('./email-service');

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

  beforeEach(() => {
    mockDatabase = new Database() as jest.Mocked<Database>;
    mockEmailService = new EmailService() as jest.Mocked<EmailService>;
    userService = new UserService(mockDatabase, mockEmailService);
  });

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

  describe('createUser', () => {
    const validUserData = {
      email: 'john@example.com',
      firstName: 'John',
      lastName: 'Doe'
    };

    it('should create user successfully', async () => {
      // Arrange
      const savedUser = { id: '123', ...validUserData };
      mockDatabase.save.mockResolvedValue(savedUser);

      // Act
      const result = await userService.createUser(validUserData);

      // Assert
      expect(result).toEqual(savedUser);
      expect(mockDatabase.save).toHaveBeenCalledWith(
        expect.objectContaining(validUserData)
      );
      expect(mockEmailService.sendWelcomeEmail).toHaveBeenCalledWith(
        validUserData.email
      );
    });

    it('should throw ValidationError for invalid email', async () => {
      const invalidData = { ...validUserData, email: 'invalid' };

      await expect(userService.createUser(invalidData))
        .rejects
        .toThrow('Invalid email format');

      expect(mockDatabase.save).not.toHaveBeenCalled();
    });

    it('should handle database errors', async () => {
      mockDatabase.save.mockRejectedValue(new Error('DB Error'));

      await expect(userService.createUser(validUserData))
        .rejects
        .toThrow('Failed to create user');
    });

    it('should continue even if welcome email fails', async () => {
      const savedUser = { id: '123', ...validUserData };
      mockDatabase.save.mockResolvedValue(savedUser);
      mockEmailService.sendWelcomeEmail.mockRejectedValue(
        new Error('Email failed')
      );

      const result = await userService.createUser(validUserData);

      expect(result).toEqual(savedUser);
      // User still created even though email failed
    });
  });

  describe('getUserById', () => {
    it('should return user when found', async () => {
      const user = { id: '123', email: 'john@example.com' };
      mockDatabase.findById.mockResolvedValue(user);

      const result = await userService.getUserById('123');

      expect(result).toEqual(user);
    });

    it('should throw NotFoundError when user not found', async () => {
      mockDatabase.findById.mockResolvedValue(null);

      await expect(userService.getUserById('999'))
        .rejects
        .toThrow('User not found');
    });
  });
});

Resources

参考资源