-
Notifications
You must be signed in to change notification settings - Fork 0
Unit Testing
Unit testing is the practice of testing individual units or components of code in isolation from the rest of the system. A "unit" is typically a function, method, or class that performs a specific task. Unit tests verify that these units behave as expected when provided with different inputs.
- Isolation: Units are tested in isolation from external dependencies
- Speed: Execute very quickly (milliseconds)
- Deterministic: Always produce the same result for the same input
- Automated: Can be run automatically as part of a build process
- Focused: Each test verifies a single behavior or aspect
- Early bug detection: Find issues before they propagate through the system
- Code quality: Encourages cleaner, more modular design
- Documentation: Tests serve as examples of how to use the code
- Confidence: Provides confidence when making changes or refactoring
- Developer productivity: Saves time by catching issues early
Structure tests using the Arrange-Act-Assert pattern:
test('calculateDiscount applies percentage correctly', () => {
// Arrange - Set up the test data
const price = 100;
const discountPercentage = 20;
// Act - Call the function being tested
const discountedPrice = calculateDiscount(price, discountPercentage);
// Assert - Verify the result
expect(discountedPrice).toBe(80);
});Each test should verify a single behavior:
// Good: Separate tests for different behaviors
test('isValidEmail returns true for valid email', () => {
expect(isValidEmail('user@example.com')).toBe(true);
});
test('isValidEmail returns false for invalid email', () => {
expect(isValidEmail('not-an-email')).toBe(false);
});
// Avoid: Testing multiple behaviors in one test
test('isValidEmail validates emails correctly', () => {
expect(isValidEmail('user@example.com')).toBe(true);
expect(isValidEmail('not-an-email')).toBe(false);
expect(isValidEmail('')).toBe(false);
expect(isValidEmail('user@example')).toBe(false);
});Name tests to clearly indicate what they're testing:
// Good: Clear what's being tested
test('should calculate correct tax for standard rate', () => {...});
test('should throw error for negative amounts', () => {...});
// Avoid: Vague test names
test('tax calculation works', () => {...});
test('test errors', () => {...});Use mocks or stubs to isolate units from external dependencies:
// Example with Jest mocks
test('fetchUserProfile calls the API and transforms the result', async () => {
// Mock the API call
apiClient.get = jest.fn().mockResolvedValue({
id: '123',
firstName: 'John',
lastName: 'Doe',
email: 'john@example.com'
});
const profile = await userService.fetchUserProfile('123');
// Verify API was called correctly
expect(apiClient.get).toHaveBeenCalledWith('/users/123');
// Verify the result was transformed correctly
expect(profile).toEqual({
id: '123',
name: 'John Doe',
email: 'john@example.com'
});
});Include tests for boundary conditions and error cases:
// Testing a function that processes age
test('processAge handles minimum age (18)', () => {...});
test('processAge handles maximum age (120)', () => {...});
test('processAge rejects negative age', () => {...});
test('processAge rejects non-numeric input', () => {...});Unit tests should execute quickly:
- Avoid unnecessary computation
- Minimize setup and teardown
- Use mocks instead of real external services
- Prefer in-memory data structures over file/network operations
Tests should not depend on other tests or run order:
// Bad: Tests depend on shared state
let sharedUser;
test('createUser creates a new user', () => {
sharedUser = createUser('Test User');
expect(sharedUser.name).toBe('Test User');
});
test('updateUser updates an existing user', () => {
updateUser(sharedUser, { age: 30 });
expect(sharedUser.age).toBe(30);
});
// Good: Tests are independent
test('createUser creates a new user', () => {
const user = createUser('Test User');
expect(user.name).toBe('Test User');
});
test('updateUser updates an existing user', () => {
const user = createUser('Original Name');
updateUser(user, { age: 30 });
expect(user.age).toBe(30);
});- Jest: Full-featured testing framework with built-in mocking and assertions
- Mocha: Flexible testing framework, often paired with Chai for assertions
- Jasmine: Behavior-driven development framework with built-in assertions
- pytest: Feature-rich, extensible testing framework
- unittest: Standard library testing framework
- nose2: Extended unittest framework
- JUnit: Standard testing framework for Java
- TestNG: Advanced testing framework with more features than JUnit
- Mockito: Popular mocking framework for Java
- NUnit: Widely used testing framework for .NET
- xUnit.net: Modern testing tool for .NET
- MSTest: Microsoft's testing framework
Different types of test replacements:
- Dummy: Objects passed around but never used
- Stub: Provides predefined answers to calls
- Spy: Records calls made to it for later verification
- Mock: Pre-programmed with expectations and can fail tests if expectations are not met
- Fake: Simplified working implementation of a dependency
Running the same test with different inputs:
// Jest example
test.each([
[1, 1, 2],
[2, 2, 4],
[3, 3, 6]
])('add(%i, %i) => %i', (a, b, expected) => {
expect(add(a, b)).toBe(expected);
});# pytest example
@pytest.mark.parametrize("a,b,expected", [
(1, 1, 2),
(2, 2, 4),
(3, 3, 6)
])
def test_add(a, b, expected):
assert add(a, b) == expectedMany frameworks provide hooks for test setup and cleanup:
// Jest example
beforeEach(() => {
// Setup before each test
mockDatabase = createMockDatabase();
});
afterEach(() => {
// Cleanup after each test
mockDatabase.reset();
});# pytest example
@pytest.fixture
def database():
# Setup
db = create_test_database()
yield db
# Teardown
db.close()Code coverage measures how much of your code is exercised by your tests.
- Statement coverage: Percentage of statements executed
- Branch coverage: Percentage of branches (if/else paths) executed
- Function coverage: Percentage of functions called
- Line coverage: Percentage of code lines executed
- JavaScript: Jest (built-in), Istanbul
- Python: Coverage.py, pytest-cov
- Java: JaCoCo, Cobertura
- C#: Coverlet, NCover
- Focus on quality over quantity
- Aim for high coverage on critical components
- Don't use coverage as the only quality metric
- Uncovered code often indicates design issues
Our repository includes several unit testing examples:
- "Test Driven Development: By Example" by Kent Beck
- "Working Effectively with Unit Tests" by Jay Fields
- "The Art of Unit Testing" by Roy Osherove