On this page25 sections
- Prerequisites
- Connection With Previous Lectures
- Learning Objectives
- 1. What Is a Test?
- 2. Why Checking by Eye Does Not Scale
- 3. Regressions
- 4. Kinds of Tests
- 5. The test Package Is Already There
- 6. The test/ Folder and _test.dart
- 7. The Code We Will Test
- 8. Your First Test: test() and expect()
- 9. Running dart test — a Pass
- 10. Running dart test — a Failure
- 11. Several Tests in One File
- 12. What Makes a Good Test
- 13. A Real-World Example: Catching a Regression
- Code Examples
- Common Beginner Mistakes
- Best Practices
- Mini Challenge
- Practice Questions
- Practice Answers
- Quick Revision
- Key Takeaways
- What's Next?
Lecture 118 — Introduction to Testing
| Category | 20 — Testing |
| Lecture | 118 of 140 |
| Estimated video length | 8 min |
| Target | Dart 3.12.2 |
| Level | Advanced |
Until now we checked our code by running it and reading the output ourselves. A test is a small program that does that checking for us, and it can rerun every check in a second.
Contents
Prerequisites · Connection · Objectives · Lesson · Code Examples · Common Mistakes · Best Practices · Mini Challenge · Practice Questions · Answers · Takeaways
Every error message in this lecture is real. Each one was produced by running the code shown on Dart 3.12.2, so you will recognize it when you meet it yourself.
Prerequisites
Students should already understand, from Lectures 001–117:
- creating a project with
dart createand itsbin/,lib/andtest/folders (Lecture 002) - functions, lists and classes (Categories 6, 7 and 9)
assert(Lecture 086)package:imports from outsidelib/(Lecture 103)dev_dependenciesinpubspec.yaml(Lecture 104)- running
dartcommands in the terminal (Lecture 113)
Connection With Previous Lectures
So far we have checked our code like this:
void main() {
print(grade(80)); // should be A
print(grade(60)); // should be B
}We run it, read the output, and decide by eye whether it is right. That works for two lines. It does not work for two hundred, and nobody rereads every line after each change. When a small edit quietly breaks something that used to work, we find out from a user, not from Dart. We need checks that Dart can run and judge by itself.
Learning Objectives
By the end of this lecture you will be able to:
- Explain why automated tests are better than checking output by eye
- Understand what a regression is
- Recognize unit, integration and end-to-end tests
- Find the
testpackage in a new project'spubspec.yaml - Name and place test files correctly (
test/…_test.dart) - Write a test with
test()andexpect() - Run tests with
dart testand read both a pass and a failure - Recognize what makes a test good
1. What Is a Test?
A test is code that calls your code and checks that the answer is what you expected.
Analogy — a test is like the answer key a teacher uses to mark an exam. The teacher does not solve every question again; they compare each answer with the key. A test compares what your function returns with the answer you wrote down in advance.
2. Why Checking by Eye Does Not Scale
Checking by eye is slow (a person must read every line), unreliable (79 and 97
look alike late in the day) and forgotten (after a change, nobody rechecks the parts they
did not touch). An automated test runs in a fraction of a second, compares exactly, and runs
every check every time.
3. Regressions
A regression is a bug in something that used to work. Imagine Ali changes
mark >= 80 to mark > 80 while tidying the code. The program still runs — but a student
with exactly 80 now gets a B instead of an A.
Nobody notices — but a test that checks grade(80) would, at once. Catching regressions is
the main reason professional code has tests: they let you change code without fear.
4. Kinds of Tests
Tests come in three sizes:
| Kind | What it checks | Example |
|---|---|---|
| Unit test | One small piece — a function or a class — on its own | grade(80) returns 'A' |
| Integration test | Several pieces working together | read marks from a file, then calculate the report |
| End-to-end test | The whole program, the way a user runs it | run the app and check what it prints |
Unit tests are small and fast, so most tests are unit tests. We will study unit tests in depth in Lecture 119 — Unit Testing.
5. The test Package Is Already There
Dart's testing tools live in a package called test. You do not need to install it:
dart create already added it. Open pubspec.yaml in a new project:
dev_dependencies:
lints: ^6.0.0
test: ^1.25.6It sits under dev_dependencies (Lecture 104) because only development needs it. In an older project without it, dart pub add dev:test adds
it.
6. The test/ Folder and _test.dart
Tests live in the project's test/ folder, and every test file name ends in _test.dart. A
new project already has one:
marks_app/
bin/marks_app.dart
lib/marks_app.dart
test/marks_app_test.dartThe convention is one test file per lib/ file: lib/marks_app.dart is tested by
test/marks_app_test.dart. dart test looks only for files that match this pattern.
7. The Code We Will Test
Replace lib/marks_app.dart with three small functions:
int total(List<int> marks) {
int sum = 0;
for (final 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';
}Our program in bin/ can use them exactly as before:
$ dart run
Total: 240
Average: 80.0
Grade: A8. Your First Test: test() and expect()
Replace test/marks_app_test.dart with:
import 'package:marks_app/marks_app.dart';
import 'package:test/test.dart';
void main() {
test('total adds all marks', () {
expect(total([80, 75, 85]), 240);
});
}- The first import brings in the code being tested; the second brings in
testandexpect. test()takes a description and a function holding the check.expect(actual, expected)compares what the code returned with what it should return. Actual first, expected second.
expect can do much more than check equality. We will meet matchers in Lecture 119.
9. Running dart test — a Pass
Run dart test from the project folder:
$ dart test
00:00 +0: loading test/marks_app_test.dart
00:00 +0: test/marks_app_test.dart: total adds all marks
00:00 +1: All tests passed!00:00 is the time taken, and +1 counts passing tests. In a real terminal these lines are
rewritten in place, so you usually see only the last one:
00:00 +1: All tests passed!10. Running dart test — a Failure
Change the expected value to 250 on purpose and run again:
$ dart test
00:00 +0: loading test/marks_app_test.dart
00:00 +0: test/marks_app_test.dart: total adds all marks
00:00 +0 -1: test/marks_app_test.dart: total adds all marks [E]
Expected: <250>
Actual: <240>
package:matcher expect
test/marks_app_test.dart 6:5 main.<fn>
00:00 +0 -1: Some tests failed.
Failing tests:
test/marks_app_test.dart: total adds all marks
Consider enabling the flag chain-stack-traces to receive more detailed exceptions.
For example, 'dart test --chain-stack-traces'.Read it top to bottom: -1 counts failures, [E] marks the failing test, Expected and
Actual show the disagreement, and 6:5 is the line of the expect that failed. The
command also ends with exit code 1 instead of 0, so other tools can tell that tests
failed. Put 240 back before moving on.
11. Several Tests in One File
One file can hold many tests. Each test() checks one behaviour:
import 'package:marks_app/marks_app.dart';
import 'package:test/test.dart';
void main() {
test('total adds all marks', () {
expect(total([80, 75, 85]), 240);
});
test('average divides the total by the count', () {
expect(average([80, 75, 85]), 80.0);
});
test('grade gives A for 80 and above', () {
expect(grade(80), 'A');
expect(grade(95), 'A');
});
test('grade gives B from 60 to 79', () {
expect(grade(60), 'B');
expect(grade(79), 'B');
});
test('grade gives C below 60', () {
expect(grade(59), 'C');
});
}00:00 +5: All tests passed!To run one file, name it: dart test test/marks_app_test.dart. To run only tests whose
description contains a word, use --name:
$ dart test --name grade
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 from 60 to 79
00:00 +2: test/marks_app_test.dart: grade gives C below 60
00:00 +3: All tests passed!12. What Makes a Good Test
- It checks one behaviour, and its description says which one.
- It uses fixed values and a hand-worked expected answer, never a value computed by the same code.
- It tests the edges —
80and79, not just95— because bugs hide at boundaries. - It is independent of every other test.
- It fails for a real reason. A test that cannot fail checks nothing.
13. A Real-World Example: Catching a Regression
Now Ali "tidies" grade and writes mark > 80. The program still runs without any error.
But the tests from section 11 catch it:
$ dart test
…
00:00 +2 -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 14:5 main.<fn>
00:00 +2 -1: test/marks_app_test.dart: grade gives B from 60 to 79
00:00 +3 -1: test/marks_app_test.dart: grade gives C below 60
00:00 +4 -1: Some tests failed.
…Four tests pass; one fails and names the exact line — grade(80) on line 14. For strings,
Which: points at the first differing character. Ali restores >=, reruns, and sees
+5: All tests passed!. The bug never reached a student.
Tests also run with assertions switched on, so the assert checks from Lecture 086 fire
inside tests without --enable-asserts.
Code Examples
To keep each example self-contained, the function sits in the test file. In a real project it
lives in lib/ and is imported with package:. Save each as a file in test/ and run it
with dart test test/<file>.
Example 1 — One Test, One Check
import 'package:test/test.dart';
int twice(int number) => number * 2;
void main() {
test('twice doubles a number', () {
expect(twice(4), 8);
});
}00:00 +1: All tests passed!Example 2 — Several Checks in One Test
import 'package:test/test.dart';
int square(int number) => number * number;
void main() {
test('square works for several numbers', () {
expect(square(3), 9);
expect(square(0), 0);
expect(square(-2), 4);
});
}00:00 +1: All tests passed!Three expect calls, one test: +1. The count is tests, not checks.
Example 3 — Testing a String
import 'package:test/test.dart';
String greet(String name) => 'Hello, $name!';
void main() {
test('greet builds a greeting', () {
expect(greet('Sara'), 'Hello, Sara!');
});
}00:00 +1: All tests passed!Example 4 — Testing a bool
import 'package:test/test.dart';
bool isPassing(int mark) => mark >= 50;
void main() {
test('50 and above passes', () {
expect(isPassing(50), true);
});
test('below 50 fails', () {
expect(isPassing(49), false);
});
}00:00 +2: All tests passed!Example 5 — Testing a List
import 'package:test/test.dart';
List<int> evens(List<int> numbers) => numbers.where((n) => n.isEven).toList();
void main() {
test('evens keeps only even numbers', () {
expect(evens([1, 2, 3, 4]), [2, 4]);
});
test('evens of an empty list is empty', () {
expect(evens([]), []);
});
}00:00 +2: All tests passed!expect compares two lists element by element.
Example 6 — A Test That Finds a Bug
import 'package:test/test.dart';
String initials(String first, String last) => '${first[0]}${last[0]}';
void main() {
test('initials takes the first letters', () {
expect(initials('Ali', 'Khan'), 'AK');
});
test('initials are upper case', () {
expect(initials('zayan', 'ahmed'), 'ZA');
});
}…
00:00 +1 -1: initials are upper case [E]
Expected: 'ZA'
Actual: 'za'
Which: is different.
Expected: ZA
Actual: za
^
Differ at offset 0
package:matcher expect
test/initials_test.dart 11:5 main.<fn>
00:00 +1 -1: Some tests failed.The fix is in the function, not the test:
String initials(String first, String last) =>
'${first[0]}${last[0]}'.toUpperCase();00:00 +2: All tests passed!Example 7 — Student Grades at the Boundaries
import 'package:test/test.dart';
String grade(int mark) {
if (mark >= 80) return 'A';
if (mark >= 60) return 'B';
return 'C';
}
void main() {
test('A starts exactly at 80', () {
expect(grade(80), 'A');
expect(grade(79), 'B');
});
test('B starts exactly at 60', () {
expect(grade(60), 'B');
expect(grade(59), 'C');
});
}00:00 +2: All tests passed!Each test checks both sides of one boundary — exactly where a > versus >= bug would show.
Example 8 — Shopping Total
import 'package:test/test.dart';
double cartTotal(List<double> prices, double discountRate) {
double subtotal = 0;
for (final price in prices) {
subtotal += price;
}
return subtotal - subtotal * discountRate;
}
void main() {
test('no discount returns the plain sum', () {
expect(cartTotal([500, 250], 0), 750.0);
});
test('10% discount is taken off', () {
expect(cartTotal([500, 250], 0.10), 675.0);
});
test('an empty cart costs nothing', () {
expect(cartTotal([], 0.10), 0.0);
});
}00:00 +3: All tests passed!The expected values were worked out by hand: 750 − 75 = 675.
Example 9 — Bank Balance
import 'package:test/test.dart';
class BankAccount {
int balance;
BankAccount(this.balance);
bool withdraw(int amount) {
if (amount > balance) return false;
balance -= amount;
return true;
}
}
void main() {
test('withdraw takes money off the balance', () {
BankAccount account = BankAccount(1000);
expect(account.withdraw(250), true);
expect(account.balance, 750);
});
test('withdraw refuses more than the balance', () {
BankAccount account = BankAccount(100);
expect(account.withdraw(250), false);
expect(account.balance, 100);
});
}00:00 +2: All tests passed!Each test creates its own account, so neither depends on the other.
Example 10 — Temperature Conversion
import 'package:test/test.dart';
double toFahrenheit(double celsius) => celsius * 9 / 5 + 32;
void main() {
test('water freezes at 32 °F', () {
expect(toFahrenheit(0), 32.0);
});
test('water boils at 212 °F', () {
expect(toFahrenheit(100), 212.0);
});
test('25 °C is 77 °F', () {
expect(toFahrenheit(25), 77.0);
});
}00:00 +3: All tests passed!Common Beginner Mistakes
1. Forgetting the _test.dart ending
A file named test/marks_check.dart is ignored:
$ dart test
No tests ran.
No tests were found.2. Forgetting to import package:test/test.dart
// import 'package:test/test.dart'; ← missing
// void main() {
// test('total adds all marks', () {
// expect(total([80, 75, 85]), 240);
// });
// }Failed to load "test/a_test.dart":
test/a_test.dart:5:5: Error: Method not found: 'expect'.
expect(total([80, 75, 85]), 240);
^^^^^^
test/a_test.dart:4:3: Error: Method not found: 'test'.
test('total adds all marks', () {
^^^^3. Running dart test outside the project
No pubspec.yaml file found - run this command in your project folder.4. Swapping actual and expected
expect(250, total([80, 75, 85])) still fails, but the message reads backwards:
Expected: <240>
Actual: <250>Keep the order expect(actual, expected).
5. Printing instead of expecting
test('prints the total', () {
print(total([80, 75, 85]));
});This always passes: it prints 240, but nothing compares it with anything.
Best Practices
- Keep one test file per
lib/file, named<name>_test.dart. - Write descriptions that read as a sentence:
'grade gives A for 80 and above'. - Run
dart testafter every change, and before you share your code. - When a test fails, fix the code — change the test only if the expectation was wrong.
Mini Challenge
Shipping is free for carts of 1000 or more, otherwise it costs 150. Write
int shippingCost(int cartTotal) and three tests: a small cart (400), exactly 1000, and
just under (999).
Try it yourself before opening the solution.
Show solution
import 'package:test/test.dart';
int shippingCost(int cartTotal) => cartTotal >= 1000 ? 0 : 150;
void main() {
test('small carts pay 150', () {
expect(shippingCost(400), 150);
});
test('1000 exactly ships free', () {
expect(shippingCost(1000), 0);
});
test('just under 1000 still pays', () {
expect(shippingCost(999), 150);
});
}00:00 +3: All tests passed!1000 and 999 sit on either side of the boundary, so a > instead of >= would fail.
Practice Questions
Beginner
- Which file should hold the tests for
lib/bank.dart? - In which section of
pubspec.yamlistestlisted, and why there? - Write a test that checks
add(2, 3)returns5.
Application
- Write two tests for
bool isAdult(int age) => age >= 18;— one for18, one for17. - A run prints
Expected: <100>andActual: <90>. What happened? - Write a test that checks
discounted(1000)returns900.0, wherediscountedtakes 10% off a price. - Is each a unit, integration or end-to-end test? (a)
grade(59)returns'C'; (b) launching the program and checking its printed report; (c) loading marks from a file and calculating their average. - Write
int highest(List<int> marks)and three tests: best mark in the middle, best mark first, and a single mark.
Challenge
bool isLeapYear(int year) => year % 4 == 0;is wrong: 1900 was not a leap year, but 2000 was. Write tests for 2024, 2023, 1900 and 2000, watch one fail, then fix the function.- Write a
BankAccountwithbalancestarting at0andbool deposit(int amount)that refuses amounts of0or less. Test a valid deposit and a negative one, checking both the return value and the balance.
Practice Answers
Question 1 — Solution
test/bank_test.dart.
Question 2 — Solution
Under dev_dependencies, because tests are needed only while developing, not when the
finished program runs.
Question 3 — Solution
import 'package:test/test.dart';
int add(int a, int b) => a + b;
void main() {
test('add sums two numbers', () {
expect(add(2, 3), 5);
});
}00:00 +1: All tests passed!Question 4 — Solution
import 'package:test/test.dart';
bool isAdult(int age) => age >= 18;
void main() {
test('18 is an adult', () {
expect(isAdult(18), true);
});
test('17 is not an adult', () {
expect(isAdult(17), false);
});
}00:00 +2: All tests passed!Question 5 — Solution
The test expected 100, but the code returned 90. Either the code has a bug or the expected
value was worked out wrongly — check by hand which one.
Question 6 — Solution
import 'package:test/test.dart';
double discounted(double price) => price * 0.9;
void main() {
test('discounted takes 10% off', () {
expect(discounted(1000), 900.0);
});
}00:00 +1: All tests passed!Question 7 — Solution
(a) unit, (b) end-to-end, (c) integration.
Question 8 — Solution
import 'package:test/test.dart';
int highest(List<int> marks) {
int best = marks[0];
for (final mark in marks) {
if (mark > best) best = mark;
}
return best;
}
void main() {
test('highest finds the best mark', () {
expect(highest([70, 95, 80]), 95);
});
test('highest works when the best mark is first', () {
expect(highest([95, 70, 80]), 95);
});
test('highest works with one mark', () {
expect(highest([60]), 60);
});
}00:00 +3: All tests passed!Question 9 — Solution
With the original function, the 1900 test fails:
00:00 +2 -1: 1900 is not a leap year [E]
Expected: <false>
Actual: <true>The fixed function and tests:
import 'package:test/test.dart';
bool isLeapYear(int year) =>
(year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
void main() {
test('2024 is a leap year', () {
expect(isLeapYear(2024), true);
});
test('2023 is not a leap year', () {
expect(isLeapYear(2023), false);
});
test('1900 is not a leap year', () {
expect(isLeapYear(1900), false);
});
test('2000 is a leap year', () {
expect(isLeapYear(2000), true);
});
}00:00 +4: All tests passed!Question 10 — Solution
import 'package:test/test.dart';
class BankAccount {
int balance = 0;
bool deposit(int amount) {
if (amount <= 0) return false;
balance += amount;
return true;
}
}
void main() {
test('deposit adds to the balance', () {
BankAccount account = BankAccount();
expect(account.deposit(500), true);
expect(account.balance, 500);
});
test('deposit refuses a negative amount', () {
BankAccount account = BankAccount();
expect(account.deposit(-50), false);
expect(account.balance, 0);
});
}00:00 +2: All tests passed!Quick Revision
- What is a regression?
- Name the three kinds of tests.
- Where do test files go, and how must their names end?
- What two arguments does
test()take? - In
expect(a, b), which is the actual value? - What do
+3 -1mean indart testoutput? - Why does a test with only
printalways pass?
Key Takeaways
- A test checks code automatically against an answer written in advance.
- Tests catch regressions — bugs in code that used to work.
- Unit tests check one piece, integration tests several together, end-to-end the whole program.
dart createalready addstestunderdev_dependencies.- Test files live in
test/and end in_test.dart. test('description', () { … })groups checks;expect(actual, expected)makes them.dart testreports passes (+), failures (-), and Expected vs Actual for each failure.- Good tests check one behaviour, use hand-worked answers, and cover the edges.
What's Next?
Lecture 119 — Unit Testing. You can now write and run a test. Next we will look closely at
unit tests: the matchers that make expect far more expressive, and how to choose the cases
that give real confidence in a function.