On this page23 sections
- Prerequisites
- Connection With Previous Lectures
- Learning Objectives
- 1. The Project
- 2. setUp — Before Every Test
- 3. The Run Order
- 4. tearDown — Cleaning Up
- 5. Why Every Test Needs a Fresh Start
- 6. Nested Groups with Their Own setUp
- 7. Mirror lib/ in test/
- 8. Helpers and Fixtures
- 9. skip: and tags:
- 10. Running Parts of the Suite
- 11. A Real-World Example
- Code Examples
- Common Beginner Mistakes
- Best Practices
- Mini Challenge
- Practice Questions
- Practice Answers
- Quick Revision
- Key Takeaways
- What's Next?
Lecture 122 — Test Setup & Organization
| Category | 20 — Testing |
| Lecture | 122 of 140 |
| Estimated video length | 9 min |
| Target | Dart 3.12.2 |
| Level | Advanced |
Every test so far built its own objects from scratch, line by line.
setUpandtearDownprepare and clean up once per test, and a tidytest/folder keeps a growing suite easy to find your way around.
Contents
Prerequisites · Connection · Objectives · Lesson · Code Examples · Common Mistakes · Best Practices · Mini Challenge · Practice Questions · Answers · Takeaways
Every output and error message in this lecture is real. Each one was produced with
dart teston Dart 3.12.2, so you will recognize it when you meet it yourself.
Prerequisites
Students should already understand, from Lectures 006–121:
latevariables (Lecture 006)lib/src/,exportandpackage:imports (Lectures 102–103)- reading files and temporary folders with
dart:io(Lectures 106–107) test,expect, matchers,group()anddart test -n(Lectures 118–119)
Connection With Previous Lectures
In Lecture 120 every BankAccount test began the same way:
var account = BankAccount('Ali');
account.deposit(500);Twenty tests means forty copied lines. The obvious shortcut — create the account once, at
the top of main() — makes tests pass or fail depending on the order they run in
(section 5). We need shared setup that still gives every test a clean start.
Learning Objectives
By the end of this lecture you will be able to:
- Prepare a fresh object for every test with
setUp - Clean up after every test with
tearDown - Run code once per file or group with
setUpAllandtearDownAll - Explain why shared mutable state makes tests order-dependent
- Nest groups that each add their own
setUp - Organize
test/to mirrorlib/, with helper and fixture files - Mark tests with
tags:andskip: - Run a folder or a file, choose a reporter, and collect coverage
1. The Project
We use a package created with dart create -t package bank_app. The BankAccount class from
Lecture 120 lives in lib/src/account.dart, and lib/bank_app.dart exports it. Its
deposit and withdraw methods return false when they refuse an amount, and balance
reads the private _balance, which starts at 0.
2. setUp — Before Every Test
setUp(callback) registers code that runs before each test in the file. Declare the
variable with late (Lecture 006) and assign it inside setUp:
import 'package:bank_app/bank_app.dart';
import 'package:test/test.dart';
void main() {
late BankAccount account;
setUp(() {
account = BankAccount('Ali');
account.deposit(500);
});
test('deposit adds to the balance', () {
account.deposit(100);
expect(account.balance, 600);
});
test('withdraw takes from the balance', () {
account.withdraw(200);
expect(account.balance, 300);
});
}00:00 +0: loading test/fresh_test.dart
00:00 +0: deposit adds to the balance
00:00 +1: withdraw takes from the balance
00:00 +2: All tests passed!Analogy — a restaurant resets every table before the next guests sit down.
setUpresets the table for each test.
The second test sees 500, not 600: it got a brand-new account.
3. The Run Order
Four functions hook into the run. Printing from each shows exactly when they fire:
import 'package:test/test.dart';
void main() {
setUpAll(() => print('setUpAll'));
tearDownAll(() => print('tearDownAll'));
setUp(() => print(' setUp'));
tearDown(() => print(' tearDown'));
test('first', () => print(' test 1'));
test('second', () => print(' test 2'));
}00:00 +0: loading test/order_test.dart
00:00 +0: (setUpAll)
setUpAll
00:00 +0: first
setUp
test 1
tearDown
00:00 +1: second
setUp
test 2
tearDown
00:00 +2: (tearDownAll)
tearDownAll
00:00 +2: All tests passed!setUp and tearDown wrap every test; setUpAll and tearDownAll run once, before
the first test and after the last.
4. tearDown — Cleaning Up
tearDown runs after each test, even when the test fails. Use it to remove anything a
test leaves behind, such as a temporary folder (Lecture 107):
import 'dart:io';
import 'package:test/test.dart';
void main() {
late Directory folder;
setUp(() {
folder = Directory.systemTemp.createTempSync('bank_');
});
tearDown(() {
folder.deleteSync(recursive: true);
});
test('saves a statement file', () {
var file = File('${folder.path}/statement.txt');
file.writeAsStringSync('Ali: 350.0');
expect(file.readAsStringSync(), 'Ali: 350.0');
});
}00:00 +0: loading test/export_test.dart
00:00 +0: saves a statement file
00:00 +1: All tests passed!setUpAll suits slow work whose result is only read — see section 8.
5. Why Every Test Needs a Fresh Start
The shortcut — one account shared by both tests:
void main() {
var account = BankAccount('Ali');
test('deposit adds to the balance', () {
account.deposit(500);
expect(account.balance, 500);
});
test('withdraw takes from the balance', () {
account.withdraw(200);
expect(account.balance, 300);
});
}dart test passes both. Now run only the second test with -n (Lecture 119):
$ dart test test/shared_test.dart -n withdraw
00:00 +0: withdraw takes from the balance
00:00 +0 -1: withdraw takes from the balance [E]
Expected: <300>
Actual: <0.0>The withdraw test secretly needed the deposit test to run first. Shuffling with
--test-randomize-ordering-seed=1 breaks it too. The setUp version
from section 2 passes in any order, alone or together.
6. Nested Groups with Their Own setUp
A group can have its own setUp. The outer one runs first; tear-downs run in reverse:
import 'package:test/test.dart';
void main() {
setUp(() => print('outer setUp'));
tearDown(() => print('outer tearDown'));
group('savings', () {
setUp(() => print(' inner setUp'));
tearDown(() => print(' inner tearDown'));
test('earns interest', () => print(' test'));
});
}00:00 +0: savings earns interest
outer setUp
inner setUp
test
inner tearDown
outer tearDown
00:00 +1: All tests passed!Use the outer setUp for the basic object and a group's setUp for a special situation, such
as "when only 50 is left".
7. Mirror lib/ in test/
Give every library file one test file at the same path, ending in _test.dart:
bank_app/
lib/
bank_app.dart
src/
account.dart
reports/
history.dart
test/
account_test.dart
reports/
history_test.dart
helpers/
accounts.dart
fixtures/
ali_history.txtdart test runs only _test.dart files, so helpers/ and fixtures/ never run as tests.
8. Helpers and Fixtures
A helper is a function many test files share. A fixture is a file of sample data.
Here is test/helpers/accounts.dart:
import 'dart:io';
import 'package:bank_app/bank_app.dart';
BankAccount accountWith(double balance, {String owner = 'Ali'}) {
var account = BankAccount(owner);
account.deposit(balance);
return account;
}
List<String> readFixture(String name) =>
File('test/fixtures/$name').readAsLinesSync();readAsLinesSync is the synchronous twin of readAsLines (Lecture 106); the path starts at
the package folder, where dart test runs. The fixture ali_history.txt holds three lines —
deposit 500, withdraw 200, deposit 50 — and the library's replay function applies
them to an account. The test reads the fixture once with setUpAll:
import 'package:bank_app/bank_app.dart';
import 'package:test/test.dart';
import '../helpers/accounts.dart';
void main() {
late List<String> history;
setUpAll(() {
history = readFixture('ali_history.txt');
});
test('replaying the saved history gives 350', () {
var account = BankAccount('Ali');
replay(account, history);
expect(account.balance, 350);
});
}00:00 +0: loading test/reports/history_test.dart
00:00 +0: (setUpAll)
00:00 +0: replaying the saved history gives 350
00:00 +1: (tearDownAll)
00:00 +1: All tests passed!package: imports reach only lib/, so helpers are imported with a relative path.
Sharing history is safe: no test changes it.
9. skip: and tags:
skip: keeps a test in the file without running it; the reason is printed. tags: labels a
test so you can include or exclude it:
import 'package:test/test.dart';
void main() {
test('loads a year of statements', () async {
await Future.delayed(Duration(seconds: 2));
expect(365, greaterThan(300));
}, tags: 'slow');
test('interest for leap years', () {
expect(366 - 365, 1);
}, skip: 'Waiting for the new interest rules');
}$ dart test test/tags_test.dart -x slow
00:00 +0: interest for leap years
Skip: Waiting for the new interest rules
00:00 +0 ~1: All tests skipped.~1 counts skipped tests. -x slow leaves out tagged tests; -t slow runs only them.
Otherwise the runner warns A tag was used that wasn't specified in dart_test.yaml. — so
declare tags in dart_test.yaml in the package folder:
tags:
slow:To tag a whole file, put @Tags(['slow']) above a library; directive (Lecture 102). Both
skip: and tags: also work on a group.
10. Running Parts of the Suite
Pass a folder or a file to dart test:
$ dart test test/reports
00:00 +0: loading test/reports/history_test.dart
00:00 +0: test/reports/history_test.dart: (setUpAll)
00:00 +0: test/reports/history_test.dart: replaying the saved history gives 350
00:00 +1: test/reports/history_test.dart: (tearDownAll)
00:00 +1: All tests passed!In a terminal the default compact reporter rewrites a single line. --reporter expanded
(-r expanded) prints one line per test, as in every output in this lecture.
--coverage-path records which lines of lib/ your tests ran, as an lcov report:
$ dart test --coverage-path=coverage/lcov.info
00:00 +5: All tests passed!Inside lcov.info, each file lists its lines found (LF) and lines hit (LH):
SF:.../bank_app/lib/src/account.dart
LF:8
LH:8All 8 lines ran — a line that never runs is a line no test checks. --coverage=coverage
saves raw data in a folder instead.
11. A Real-World Example
test/account_test.dart combines a shared setUp, the helper, groups, and a nested group
with its own setUp:
import 'package:bank_app/bank_app.dart';
import 'package:test/test.dart';
import 'helpers/accounts.dart';
void main() {
late BankAccount account;
setUp(() {
account = accountWith(500);
});
group('withdraw', () {
test('takes from the balance', () {
account.withdraw(200);
expect(account.balance, 300);
});
group('when only 50 is left', () {
setUp(() => account.withdraw(450));
test('allows the last 50', () {
expect(account.withdraw(50), isTrue);
expect(account.balance, 0);
});
test('refuses 100', () {
expect(account.withdraw(100), isFalse);
expect(account.balance, 50);
});
});
});
}00:00 +0: loading test/account_test.dart
00:00 +0: withdraw takes from the balance
00:00 +1: withdraw when only 50 is left allows the last 50
00:00 +2: withdraw when only 50 is left refuses 100
00:00 +3: All tests passed!The names read as a specification, and every test runs alone or in any order.
Code Examples
Each example is one file in test/; run it with dart test test/<file>.dart.
Example 1 — A Fresh List per Test
import 'package:test/test.dart';
void main() {
late List<String> cart;
setUp(() {
cart = ['Notebook'];
});
test('adding an item', () {
cart.add('Pen');
expect(cart, hasLength(2));
});
test('a new test starts with one item again', () {
expect(cart, ['Notebook']);
});
}00:00 +0: adding an item
00:00 +1: a new test starts with one item again
00:00 +2: All tests passed!Example 2 — tearDown Clears a Log
import 'package:test/test.dart';
var log = <String>[];
void main() {
tearDown(() {
log.clear();
});
test('records one message', () {
log.add('Ali logged in');
expect(log, hasLength(1));
});
test('the log is empty again', () {
expect(log, isEmpty);
});
}00:00 +0: records one message
00:00 +1: the log is empty again
00:00 +2: All tests passed!Example 3 — setUpAll Builds Data Once
import 'package:test/test.dart';
void main() {
late List<int> marks;
setUpAll(() {
marks = List.generate(1000, (i) => i % 101);
});
test('there are 1000 marks', () {
expect(marks, hasLength(1000));
});
}00:00 +0: (setUpAll)
00:00 +0: there are 1000 marks
00:00 +1: (tearDownAll)
00:00 +1: All tests passed!Example 4 — Layered setUp
import 'package:test/test.dart';
void main() {
late int score;
setUp(() => score = 0);
group('bonus round', () {
setUp(() => score += 50);
test('starts at 50', () {
expect(score, 50);
});
group('double bonus', () {
setUp(() => score *= 2);
test('starts at 100', () {
expect(score, 100);
});
});
});
}00:00 +0: bonus round starts at 50
00:00 +1: bonus round double bonus starts at 100
00:00 +2: All tests passed!Example 5 — Skipping a Group
import 'package:test/test.dart';
void main() {
group('currency rates from the bank', () {
test('loads the latest rate', () {
expect(true, isTrue);
});
}, skip: 'The bank API is not ready yet');
}00:00 +0: currency rates from the bank loads the latest rate
Skip: The bank API is not ready yet
00:00 +0 ~1: All tests skipped.Example 6 — Tagging a Group
import 'package:test/test.dart';
void main() {
group('report', () {
test('a full year of marks', () async {
await Future.delayed(Duration(seconds: 1));
expect(12, greaterThan(10));
});
}, tags: 'slow');
}$ dart test test/report_test.dart -t slow
00:00 +0: report a full year of marks
00:01 +1: All tests passed!Example 7 — Student Marks
import 'package:test/test.dart';
void main() {
late Map<String, int> gradebook;
setUp(() {
gradebook = {'Ali': 80, 'Sara': 92};
});
group('gradebook', () {
test('adds Zayan', () {
gradebook['Zayan'] = 75;
expect(gradebook, hasLength(3));
});
test('removes Ali', () {
gradebook.remove('Ali');
expect(gradebook.keys, ['Sara']);
});
});
}00:00 +0: gradebook adds Zayan
00:00 +1: gradebook removes Ali
00:00 +2: All tests passed!Example 8 — Shopping Total
import 'package:test/test.dart';
class Cart {
final prices = <double>[];
double discount = 0;
double get total {
var sum = prices.fold(0.0, (a, b) => a + b);
return sum - sum * discount / 100;
}
}
void main() {
late Cart cart;
setUp(() {
cart = Cart();
cart.prices.addAll([500, 250, 250]);
});
test('total without discount', () {
expect(cart.total, 1000);
});
group('with a 10% discount', () {
setUp(() => cart.discount = 10);
test('total is 900', () {
expect(cart.total, 900);
});
});
}00:00 +0: total without discount
00:00 +1: with a 10% discount total is 900
00:00 +2: All tests passed!Example 9 — Bank Balance with a Helper
import 'package:test/test.dart';
import 'helpers/accounts.dart';
void main() {
test('Sara can withdraw part of her balance', () {
var account = accountWith(1000, owner: 'Sara');
expect(account.withdraw(400), isTrue);
expect(account.balance, 600);
});
}00:00 +0: Sara can withdraw part of her balance
00:00 +1: All tests passed!Example 10 — Temperature Readings
import 'package:test/test.dart';
double average(List<double> values) =>
values.fold(0.0, (a, b) => a + b) / values.length;
void main() {
late List<double> readings;
setUp(() => readings = [20, 24]);
test('average of two readings', () {
expect(average(readings), 22);
});
test('a cold reading lowers the average', () {
readings.add(10);
expect(average(readings), closeTo(18, 0.01));
});
}00:00 +0: average of two readings
00:00 +1: a cold reading lowers the average
00:00 +2: All tests passed!Common Beginner Mistakes
1. Sharing one mutable object between tests — the tests pass together but fail alone or
shuffled (section 5). Create the object in setUp.
2. Forgetting late
// BankAccount account; // ✗
// setUp(() { account = BankAccount('Ali'); });Error: Non-nullable variable 'account' must be assigned before it can be used.Dart cannot see that setUp assigns it before each test. late promises it will.
3. Calling setUp inside a test
Bad state: Can't call setUp() once tests have begun running.setUp belongs in main() or a group, never inside test.
4. Naming a helper file _test.dart
Failed to load "test/helpers/accounts_test.dart": Missing definition of `main` method.The runner treats every _test.dart file as a test file. Name helpers without the suffix.
Best Practices
- Create mutable objects in
setUp, so every test starts fresh. - Put
setUpandtearDownat the top ofmain()or thegroup. - Mirror
lib/intest/: one_test.dartfile per library file. - Keep shared code in
test/helpers/and sample data intest/fixtures/. - Always give
skip:a reason, and declare tags indart_test.yaml.
Mini Challenge
Zayan's PiggyBank(coins) has spend(amount), which returns false when coins run short.
Create a bank with 10 coins in setUp. In a group "after buying a sweet for 3", add a setUp
that spends 3, then test that 7 coins are left and that spending 8 more is refused.
Try it yourself before opening the solution.
Show solution
import 'package:test/test.dart';
class PiggyBank {
int coins;
PiggyBank(this.coins);
bool spend(int amount) {
if (amount > coins) return false;
coins -= amount;
return true;
}
}
void main() {
late PiggyBank piggy;
setUp(() => piggy = PiggyBank(10));
group('after buying a sweet for 3', () {
setUp(() => piggy.spend(3));
test('has 7 coins left', () {
expect(piggy.coins, 7);
});
test('cannot spend 8 more', () {
expect(piggy.spend(8), isFalse);
expect(piggy.coins, 7);
});
});
}00:00 +0: after buying a sweet for 3 has 7 coins left
00:00 +1: after buying a sweet for 3 cannot spend 8 more
00:00 +2: All tests passed!Practice Questions
Work through these in order — they run from beginner to application to challenge.
Beginner
- Which runs before every test, and which runs only once:
setUporsetUpAll? - Use
setUpto give each test a fresh[70, 80]list. One test adds90and checks the length is 3; another checks the list is still[70, 80]. - Skip a test named
'monthly report'with the reason'Not finished'.
Application
-
Predict what this file prints, in order:
setUpAll(() => print('A')); setUp(() => print('B')); tearDown(() => print('C')); group('shop', () { setUp(() => print('D')); tearDown(() => print('E')); test('buys', () => print('F')); }); -
main()declaresvar total = 0;. Test'adds 100'adds 100 and expects100; test'adds 50 more'adds 50 and expects150. Why doesdart test -n 'adds 50'fail? Fix it. -
Where should the tests for
lib/src/shop/cart.dartlive? -
Tag a test
'slow'. Write the commands that run only slow tests and that skip them, and thedart_test.yamlthat declares the tag.
Challenge
- A student saves shared helpers in
test/helpers/accounts_test.dart, anddart testnow fails. What is the error, and what is the fix? - In an outer
setUp, create Sara's account with 1000. In a group "after a withdrawal of 300", add asetUpthat withdraws 300, then test that the balance is 700. - Write one command that runs only the
test/reportsfolder, prints one line per test, and saves raw coverage data intocoverage/.
Practice Answers
Each code answer is the body of main() in a file that imports package:test/test.dart
(and package:bank_app/bank_app.dart for Question 9).
Question 1 — Solution
setUp runs before every test. setUpAll runs once, before the first test.
Question 2 — Solution
late List<int> marks;
setUp(() {
marks = [70, 80];
});
test('adding 90 makes three marks', () {
marks.add(90);
expect(marks, hasLength(3));
});
test('the next test sees only two marks', () {
expect(marks, [70, 80]);
});00:00 +0: adding 90 makes three marks
00:00 +1: the next test sees only two marks
00:00 +2: All tests passed!Question 3 — Solution
test('monthly report', () {
expect(1, 1);
}, skip: 'Not finished');00:00 +0: monthly report
Skip: Not finished
00:00 +0 ~1: All tests skipped.Question 4 — Solution
A
B
D
F
E
CsetUpAll first, then outer and inner setUp, the test, and the tear-downs in reverse.
Question 5 — Solution
The second test depends on the first one having run. Reset total before each test, and let
each test expect only its own change:
late int total;
setUp(() => total = 0);
test('adds 100', () {
total += 100;
expect(total, 100);
});
test('adds 50', () {
total += 50;
expect(total, 50);
});00:00 +0: adds 100
00:00 +1: adds 50
00:00 +2: All tests passed!Question 6 — Solution
In test/shop/cart_test.dart — the same path as in lib/src/, with the _test.dart ending.
Question 7 — Solution
Add tags: 'slow' after the test's body, then:
dart test -t slow
dart test -x slowtags:
slow:Question 8 — Solution
The error is Missing definition of main method: every _test.dart file is run as a test
file. Rename it to test/helpers/accounts.dart.
Question 9 — Solution
late BankAccount account;
setUp(() {
account = BankAccount('Sara');
account.deposit(1000);
});
group('after a withdrawal of 300', () {
setUp(() => account.withdraw(300));
test('the balance is 700', () {
expect(account.balance, 700);
});
});00:00 +0: after a withdrawal of 300 the balance is 700
00:00 +1: All tests passed!Question 10 — Solution
dart test test/reports --reporter expanded --coverage=coverageQuick Revision
- When does
tearDownrun, and does it run after a failing test? - Why must a variable assigned in
setUpbelate? - How can a test pass with
dart testbut fail with-n? - In nested groups, which
setUpruns first? - Why are files in
test/helpers/never run as tests?
Key Takeaways
setUpruns before every test andtearDownafter it;setUpAllandtearDownAllrun once.- Shared mutable state makes tests order-dependent; create mutable objects in
setUp. - Nested groups add their own
setUp: outer first, tear-downs in reverse. - Mirror
lib/intest/; keep helpers intest/helpers/and data intest/fixtures/. - Only
_test.dartfiles run as tests; import helpers with relative paths. skip:parks a test with a reason;tags:plus-t/-xselects tests.dart test <folder or file>,-r expanded, and--coverage-pathcontrol a run.
What's Next?
Lecture 123 — Writing Clean Dart. This closes the Testing category. Our code now has a safety net of fast, independent tests — exactly what makes it safe to improve working code. Next we begin Professional Dart Development by making code easier to read.