๐งช Lesson 10: Testing Your Code
Why Testing Matters โ The Safety Net
๐ง Tests verify your code works correctly โ and keeps working when you make changes
Without tests, every code change is a gamble. With tests, you have a safety net that catches regressions instantly. Dart provides the test package for writing and running tests.
โ Without Tests
- Manually verify every change
- Fear refactoring โ "what if I break something?"
- Bugs discovered by users
- No documentation of expected behavior
- Slow, unreliable verification
โ With Tests
- Run all tests in seconds with
dart test - Refactor confidently โ tests tell you if something broke
- Bugs caught before they reach users
- Tests serve as living documentation
- Fast, consistent, automated verification
The Anatomy of a Dart Test
๐ง Every test has three parts: import the library, define the test, assert expectations
The test package provides test() to define individual tests and expect() to make assertions about what should happen.
Basic Test Structure โ Line by Line
// 1. Import the test package
import 'package:test/test.dart';
// 2. The code you want to test
int add(int a, int b) => a + b;
// 3. The main() function โ where tests run from
void main() {
// 4. Define a test with a descriptive name
test('adds two positive numbers', () {
// 5. The actual value and expected value
final result = add(2, 3);
// 6. Assert that result matches expectation
expect(result, equals(5));
});
}
๐ Breakdown
package:test/test.dartmain() is the test entry pointtest() defines one test caseexpect(actual, matcher) assertsRunning Tests
Run dart test in your terminal. Each passing test shows a green +.
The Official Tutorial Example โ Calculator Tests
๐ This complete example comes directly from the official Dart testing tutorial
The official tutorial demonstrates testing with a simple calculator. Here's the full example with explanations of what each part does.
๐ง The official tutorial shows a complete test file for a calculator with multiple test cases
Below is the example code from the tutorial, broken down into the calculator functions and their tests.
The Calculator Code (What We're Testing)
// calculator.dart โ The code under test
class Calculator {
int add(int a, int b) => a + b;
int subtract(int a, int b) => a - b;
int multiply(int a, int b) => a * b;
double divide(int a, int b) {
if (b == 0) throw ArgumentError('Cannot divide by zero');
return a / b;
}
}
The Complete Test File (from the official tutorial)
import 'package:test/test.dart';
import 'package:myapp/calculator.dart';
void main() {
final calculator = Calculator();
// Test 1: Addition
test('calculator adds two numbers', () {
expect(calculator.add(2, 3), equals(5));
expect(calculator.add(-5, 10), equals(5));
});
// Test 2: Subtraction
test('calculator subtracts two numbers', () {
expect(calculator.subtract(10, 4), equals(6));
});
// Test 3: Multiplication
test('calculator multiplies two numbers', () {
expect(calculator.multiply(3, 4), equals(12));
});
// Test 4: Division (including the error case!)
test('calculator divides two numbers', () {
expect(calculator.divide(10, 2), equals(5.0));
});
// Test 5: Division by zero throws an error
test('calculator throws when dividing by zero', () {
expect(
() => calculator.divide(10, 0),
throwsA(isA<ArgumentError>()),
);
});
}
๐ What Each Part Does
() => ... wraps function in a closurethrowsA(isA<ArgumentError>()) โ check error type() => calculator.divide(10, 0) in a closure. This is required because expect() needs to catch the exception when the function runs, not when it's passed as an argument.
Expected Test Output
Matchers โ The expect() Power Tools
๐ง Matchers let you describe what you expect. Dart compares the actual value against the matcher.
The expect() function takes two arguments: the actual value and a matcher. Matchers can check equality, type, ranges, collections, strings, and more.
โ๏ธ Equality
expect(42, equals(42));
expect('hi', isNot(equals('bye')));
expect(obj, same(expectedObj));
equals() โ value equalityisNot() โ negate any matchersame() โ identical reference
๐ข Numeric
expect(10, greaterThan(5));
expect(3, lessThan(10));
expect(0.5, closeTo(0.5, 0.01));
expect(5, inInclusiveRange(1, 10));
closeTo() โ approximate equality for doubles
๐ Strings
expect('hello', contains('ell'));
expect('dart', startsWith('da'));
expect('end', endsWith('nd'));
expect('Dart', matches(RegExp(r'D.rt')));
๐ Collections
expect([1,2,3], contains(2));
expect([], isEmpty);
expect([1,2], isNotEmpty);
expect([1,2], hasLength(2));
expect([1,2], containsAll([2,1]));
๐ท๏ธ Types & Null
expect('hi', isA<String>());
expect(null, isNull);
expect(42, isNotNull);
expect(true, isTrue);
expect(false, isFalse);
๐ฅ Exceptions
expect(
() => throwFunc(),
throwsException
);
expect(
() => badFunc(),
throwsA(isA<FormatException>())
);
expect(
() => safeFunc(),
returnsNormally
);
throwsA() โ checks specific exception type
๐งช Test Matchers Live
Organizing Tests with Groups
๐ง Use group() to organize related tests into logical categories
Groups make test output easier to read and let you run subsets of tests. You can nest groups inside groups for hierarchical organization. Each group can have its own setUp() and tearDown() functions.
Group Structure Example
import 'package:test/test.dart';
void main() {
// Top-level group: Calculator
group('Calculator', () {
late Calculator calc;
// setUp runs before EACH test in this group
setUp(() {
calc = Calculator();
});
// Sub-group: addition tests
group('addition', () {
test('positive numbers', () {
expect(calc.add(2, 3), equals(5));
});
test('negative numbers', () {
expect(calc.add(-2, -3), equals(-5));
});
});
// Sub-group: division tests
group('division', () {
test('normal division', () {
expect(calc.divide(10, 2), equals(5.0));
});
test('division by zero', () {
expect(
() => calc.divide(10, 0),
throwsA(isA<ArgumentError>()),
);
});
});
});
}
setUp() runs before every test in its group โ perfect for creating fresh objects. tearDown() runs after every test โ useful for cleanup like closing files or database connections.
Expected Grouped Output
Testing Asynchronous Code
๐ง Async tests use the same test() function โ just make the callback async
There are two main patterns: use async/await inside the test callback, or use the completion() matcher for Futures.
Pattern 1: async/await (Recommended)
// Make the test callback async, then use await
test('fetchUser returns a User object', () async {
final user = await fetchUser(1);
expect(user.id, equals(1));
expect(user.name, isNotNull);
});
// Testing async errors
test('fetchUser throws on invalid ID', () async {
expect(
() async => await fetchUser(-1),
throwsA(isA<ArgumentError>()),
);
});
Pattern 2: completion() Matcher
// completion() matches the value a Future will produce
test('fetchUser completes with User', () {
expect(
fetchUser(1),
completion(isA<User>()),
);
});
๐งช Simulate Async Test
Testing Exceptions โ Ensuring Errors Are Handled
๐ง When your code should throw an error, your tests should verify it does
Use throwsA() to verify a specific exception type is thrown, or throwsException for any exception. Always wrap the throwing code in a closure () => ....
Exception Testing Patterns
// 1. Test that ANY exception is thrown
test('throws an exception', () {
expect(
() => riskyOperation(),
throwsException,
);
});
// 2. Test for a SPECIFIC exception type
test('throws FormatException for bad JSON', () {
expect(
() => jsonDecode('not valid json'),
throwsA(isA<FormatException>()),
);
});
// 3. Test that exception has specific message
test('throws with specific message', () {
expect(
() => divide(10, 0),
throwsA(
isA<ArgumentError>().having(
(e) => e.message,
'message',
contains('zero'),
),
),
);
});
// 4. Test that code does NOT throw
test('returns normally', () {
expect(
() => safeOperation(),
returnsNormally,
);
});
expect(divide(10,0), throwsException) without () =>, the exception is thrown BEFORE expect() can catch it. The closure () => divide(10,0) delays execution so the test framework can catch the error.
๐ฏ Testing Quiz
Score: 0/5 | Hints: 3
Loading question...
Practice Projects
Apply what you've learned by building these hands-on projects.