Overview
Numix maintains a comprehensive test suite to ensure mathematical accuracy and application reliability. The project requires 100% test coverage for calculation logic, as precision is critical for financial calculations.
All mathematical operations and business logic must be thoroughly tested before deployment. Financial calculation errors can have serious real-world consequences.
Test Structure
Numix uses Flutter’s built-in testing framework with tests organized to mirror the feature-first architecture:
test/
└── features/
├── discount_calculator/
│ └── providers/
│ └── discount_provider_test.dart
└── sales_price_calculator/
└── providers/
└── sales_price_provider_test.dart
Running Tests
Run All Tests
Execute the entire test suite:
Run Specific Test File
Test a single feature:
flutter test test/features/discount_calculator/providers/discount_provider_test.dart
Run with Coverage
Generate a coverage report:
The project aims for 100% coverage of mathematical operations. Review coverage reports regularly to identify untested code paths.
Test Anatomy
Basic Test Structure
Numix tests follow the Arrange-Act-Assert pattern:
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:numix/features/discount_calculator/providers/discount_provider.dart';
void main() {
group('DiscountCalculatorProvider', () {
late DiscountCalculatorProvider provider;
late SharedPreferences prefs;
setUp(() async {
// Arrange: Set up test environment
SharedPreferences.setMockInitialValues({});
prefs = await SharedPreferences.getInstance();
provider = DiscountCalculatorProvider(prefs);
});
test('Calculates simple percentage discount correctly', () {
// Act: Perform the calculation
provider.calculateDiscount(
originalPriceStr: '100',
primaryDiscountStr: '20'
);
// Assert: Verify the results
expect(provider.errorMessage, isNull);
expect(provider.savedAmount, 20.0);
expect(provider.subtotal, 80.0);
expect(provider.finalPrice, 80.0);
});
});
}
Test Groups
Organize related tests using group():
group('DiscountCalculatorProvider', () {
// Multiple related tests
test('Initial values are null', () { ... });
test('Calculates simple discount', () { ... });
test('Handles invalid input', () { ... });
});
Testing Mathematical Operations
Testing Calculation Accuracy
Verify that calculations produce correct results:
test('Calculates sequential percentage discount correctly', () {
provider.calculateDiscount(
originalPriceStr: '100',
primaryDiscountStr: '20',
additionalDiscountStr: '10',
);
expect(provider.errorMessage, isNull);
expect(provider.savedAmount, 28.0); // 20% + 10% of remaining
expect(provider.subtotal, 72.0);
expect(provider.finalPrice, 72.0);
});
Sequential discounts: First discount is 20% of 100 = 20, leaving 80. Second discount is 10% of 80 = 8, leaving 72. Total saved is 28.
Testing Tax Calculations
Ensure taxes are applied correctly to the discounted price:
test('Calculates correctly with tax', () {
provider.calculateDiscount(
originalPriceStr: '100',
primaryDiscountStr: '20',
taxStr: '15',
);
expect(provider.errorMessage, isNull);
expect(provider.subtotal, 80.0); // After discount
expect(provider.taxAmount, 12.0); // 15% of 80
expect(provider.finalPrice, 92.0); // 80 + 12
});
Testing Margin vs. Markup
Verify both profit calculation methods:
test('Calculates Markup correctly with tax', () {
provider.calculatePrice(
costStr: '100',
profitPercentStr: '20',
taxStr: '10'
);
expect(provider.profitAmount, 20.0); // 20% of cost
expect(provider.baseSalePrice, 120.0); // Cost + profit
expect(provider.taxAmount, 12.0); // 10% of sale price
expect(provider.finalPrice, 132.0); // Sale price + tax
});
Testing Validation Rules
Testing Error Cases
Verify that validation rules are properly enforced:
test('Percentage > 100 shows error', () {
provider.calculateDiscount(
originalPriceStr: '100',
primaryDiscountStr: '110'
);
expect(provider.errorMessage, 'Los porcentajes de descuento no pueden exceder 100%');
expect(provider.finalPrice, isNull);
});
test('Negative values show error', () {
provider.calculateDiscount(
originalPriceStr: '-100',
primaryDiscountStr: '20'
);
expect(provider.errorMessage, 'Los valores no pueden ser negativos');
expect(provider.finalPrice, isNull);
});
Testing Boundary Conditions
Test edge cases and boundaries:
test('Fixed discount exceeds original price shows error', () {
provider.setDiscountType(DiscountType.fixedAmount);
provider.calculateDiscount(
originalPriceStr: '100',
primaryDiscountStr: '120'
);
expect(provider.errorMessage, 'El descuento no puede ser mayor al precio original');
expect(provider.finalPrice, isNull);
});
test('Gross Margin fails if percentage is >= 100', () {
provider.setMarginType(MarginType.margin);
provider.calculatePrice(
costStr: '100',
profitPercentStr: '100'
);
expect(provider.errorMessage, 'El margen sobre venta debe ser menor a 100%');
expect(provider.finalPrice, isNull);
});
Always test boundary conditions: zero values, maximum valid values, and values just beyond the valid range.
Invalid Number Strings
Verify that non-numeric input is handled gracefully:
test('Sets error for invalid strings', () {
provider.calculatePrice(
costStr: 'abc',
profitPercentStr: '20'
);
expect(provider.errorMessage, 'Valores numéricos inválidos');
expect(provider.finalPrice, isNull);
});
Testing State Management
Testing Initial State
Verify correct initialization:
test('Initial values are null', () {
expect(provider.finalPrice, isNull);
expect(provider.savedAmount, isNull);
expect(provider.subtotal, isNull);
expect(provider.taxAmount, isNull);
expect(provider.errorMessage, isNull);
expect(provider.discountType, DiscountType.percentage);
});
Testing State Clearing
Ensure the clear method resets all state:
test('Clears values correctly', () {
provider.calculateDiscount(
originalPriceStr: '100',
primaryDiscountStr: '20'
);
provider.clear();
expect(provider.finalPrice, isNull);
expect(provider.savedAmount, isNull);
expect(provider.errorMessage, isNull);
expect(provider.originalPriceInput, '');
});
Testing Persistence
Testing SharedPreferences Integration
Verify that state persists across provider instances:
test('Persists and loads data from SharedPreferences', () async {
// Save data with first provider instance
provider.calculateDiscount(
originalPriceStr: '200',
primaryDiscountStr: '50'
);
// Create new provider instance (simulates app restart)
final newProvider = DiscountCalculatorProvider(prefs);
// Verify data was restored
expect(newProvider.originalPriceInput, '200');
expect(newProvider.primaryDiscountInput, '50');
expect(newProvider.finalPrice, 100.0);
});
Use SharedPreferences.setMockInitialValues({}) in setUp() to ensure a clean state for each test.
Writing New Tests
When adding new features or calculators, follow this testing checklist:
Essential Test Cases
-
Happy Path Tests
- Basic calculation with valid input
- Calculation with optional parameters
- Calculation with all parameters
-
Validation Tests
- Invalid input strings
- Negative values
- Zero values
- Boundary conditions
- Business rule violations
-
State Management Tests
- Initial state
- State after calculation
- State after clearing
- State persistence and restoration
-
Edge Cases
- Empty strings
- Very large numbers
- Very small numbers
- Multiple rapid calculations
Example Test Template
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:numix/features/your_feature/providers/your_provider.dart';
void main() {
group('YourProvider', () {
late YourProvider provider;
late SharedPreferences prefs;
setUp(() async {
SharedPreferences.setMockInitialValues({});
prefs = await SharedPreferences.getInstance();
provider = YourProvider(prefs);
});
test('Initial state is correct', () {
// Test initial values
});
test('Calculates correctly with valid input', () {
// Test happy path
});
test('Handles invalid input gracefully', () {
// Test error handling
});
test('Validates business rules', () {
// Test domain-specific rules
});
test('Persists and restores state', () {
// Test SharedPreferences integration
});
});
}
Continuous Integration
Numix tests should run automatically in CI/CD pipelines:
# Example GitHub Actions configuration
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: subosito/flutter-action@v2
- run: flutter pub get
- run: flutter test --coverage
- run: flutter test --coverage --coverage-path=coverage/lcov.info
Never merge code that reduces test coverage below 100% for mathematical operations. All calculation logic must be fully tested.
Debugging Tests
test('Debug calculation', () {
provider.calculateDiscount(
originalPriceStr: '100',
primaryDiscountStr: '20',
);
print('Saved: ${provider.savedAmount}');
print('Subtotal: ${provider.subtotal}');
print('Final: ${provider.finalPrice}');
expect(provider.finalPrice, 80.0);
});
Run Single Test
flutter test --plain-name="Calculates simple percentage discount correctly"
Best Practices
- Test behavior, not implementation - Focus on what the code does, not how
- Use descriptive test names - Test names should clearly state what they verify
- Keep tests independent - Each test should run successfully in isolation
- Test one thing at a time - Each test should verify a single behavior
- Use meaningful assertions - Include multiple
expect() calls to thoroughly verify results
- Mock external dependencies - Use mock SharedPreferences for isolated tests
- Test error messages - Verify that user-facing error messages are correct
- Cover edge cases - Don’t just test the happy path
Run tests frequently during development. Fast feedback helps catch errors early and builds confidence in your code.