Dart Unit Testing
Unit testing is a fundamental means of ensuring code quality.
It verifies whether the behavior of each smallest functional unit (usually a function or method) meets expectations.
This chapter introduces the usage of Dart's test package, writing test cases with test(), assertions with expect(), and grouping tests with group().
Installation and configuration of the test package
Dart officially provides the test package for writing and running tests.
First, add the dependency in pubspec.yaml:
# 文件路径:pubspec.yaml dev_dependencies: test: ^1.24.0
Then run the following command to install:
$ dart pub get
Test files are usually placed in the project's test/ directory, with filenames ending in _test.dart.
A typical project structure:
my_project/ ├── lib/ │ └── calculator.dart # 被测试的代码 ├── test/ │ └── calculator_test.dart # 测试文件 └── pubspec.yaml
Writing test() test cases
The test() function is the basic unit for writing test cases.
It takes two parameters: a test description (string) and a test function body.
Example
The code under test (lib/calculator.dart):
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(Divisor cannot be 0);
}
return a / b;
}
bool isEven(int n) => n % 2 == 0;
List<int> filterPositive(List<int> numbers) {
return numbers.where((n) => n > 0).toList();
}
}
Example
Test file (test/calculator_test.dart):
import 'package:test/test.dart';
import 'package:my_project/calculator.dart';
void main() {
// Create a Calculator instance for testing
var calculator = Calculator();
// test() function: the first parameter is the test description, the second is the test function
test('add() adds two positive numbers', () {
var result = calculator.add(3, 5);
// expect() assertion: verifies whether the actual result equals the expected value
expect(result, equals(8));
});
test('add() adds negative numbers', () {
expect(calculator.add(-3, 5), equals(2));
expect(calculator.add(-3, -5), equals(-8));
});
test('subtract() subtraction operation', () {
expect(calculator.subtract(10, 3), equals(7));
});
test('multiply() multiplication operation', () {
expect(calculator.multiply(4, 5), equals(20));
// Multiply by zero
expect(calculator.multiply(100, 0), equals(0));
});
test(divide() normal division, () {
expect(calculator.divide(10, 2), equals(5.0));
expect(calculator.divide(7, 2), equals(3.5));
});
test(isEven() even number check, () {
expect(calculator.isEven(4), isTrue);
expect(calculator.isEven(5), isFalse);
});
}
Run tests:
$ dart test
00:00 +6: All tests passed!
expect assertions
expect() is the most core function in testing; it verifies whether an actual value satisfies a condition.
Basic syntax: expect(actual, matcher).
Common Matchers
| Matcher | Purpose | Example |
|---|---|---|
| equals(expected) | Verify values are equal | expect(result, equals(42)) |
| isTrue / isFalse | Verify boolean values | expect(flag, isTrue) |
| isNull / isNotNull | Verify null | expect(value, isNull) |
| contains(value) | Contains an element (list) or substring (string). | expect(list, contains('a')) |
| isA | Verify types | expect(obj, isA |
| throws | Verify exception thrown | expect(() => f(), throwsException) |
| isNotEmpty | non-null | expect(list, isNotEmpty) |
| hasLength(n) | Verify length | expect(list, hasLength(3)) |
| greaterThan(n) | greater than | expect(score, greaterThan(60)) |
| closeTo(value, delta) | Floating-point numbers approximately equal | expect(3.14, closeTo(3.1, 0.1)) |
Example
Practical applications of various Matchers:
void main() {
test(EXAMPLE Various Assertion Examples, () {
// Basic equality
expect(2 + 2, equals(4));
// Boolean values
expect('hello'.contains('h'), isTrue);
expect(''.isEmpty, isTrue);
// null check
String? name;
expect(name, isNull);
name = 'EXAMPLE';
expect(name, isNotNull);
// Type check
expect('EXAMPLE', isA<String>());
expect(42, isA<int>());
// List/string contains
expect([1, 2, 3], contains(2));
expect('Hello, EXAMPLE!', contains('EXAMPLE'));
// Length
expect([1, 2, 3], hasLength(3));
expect('Dart', hasLength(4));
// Numeric comparison
expect(100, greaterThan(50));
expect(30, lessThan(60));
expect(75, greaterThanOrEqualTo(60));
// Floating-point comparison (avoid precision issues)
expect(0.1 + 0.2, closeTo(0.3, 0.001));
// Collection is not empty
expect([1, 2], isNotEmpty);
// List equality
expect([1, 2, 3], equals([1, 2, 3]));
// Map contains a key
var user = {'name': 'example', 'age': 10};
expect(user, containsPair('name', 'example'));
expect(user.keys, contains('age'));
});
}
00:00 +1: All tests passed!
Testing exceptions
Example
int divide(int a, int b) {
if (b == 0) throw ArgumentError(Divisor cannot be 0);
return a ~/ b;
}
void main() {
test(divide() should throw an exception when dividing by zero, () {
// Verify that any exception is thrown
expect(() => divide(10, 0), throwsException);
// Verify that a specific type of exception is thrown
expect(() => divide(10, 0), throwsArgumentError);
// Verify exception message
expect(
() => divide(10, 0),
throwsA(predicate((e) =>
e is ArgumentError &&
e.message.contains(divisor))),
);
});
test(divide() does not throw an exception in normal cases, () {
expect(() => divide(10, 2), returnsNormally);
expect(divide(10, 2), equals(5));
});
}
00:00 +2: All tests passed!
When testing exceptions, you need to wrap the code that may throw an exception in an anonymous function (() => code), rather than calling it directly. If you write expect(divide(10, 0), throwsException) directly, divide will throw an exception immediately, and expect never gets a chance to execute.
Grouping tests with group()
group() is used to organize related test cases together, making the test structure clearer.
Example
// Function under test
class StringUtils {
static String capitalize(String s) {
if (s.isEmpty) return s;
return s[0].toUpperCase() + s.substring(1);
}
static String reverse(String s) {
return s.split('').reversed.join('');
}
static bool isPalindrome(String s) {
var clean = s.toLowerCase().replaceAll(' ', '');
return clean == reverse(clean);
}
static int countWords(String s) {
if (s.trim().isEmpty) return 0;
return s.trim().split(RegExp(r'\s+')).length;
}
}
void main() {
// group() nested test organization
group('StringUtils', () {
// Subgroup
group('capitalize()', () {
test(Capitalizes the first letter of a normal word, () {
expect(StringUtils.capitalize('hello'), equals('Hello'));
});
test(An already capitalized word remains unchanged, () {
expect(StringUtils.capitalize('Hello'), equals('Hello'));
});
test(An empty string returns an empty string, () {
expect(StringUtils.capitalize(''), equals(''));
});
test(Single character, () {
expect(StringUtils.capitalize('a'), equals('A'));
});
});
group('reverse()', () {
test(Reverses a normal string, () {
expect(StringUtils.reverse('EXAMPLE'), equals('BOONUR'));
});
test(Reversing a palindrome string leaves it unchanged, () {
expect(StringUtils.reverse('aba'), equals('aba'));
});
test(Empty string, () {
expect(StringUtils.reverse(''), equals(''));
});
});
group('isPalindrome()', () {
test(Palindrome string returns true, () {
expect(StringUtils.isPalindrome('racecar'), isTrue);
expect(StringUtils.isPalindrome('A man a plan a canal Panama'), isTrue);
});
test(Non-palindrome string returns false, () {
expect(StringUtils.isPalindrome('hello'), isFalse);
});
test(An empty string is considered a palindrome, () {
expect(StringUtils.isPalindrome(''), isTrue);
});
});
group('countWords()', () {
test(Normal sentence, () {
expect(StringUtils.countWords('Hello World Dart'), equals(3));
});
test(Extra spaces, () {
expect(StringUtils.countWords(' Hello World '), equals(2));
});
test(Empty string, () {
expect(StringUtils.countWords(''), equals(0));
expect(StringUtils.countWords(' '), equals(0));
});
});
});
}
00:00 +12: All tests passed!
group() can be nested to create a hierarchical test structure.
This makes test reports easier to read and also helps quickly locate failing tests.
A good grouping strategy is to organize by the "module/class/method under test". This way, when a test fails, you can immediately know which functionality has a problem.
setUp and tearDown
setUp runs before each test, tearDown runs after each test.
They are used to prepare the test environment and clean up resources.
Example
// Simulate a class that needs initialization and cleanup
class Database {
bool isConnected = false;
void connect() {
isConnected = true;
print(Database connected);
}
void disconnect() {
isConnected = false;
print(Database disconnected);
}
String query(String sql) {
if (!isConnected) throw Exception(Database not connected);
return Query result: $sql;
}
}
void main() {
group(Database test, () {
late Database db; // late lazy initialization
// Execute before each test
setUp(() {
db = Database();
db.connect();
print([setUp] Prepare test environment);
});
// Execute after each test
tearDown(() {
db.disconnect();
print([tearDown] Clean up test environment);
});
test(query() normal query, () {
var result = db.query('SELECT * FROM users');
expect(result, contains('EXAMPLE') ? result.contains('users') : result.contains('users'));
// Simplify assertions
expect(result, isNotEmpty);
});
test(query() can execute multiple queries after connecting, () {
var r1 = db.query('SELECT 1');
var r2 = db.query('SELECT 2');
expect(r1, isNotEmpty);
expect(r2, isNotEmpty);
});
test(Query throws an exception when not connected, () {
db.disconnect(); // Manually disconnect
expect(() => db.query('SELECT 1'), throwsException);
});
});
}
数据库已连接 [setUp] 准备测试环境 数据库已断开 [tearDown] 清理测试环境 数据库已连接 [setUp] 准备测试环境 数据库已断开 [tearDown] 清理测试环境 数据库已连接 [setUp] 准备测试环境 数据库已断开 [tearDown] 清理测试环境 00:00 +3: All tests passed!
Note the execution order of setUp and tearDown in the output—they execute once before and after each test.
setUp and tearDown ensure that each test starts from a clean state and is not affected by other tests. This is a key guarantee of test independence.
Asynchronous test
The Dart test package natively supports asynchronous testing.
A test function can return a Future, and the framework will automatically wait for the Future to complete.
Example
// Asynchronous function: simulate network request
Future<String> fetchUserData(int userId) async {
await Future.delayed(Duration(milliseconds: 100));
if (userId <= 0) {
throw Exception(Invalid user ID);
}
return Data of user $userId;
}
Future<List<int>> fetchNumbers() async {
await Future.delayed(Duration(milliseconds: 50));
return [1, 2, 3, 4, 5];
}
void main() {
group(Asynchronous test, () {
test(fetchUserData() fetches data normally, () async {
var data = await fetchUserData(1);
expect(data, equals(Data of user 1));
});
test(fetchUserData() throws an exception for invalid ID, () async {
expect(
() => fetchUserData(-1),
throwsA(isA<Exception>()),
);
});
test('fetchNumbers() returns the correct list', () async {
var numbers = await fetchNumbers();
expect(numbers, hasLength(5));
expect(numbers, contains(3));
expect(numbers.first, equals(1));
});
// Testing multiple asynchronous operations
test('Sequential asynchronous operations', () async {
var data1 = await fetchUserData(1);
var data2 = await fetchUserData(2);
expect(data1, isNot(equals(data2)));
expect(data1, contains('1'));
expect(data2, contains('2'));
});
});
}
00:00 +4: All tests passed!
Common commands for running tests
| Command | Features |
|---|---|
| dart test | Run all tests |
| dart test test/calculator_test.dart | Run a specified test file |
| dart test --name="add" | Only run tests whose names contain "add" |
| dart test --concurrency=4 | Run tests concurrently (speed up) |
| dart test --reporter=expanded | Verbose output mode |
| dart test --coverage=coverage | Generate test coverage data |
Testing best practices
| Practice | Description |
|---|---|
| Each test should verify only one thing | One test() corresponds to one behavior, making failure location faster. |
| Test names describe behavior, not implementation | "add() adds two positive numbers" is better than "add() test". |
| AAA Pattern | Arrange (Set up) → Act (Execute) → Assert (Verify) |
| Write failing tests first | Confirm that the test can catch errors, then write code to make it pass. |
| Boundary condition testing | Null, zero, negative numbers, very large values, very small values. |
| Keep tests independent of each other | The result of one test should not affect another test |
Tests are not a burden but insurance. The more tests you write, the more confident you are when refactoring. When a test fails, you don't need to guess what went wrong—the test will tell you precisely.
Chapter summary
This chapter introduces the complete workflow of Dart unit testing: installing the test package, writing test() test cases, using expect() assertions, organizing with group(), managing the environment with setUp/tearDown, and asynchronous testing.
Unit testing is a required course for professional developers; developing the habit of writing tests will continuously improve your code quality.
Full Tutorial Summary
Congratulations on completing your introductory learning of the Dart programming language!
Let's review the learning path of these 21 chapters:
| Phase | Chapter | Core Content |
|---|---|---|
| 1. Getting Started Basics | Chapters 1-4 | Dart overview, environment setup, first program, basic syntax. |
| 2. Core Types and Control Flow | Chapters 5-8 | Variables and data types, operators, control flow, collections. |
| 3. Functions and Object-Oriented Programming | Chapters 9-13 | Functions, classes and objects, inheritance and polymorphism, interfaces and Mixin, generics. |
| 4. Advanced Features | Chapters 14-17 | Enums and symbols, exception handling, package and library management, typedef. |
| 5. Asynchronous Programming and Testing | Chapters 18-21 | Async programming, Streams, concurrency and Isolate, unit testing. |
Having mastered all this content, you already have the ability to use Dart for daily development.
Next step: you can dive deeper.Flutter FrameworkApply Dart knowledge to UI development on mobile and web.
other extensions