On this page25 sections
- Prerequisites
- Connection With Previous Lectures
- Learning Objectives
- 1. What Is a Unit Test?
- 2. Arrange, Act, Assert
- 3. Matchers and equals
- 4. isTrue, isFalse, isNull, isNotNull
- 5. greaterThan and lessThan
- 6. closeTo for Doubles
- 7. Strings and Lists
- 8. isA<T>() — Checking the Type
- 9. reason: — Saying Why
- 10. group() and Test Names That Read as Sentences
- 11. Reading a Failure Report
- 12. Running a Single Test or Group
- 13. 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 119 — Unit Testing
| Category | 20 — Testing |
| Lecture | 119 of 140 |
| Estimated video length | 12 min |
| Target | Dart 3.12.2 |
| Level | Advanced |
expect(result, 240)can only ask "is it exactly this?". Real checks are richer — close to 0.3, contains 'Sara', has 3 items. Matchers ask those questions, andgroup()turns a pile of tests into a readable list of facts.
Contents
Prerequisites · Connection · Objectives · Lesson · Code Examples · Common Mistakes · Best Practices · Mini Challenge · Practice Questions · Answers · Takeaways
Every output in this lecture is real. Each test was run with
dart teston Dart 3.12.2, including the failures, so you will recognize them when you meet them yourself.
Prerequisites
Students should already understand, from Lectures 001–118:
0.1 + 0.2printing0.30000000000000004(Lecture 009) andint.tryParse(Lecture 010)- functions, arrow functions and anonymous functions (Lectures 030–034)
- lists,
whereand maps (Lectures 039–050) lib/,test/andpackage:imports (Lecture 103)test(),expect()and runningdart test(Lecture 118)
Connection With Previous Lectures
In Lecture 118 every test had the same shape: compute something, then
expect(actual, expected). That works while the answer is one exact value. Now try a price:
expect(0.1 + 0.2, 0.3);This test fails, even though the code is right. And how do you check that a greeting mentions Sara, or that a list has three items? You need more ways to describe "correct" — and a way to organize many tests so a failure tells you instantly what broke.
Learning Objectives
By the end of this lecture you will be able to:
- Explain what a unit test is
- Structure a test with Arrange, Act, Assert
- Use matchers:
equals,isTrue,isFalse,isNull,isNotNull,greaterThan,lessThan - Compare doubles safely with
closeTo - Check strings and lists with
contains,startsWith,hasLength,isEmpty,isA<T>() - Add a
reason:and group tests withgroup() - Run a single test or group with
--nameand--plain-name - Read a failure report line by line
1. What Is a Unit Test?
A unit is the smallest piece of code that does one job — usually one function. A unit test checks that one piece on its own, with fixed inputs and a known answer.
Analogy — a car factory tests every brake, light and engine separately before the car is assembled. If a brake fails its test, you know exactly which part to fix.
We use the marks_app project from Lecture 103, with lib/marks_app.dart replaced by:
int total(List<int> marks) {
var sum = 0;
for (var mark in marks) {
sum += mark;
}
return sum;
}
double average(List<int> marks) => total(marks) / marks.length;
String grade(int mark) {
if (mark >= 80) return 'A';
if (mark >= 60) return 'B';
return 'C';
}2. Arrange, Act, Assert
Almost every unit test has three parts, often separated by blank lines:
import 'package:marks_app/marks_app.dart';
import 'package:test/test.dart';
void main() {
test('total adds every mark', () {
// Arrange
var marks = [80, 75, 85];
// Act
var result = total(marks);
// Assert
expect(result, 240);
});
}$ dart test test/aaa_test.dart
00:00 +0: loading test/aaa_test.dart
00:00 +0: total adds every mark
00:00 +1: All tests passed!Arrange prepares the inputs, Act calls the code once, Assert checks the result. (In a terminal, these lines overwrite each other, so you mostly see the last one.)
3. Matchers and equals
The second argument of expect is a matcher: an object that describes what a correct
value looks like. A plain value is shorthand for equals:
import 'package:test/test.dart';
void main() {
test('both lines check the same thing', () {
expect(2 + 3, 5);
expect(2 + 3, equals(5));
expect([1, 2], equals([1, 2]));
});
}00:00 +0: loading test/equals_test.dart
00:00 +0: both lines check the same thing
00:00 +1: All tests passed!equals compares lists item by item, while plain [1, 2] == [1, 2] is false — they are two
separate lists.
4. isTrue, isFalse, isNull, isNotNull
For bool and nullable results, these read best:
import 'package:test/test.dart';
void main() {
test('true and false', () {
var mark = 72;
expect(mark >= 50, isTrue);
expect(mark >= 90, isFalse);
});
test('null and not null', () {
var marks = {'Ali': 80, 'Sara': 92};
expect(marks['Sara'], isNotNull);
expect(marks['Zayan'], isNull);
});
}00:00 +0: loading test/bool_null_test.dart
00:00 +0: true and false
00:00 +1: null and not null
00:00 +2: All tests passed!5. greaterThan and lessThan
Sometimes any value in a range is correct:
import 'package:test/test.dart';
void main() {
test('comparing numbers', () {
var temperature = 31.5;
expect(temperature, greaterThan(30));
expect(temperature, lessThan(40));
expect(temperature, greaterThanOrEqualTo(31.5));
});
}00:00 +0: loading test/compare_test.dart
00:00 +0: comparing numbers
00:00 +1: All tests passed!6. closeTo for Doubles
Here is the test from the start of the lecture, and its real result:
import 'package:test/test.dart';
void main() {
test('0.1 + 0.2 is 0.3', () {
expect(0.1 + 0.2, 0.3);
});
}00:00 +0: loading test/double_test.dart
00:00 +0: 0.1 + 0.2 is 0.3
00:00 +0 -1: 0.1 + 0.2 is 0.3 [E]
Expected: <0.3>
Actual: <0.30000000000000004>(The rest of the report is explained in section 11.) That is the binary rounding from
Lecture 009. closeTo(value, delta) accepts anything within delta of value:
import 'package:test/test.dart';
void main() {
test('0.1 + 0.2 is close to 0.3', () {
expect(0.1 + 0.2, closeTo(0.3, 0.0001));
});
}00:00 +0: loading test/double2_test.dart
00:00 +0: 0.1 + 0.2 is close to 0.3
00:00 +1: All tests passed!Rule: when a
doublecomes from a calculation, test it withcloseTo.
7. Strings and Lists
These matchers check part of a value:
import 'package:test/test.dart';
void main() {
test('checking text', () {
var message = 'Hello, Sara! Your total is 240.';
expect(message, startsWith('Hello'));
expect(message, endsWith('240.'));
expect(message, contains('Sara'));
});
test('checking a list', () {
var students = ['Ali', 'Sara', 'Zayan'];
expect(students, hasLength(3));
expect(students, contains('Sara'));
expect(students, isNotEmpty);
expect(<String>[], isEmpty);
});
}00:00 +0: loading test/text_list_test.dart
00:00 +0: checking text
00:00 +1: checking a list
00:00 +2: All tests passed!contains works on both strings and lists.
8. isA<T>() — Checking the Type
isA<T>() passes when the value is a T, like is from Lecture 010:
import 'package:test/test.dart';
void main() {
test('checking a type', () {
var average = 240 / 3;
expect(average, isA<double>());
expect(average, 80);
});
}00:00 +0: loading test/isa_test.dart
00:00 +0: checking a type
00:00 +1: All tests passed!/ always gives a double, and 80.0 == 80 is true, so both checks pass.
9. reason: — Saying Why
expect takes an optional reason: that is printed when the check fails:
import 'package:test/test.dart';
void main() {
test('reason', () {
var balance = 500;
expect(balance - 700, greaterThan(0), reason: 'Ali withdrew 700 from 500');
});
}00:00 +0 -1: reason [E]
Expected: a value greater than <0>
Actual: <-200>
Which: is not a value greater than <0>
Ali withdrew 700 from 500Use it when the values alone would not explain what went wrong.
10. group() and Test Names That Read as Sentences
group(description, body) collects related tests. Dart joins the group's name and the
test's name, so write them to form a sentence:
import 'package:marks_app/marks_app.dart';
import 'package:test/test.dart';
void main() {
group('grade', () {
test('gives A for 80 and above', () {
expect(grade(80), 'A');
});
test('gives B for 60 to 79', () {
expect(grade(79), 'B');
});
test('gives C below 60', () {
expect(grade(59), 'C');
});
});
}$ dart test
00:00 +0: loading test/marks_app_test.dart
00:00 +0: test/marks_app_test.dart: grade gives A for 80 and above
00:00 +1: test/marks_app_test.dart: grade gives B for 60 to 79
00:00 +2: test/marks_app_test.dart: grade gives C below 60
00:00 +3: All tests passed!"grade gives B for 60 to 79" is a specification anyone can read; "test 2" tells nobody
anything. We will organize larger test files, with setUp and shared data, in Lecture 122.
11. Reading a Failure Report
Suppose someone changes mark >= 80 to mark > 80 in grade. Run dart test again:
00:00 +0: test/marks_app_test.dart: grade gives A for 80 and above
00:00 +0 -1: test/marks_app_test.dart: grade gives A for 80 and above [E]
Expected: 'A'
Actual: 'B'
Which: is different.
Expected: A
Actual: B
^
Differ at offset 0
package:matcher expect
test/marks_app_test.dart 7:7 main.<fn>.<fn>
00:00 +0 -1: test/marks_app_test.dart: grade gives B for 60 to 79
00:00 +1 -1: test/marks_app_test.dart: grade gives C below 60
00:00 +2 -1: Some tests failed.
Failing tests:
test/marks_app_test.dart: grade gives A for 80 and above(The loading line and two hint lines at the end are left out.) Read it top to bottom:
+0 -1— none passed, one failed so far.[E]marks the failing test.- The name says what is broken: grade gives A for 80 and above.
- Expected / Actual shows the values; Which explains the difference.
marks_app_test.dart 7:7is the file, line and column of the failingexpect.- The other tests still run, and the summary lists every failure.
12. Running a Single Test or Group
--name (short -n) runs only tests whose full name contains the text. dart test --name grade runs the whole grade group; a longer text picks one test:
$ dart test -n 'gives B'
00:00 +0: loading test/marks_app_test.dart
00:00 +0: test/marks_app_test.dart: grade gives B for 60 to 79
00:00 +1: All tests passed!--name treats its text as a regular expression, where +, ( and . have special
meanings. To match the text exactly as typed, use --plain-name (short -N):
$ dart test -n '0.1 + 0.2'
00:00 +0: loading test/sum_test.dart
No tests ran.
No tests match regular expression "0.1 + 0.2".$ dart test -N '0.1 + 0.2'
00:00 +0: loading test/sum_test.dart
00:00 +0: test/sum_test.dart: 0.1 + 0.2 is close to 0.3
00:00 +1: All tests passed!13. A Real-World Example
Add a report line to lib/marks_app.dart:
String report(String name, List<int> marks) =>
'$name: total ${total(marks)}, grade ${grade(average(marks).round())}';Then test every function, one group each:
import 'package:marks_app/marks_app.dart';
import 'package:test/test.dart';
void main() {
var marks = [80, 75, 85];
group('total', () {
test('adds every mark', () {
expect(total(marks), 240);
});
});
group('average', () {
test('is a double', () {
expect(average(marks), isA<double>());
});
test('of 70, 71 and 71 is about 70.67', () {
expect(average([70, 71, 71]), closeTo(70.67, 0.01));
});
});
group('grade', () {
test('gives A from 80 upwards', () {
expect(grade(80), 'A', reason: '80 is the lowest A');
});
});
group('report', () {
test('starts with the name and shows the total and grade', () {
var line = report('Ali', marks);
expect(line, startsWith('Ali'));
expect(line, contains('total 240'));
expect(line, contains('grade A'));
});
});
}$ dart test
00:00 +0: loading test/marks_app_test.dart
00:00 +0: test/marks_app_test.dart: total adds every mark
00:00 +1: test/marks_app_test.dart: average is a double
00:00 +2: test/marks_app_test.dart: average of 70, 71 and 71 is about 70.67
00:00 +3: test/marks_app_test.dart: grade gives A from 80 upwards
00:00 +4: test/marks_app_test.dart: report starts with the name and shows the total and grade
00:00 +5: All tests passed!Five named facts about the code, checked in under a second. Change anything in lib/ and
one command tells you whether they still hold.
Code Examples
Each example is one file in test/. To keep it self-contained, the function under test sits
in the test file; in a real project it lives in lib/.
Example 1 — Arrange, Act, Assert
import 'package:test/test.dart';
void main() {
test('price times quantity gives the total', () {
var price = 450;
var quantity = 2;
var total = price * quantity;
expect(total, 900);
});
}00:00 +0: loading test/e1_test.dart
00:00 +0: price times quantity gives the total
00:00 +1: All tests passed!Example 2 — equals
import 'package:test/test.dart';
void main() {
test('toUpperCase turns ali into ALI', () {
var name = 'ali';
var result = name.toUpperCase();
expect(result, equals('ALI'));
});
}00:00 +0: loading test/e2_test.dart
00:00 +0: toUpperCase turns ali into ALI
00:00 +1: All tests passed!Example 3 — isTrue and isFalse
import 'package:test/test.dart';
void main() {
test('10 is even and 7 is not', () {
expect(10.isEven, isTrue);
expect(7.isEven, isFalse);
});
}00:00 +0: loading test/e3_test.dart
00:00 +0: 10 is even and 7 is not
00:00 +1: All tests passed!Example 4 — isNull and isNotNull
import 'package:test/test.dart';
void main() {
test('tryParse gives null for text that is not a number', () {
expect(int.tryParse('abc'), isNull);
expect(int.tryParse('42'), isNotNull);
});
}00:00 +0: loading test/e4_test.dart
00:00 +0: tryParse gives null for text that is not a number
00:00 +1: All tests passed!Example 5 — closeTo
import 'package:test/test.dart';
void main() {
test('three items of 1.1 cost about 3.3', () {
var total = 1.1 * 3;
expect(total, closeTo(3.3, 0.001));
});
}00:00 +0: loading test/e5_test.dart
00:00 +0: three items of 1.1 cost about 3.3
00:00 +1: All tests passed!1.1 * 3 is 3.3000000000000003, so expect(total, 3.3) would fail.
Example 6 — List and String Matchers
import 'package:test/test.dart';
void main() {
test('the class list has three students', () {
var students = ['Ali', 'Sara', 'Zayan'];
expect(students, hasLength(3));
expect(students, contains('Zayan'));
expect(students.first, startsWith('A'));
expect(students.where((s) => s.length > 5), isEmpty);
});
}00:00 +0: loading test/e6_test.dart
00:00 +0: the class list has three students
00:00 +1: All tests passed!Example 7 — Student Marks
import 'package:test/test.dart';
String grade(int mark) {
if (mark >= 80) return 'A';
if (mark >= 60) return 'B';
return 'C';
}
void main() {
group('grade', () {
test('gives A for 80', () {
expect(grade(80), 'A');
});
test('gives B for 79', () {
expect(grade(79), 'B');
});
test('gives C for 59', () {
expect(grade(59), 'C');
});
});
}00:00 +0: loading test/e7_test.dart
00:00 +0: grade gives A for 80
00:00 +1: grade gives B for 79
00:00 +2: grade gives C for 59
00:00 +3: All tests passed!The inputs sit right on the edges — 80, 79, 59 — because that is where a > versus >=
mistake hides.
Example 8 — Shopping Total
import 'package:test/test.dart';
double cartTotal(List<double> prices) {
var sum = 0.0;
for (var price in prices) {
sum += price;
}
return sum;
}
void main() {
group('cartTotal', () {
test('adds the price of every item', () {
var prices = [99.99, 150.0, 49.98];
var total = cartTotal(prices);
expect(total, closeTo(299.97, 0.001));
});
test('is 0 for an empty cart', () {
expect(cartTotal([]), 0, reason: 'an empty cart costs nothing');
});
});
}00:00 +0: loading test/e8_test.dart
00:00 +0: cartTotal adds the price of every item
00:00 +1: cartTotal is 0 for an empty cart
00:00 +2: All tests passed!Example 9 — Bank Balance
import 'package:test/test.dart';
double withdraw(double balance, double amount) => balance - amount;
void main() {
test('withdrawing 250 from 1000 leaves 750', () {
var balance = 1000.0;
var result = withdraw(balance, 250);
expect(result, isA<double>());
expect(result, 750);
expect(result, greaterThanOrEqualTo(0), reason: 'balance went negative');
});
}00:00 +0: loading test/e9_test.dart
00:00 +0: withdrawing 250 from 1000 leaves 750
00:00 +1: All tests passed!Example 10 — Temperature Conversion
import 'package:test/test.dart';
double toFahrenheit(double celsius) => celsius * 9 / 5 + 32;
void main() {
group('toFahrenheit', () {
test('turns 0 into 32', () {
expect(toFahrenheit(0), 32);
});
test('turns 100 into 212', () {
expect(toFahrenheit(100), 212);
});
test('turns 36.6 into about 97.9', () {
expect(toFahrenheit(36.6), closeTo(97.88, 0.01));
});
});
}$ dart test test/e10_test.dart -n 'turns 36'
00:00 +0: loading test/e10_test.dart
00:00 +0: toFahrenheit turns 36.6 into about 97.9
00:00 +1: All tests passed!Common Beginner Mistakes
1. Swapping actual and expected
// expect(240, total([80, 75, 80])); // ✗ expected value first Expected: <235>
Actual: <240>The report now says the code produced 240 — backwards. The real result always goes first:
expect(total(...), 240).
2. Forgetting the import
A test file without import 'package:test/test.dart'; does not even load:
test/noimp_test.dart:3:5: Error: Method not found: 'expect'.
test/noimp_test.dart:2:3: Error: Method not found: 'test'.3. Hiding the values behind isTrue
// expect(students.length == 3, isTrue); // ✗ Expected: true
Actual: <false>You learn nothing about the list. expect(students, hasLength(3)) reports
Which: has length of <2>.
4. Forgetting that string matchers are case-sensitive
// expect('Total: 240', startsWith('total')); // ✗ Expected: a string starting with 'total'
Actual: 'Total: 240'Best Practices
- Follow Arrange, Act, Assert; put the actual value first in
expect. - Choose the most specific matcher.
- Test edge values — 0, empty lists, and exact boundaries like 80 and 79.
- Name groups after the function and tests after the behaviour.
- Run one group with
-nwhile you work, then the whole suite before you finish.
Mini Challenge
Write a group for bool isPassing(int mark) => mark >= 50; with three tests: 50 passes,
100 passes, 49 fails. Use isTrue and isFalse.
Try it yourself before opening the solution.
Show solution
import 'package:test/test.dart';
bool isPassing(int mark) => mark >= 50;
void main() {
group('isPassing', () {
test('is true for a mark of 50', () {
expect(isPassing(50), isTrue);
});
test('is true for a mark of 100', () {
expect(isPassing(100), isTrue);
});
test('is false for a mark of 49', () {
expect(isPassing(49), isFalse);
});
});
}00:00 +0: loading test/mini_test.dart
00:00 +0: isPassing is true for a mark of 50
00:00 +1: isPassing is true for a mark of 100
00:00 +2: isPassing is false for a mark of 49
00:00 +3: All tests passed!Practice Questions
Work through these in order — they run from beginner to application to challenge.
Beginner
- Write a test named
'7 times 6 is 42'that checks7 * 6. - Write a test that checks
'Dart'has a length of 4, usinghasLength. - Which matcher fits each check: (a) a calculated
doubleis about2.5, (b) a map lookup found nothing, (c) a message begins with'Error', (d) a value is aString?
Application
- Write a test that checks
int.tryParse('12a')givesnull. expect(0.7 + 0.1, 0.8)fails. Fix the test so it passes.- For
var team = ['Ali', 'Sara', 'Zayan'];, write one test checking it has 3 members, contains'Zayan', and that its last name ends with'n'. - A test named
'works'sits insidegroup('cartTotal', …). Give it a better name and write the full namedart testwould print. - Write the command that runs only the tests in
group('average', …), and the command that runs only a test named'total + tax is correct'.
Challenge
-
Read this report and answer: which test failed, what did the code produce, and on which line is the failing
expect?00:00 +0 -1: balance grows by the deposit [E] Expected: <800> Actual: <700> package:matcher expect test/q9_test.dart 10:7 main.<fn>.<fn> -
Write a
groupfordouble applyDiscount(double price, double percent) => price - price * percent / 100;with three tests: 20% off 1500 is 1200; 0% leaves 1500 (with areason:); 15% off 99.99 is about 84.99.
Practice Answers
Question 1 — Solution
import 'package:test/test.dart';
void main() {
test('7 times 6 is 42', () {
expect(7 * 6, 42);
});
}00:00 +0: loading test/q1_test.dart
00:00 +0: 7 times 6 is 42
00:00 +1: All tests passed!Question 2 — Solution
import 'package:test/test.dart';
void main() {
test('Dart has four letters', () {
expect('Dart', hasLength(4));
});
}00:00 +0: loading test/q2_test.dart
00:00 +0: Dart has four letters
00:00 +1: All tests passed!hasLength works on strings too.
Question 3 — Solution
(a) closeTo(2.5, 0.001), (b) isNull, (c) startsWith('Error'), (d) isA<String>().
Question 4 — Solution
import 'package:test/test.dart';
void main() {
test('tryParse gives null for 12a', () {
expect(int.tryParse('12a'), isNull);
});
}00:00 +0: loading test/q4_test.dart
00:00 +0: tryParse gives null for 12a
00:00 +1: All tests passed!Question 5 — Solution
The real failure is Actual: <0.7999999999999999>. Use closeTo:
import 'package:test/test.dart';
void main() {
test('0.7 + 0.1 is close to 0.8', () {
expect(0.7 + 0.1, closeTo(0.8, 0.0001));
});
}00:00 +0: loading test/q5_test.dart
00:00 +0: 0.7 + 0.1 is close to 0.8
00:00 +1: All tests passed!Question 6 — Solution
import 'package:test/test.dart';
void main() {
test('the team has three members and includes Zayan', () {
var team = ['Ali', 'Sara', 'Zayan'];
expect(team, hasLength(3));
expect(team, contains('Zayan'));
expect(team.last, endsWith('n'));
});
}00:00 +0: loading test/q6_test.dart
00:00 +0: the team has three members and includes Zayan
00:00 +1: All tests passed!Question 7 — Solution
For example test('adds the price of every item', …). The full name is
cartTotal adds the price of every item.
Question 8 — Solution
dart test -n average
dart test -N 'total + tax is correct'-N is needed because + is special in a regular expression.
Question 9 — Solution
The test balance grows by the deposit failed. The code produced 700, not 800. The
failing expect is on line 10, column 7 of test/q9_test.dart.
Question 10 — Solution
import 'package:test/test.dart';
double applyDiscount(double price, double percent) =>
price - price * percent / 100;
void main() {
group('applyDiscount', () {
test('takes 20 percent off 1500', () {
expect(applyDiscount(1500, 20), 1200);
});
test('leaves the price alone for 0 percent', () {
expect(applyDiscount(1500, 0), 1500, reason: '0% must change nothing');
});
test('takes 15 percent off 99.99', () {
expect(applyDiscount(99.99, 15), closeTo(84.99, 0.01));
});
});
}00:00 +0: loading test/q10_test.dart
00:00 +0: applyDiscount takes 20 percent off 1500
00:00 +1: applyDiscount leaves the price alone for 0 percent
00:00 +2: applyDiscount takes 15 percent off 99.99
00:00 +3: All tests passed!The exact result, 84.9915, is within 0.01 of 84.99.
Quick Revision
- What is a unit?
- What happens in Arrange, Act and Assert?
- Which matcher does
expect(x, 5)use? - Why does
expect(0.1 + 0.2, 0.3)fail, and what fixes it? - Which matchers check part of a string?
- How does
dart testbuild a test's full name inside a group? - What is the difference between
-nand-N?
Key Takeaways
- A unit test checks one small piece of code, structured as Arrange, Act, Assert.
- The second argument of
expectis a matcher; a plain value meansequals. - Test calculated doubles with
closeTo, never exact equality. - Specific matchers (
hasLength,contains,startsWith,isA<T>()) explain failures better thanisTrue;reason:adds your own explanation. group()names join with test names — write them to read as sentences.dart test -nfilters by regular expression,-Nby plain text.- A failure report gives the test name, Expected / Actual / Which, and the file and line.
What's Next?
Lecture 120 — Testing Functions & Classes. So far we tested small functions, one value at a time. Real programs are built from classes whose objects change as you use them — a bank account after three deposits, a cart after removing an item. Next we will test functions with many cases and test objects and their state.