Watch the video (9 min)
On this page23 sections
  1. Prerequisites
  2. Connection With Previous Lectures
  3. Learning Objectives
  4. 1. The Project
  5. 2. setUp — Before Every Test
  6. 3. The Run Order
  7. 4. tearDown — Cleaning Up
  8. 5. Why Every Test Needs a Fresh Start
  9. 6. Nested Groups with Their Own setUp
  10. 7. Mirror lib/ in test/
  11. 8. Helpers and Fixtures
  12. 9. skip: and tags:
  13. 10. Running Parts of the Suite
  14. 11. A Real-World Example
  15. Code Examples
  16. Common Beginner Mistakes
  17. Best Practices
  18. Mini Challenge
  19. Practice Questions
  20. Practice Answers
  21. Quick Revision
  22. Key Takeaways
  23. What's Next?

Lecture 122 — Test Setup & Organization

Category20 — Testing
Lecture122 of 140
Estimated video length9 min
TargetDart 3.12.2
LevelAdvanced

Every test so far built its own objects from scratch, line by line. setUp and tearDown prepare and clean up once per test, and a tidy test/ 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 test on Dart 3.12.2, so you will recognize it when you meet it yourself.


Prerequisites

Students should already understand, from Lectures 006–121:

  • late variables (Lecture 006)
  • lib/src/, export and package: imports (Lectures 102–103)
  • reading files and temporary folders with dart:io (Lectures 106–107)
  • test, expect, matchers, group() and dart test -n (Lectures 118–119)

Connection With Previous Lectures

In Lecture 120 every BankAccount test began the same way:

dart
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 setUpAll and tearDownAll
  • Explain why shared mutable state makes tests order-dependent
  • Nest groups that each add their own setUp
  • Organize test/ to mirror lib/, with helper and fixture files
  • Mark tests with tags: and skip:
  • 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:

dart
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);
  });
}
output
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. setUp resets 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:

dart
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'));
}
output
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):

dart
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');
  });
}
output
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:

dart
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):

output
$ 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:

dart
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'));
  });
}
output
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:

output
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.txt

dart 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:

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:

dart
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);
  });
}
output
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:

dart
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');
}
output
$ 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:

yaml
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:

output
$ 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:

output
$ 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):

output
SF:.../bank_app/lib/src/account.dart
LF:8
LH:8

All 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:

dart
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);
      });
    });
  });
}
output
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

dart
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']);
  });
}
output
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

dart
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);
  });
}
output
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

dart
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));
  });
}
output
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

dart
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);
      });
    });
  });
}
output
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

dart
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');
}
output
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

dart
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');
}
output
$ 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

dart
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']);
    });
  });
}
output
00:00 +0: gradebook adds Zayan
00:00 +1: gradebook removes Ali
00:00 +2: All tests passed!

Example 8 — Shopping Total

dart
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);
    });
  });
}
output
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

dart
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);
  });
}
output
00:00 +0: Sara can withdraw part of her balance
00:00 +1: All tests passed!

Example 10 — Temperature Readings

dart
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));
  });
}
output
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

dart
// BankAccount account; // ✗
// setUp(() { account = BankAccount('Ali'); });
output
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

output
  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

output
  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 setUp and tearDown at the top of main() or the group.
  • Mirror lib/ in test/: one _test.dart file per library file.
  • Keep shared code in test/helpers/ and sample data in test/fixtures/.
  • Always give skip: a reason, and declare tags in dart_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
dart
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);
    });
  });
}
output
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

  1. Which runs before every test, and which runs only once: setUp or setUpAll?
  2. Use setUp to give each test a fresh [70, 80] list. One test adds 90 and checks the length is 3; another checks the list is still [70, 80].
  3. Skip a test named 'monthly report' with the reason 'Not finished'.

Application

  1. Predict what this file prints, in order:

    dart
    setUpAll(() => print('A'));
    setUp(() => print('B'));
    tearDown(() => print('C'));
    
    group('shop', () {
      setUp(() => print('D'));
      tearDown(() => print('E'));
    
      test('buys', () => print('F'));
    });
  2. main() declares var total = 0;. Test 'adds 100' adds 100 and expects 100; test 'adds 50 more' adds 50 and expects 150. Why does dart test -n 'adds 50' fail? Fix it.

  3. Where should the tests for lib/src/shop/cart.dart live?

  4. Tag a test 'slow'. Write the commands that run only slow tests and that skip them, and the dart_test.yaml that declares the tag.

Challenge

  1. A student saves shared helpers in test/helpers/accounts_test.dart, and dart test now fails. What is the error, and what is the fix?
  2. In an outer setUp, create Sara's account with 1000. In a group "after a withdrawal of 300", add a setUp that withdraws 300, then test that the balance is 700.
  3. Write one command that runs only the test/reports folder, prints one line per test, and saves raw coverage data into coverage/.

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

dart
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]);
});
output
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

dart
test('monthly report', () {
  expect(1, 1);
}, skip: 'Not finished');
output
00:00 +0: monthly report
  Skip: Not finished
00:00 +0 ~1: All tests skipped.

Question 4 — Solution

output
A
B
D
F
E
C

setUpAll 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:

dart
late int total;

setUp(() => total = 0);

test('adds 100', () {
  total += 100;
  expect(total, 100);
});

test('adds 50', () {
  total += 50;
  expect(total, 50);
});
output
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:

output
dart test -t slow
dart test -x slow
yaml
tags:
  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

dart
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);
  });
});
output
00:00 +0: after a withdrawal of 300 the balance is 700
00:00 +1: All tests passed!

Question 10 — Solution

output
dart test test/reports --reporter expanded --coverage=coverage

Quick Revision

  1. When does tearDown run, and does it run after a failing test?
  2. Why must a variable assigned in setUp be late?
  3. How can a test pass with dart test but fail with -n?
  4. In nested groups, which setUp runs first?
  5. Why are files in test/helpers/ never run as tests?

Key Takeaways

  • setUp runs before every test and tearDown after it; setUpAll and tearDownAll run 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/ in test/; keep helpers in test/helpers/ and data in test/fixtures/.
  • Only _test.dart files run as tests; import helpers with relative paths.
  • skip: parks a test with a reason; tags: plus -t / -x selects tests.
  • dart test <folder or file>, -r expanded, and --coverage-path control 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.


Video lecture

Subscribe
Placeholder video — the real recording isn't linked yet.