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.
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 Instructions 1. Test Structure (AAA Pattern) // 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)
);
});
}); });
- Test Cases by Language JavaScript/TypeScript (Jest) 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) 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) 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));
}
}
- Mocking & Test Doubles Mock External Dependencies // 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 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')
- Testing Async Code // 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'); });
- Test Coverage
JavaScript (Jest)
npm test -- --coverage
Python (pytest with coverage)
pytest --cov=src --cov-report=html
Java (Maven)
mvn test jacoco:report
Coverage Goals:
Statements: 80%+ covered Branches: 75%+ covered Functions: 85%+ covered Lines: 80%+ covered 6. Testing Edge Cases 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('')) .toBe('<script>alert("xss")</script>'); }); });
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 ❌ 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 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
Common Assertions Jest 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 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 // 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
beforeEach(() => {
mockDatabase = new Database() as jest.Mocked
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 Jest: https://jestjs.io/docs/getting-started pytest: https://docs.pytest.org/ JUnit 5: https://junit.org/junit5/docs/current/user-guide/ Mocha: https://mochajs.org/ RSpec: https://rspec.info/