Watch the video (8 min)
On this page25 sections
  1. Prerequisites
  2. Connection With Previous Lectures
  3. Learning Objectives
  4. 1. What Is a Test?
  5. 2. Why Checking by Eye Does Not Scale
  6. 3. Regressions
  7. 4. Kinds of Tests
  8. 5. The test Package Is Already There
  9. 6. The test/ Folder and _test.dart
  10. 7. The Code We Will Test
  11. 8. Your First Test: test() and expect()
  12. 9. Running dart test — a Pass
  13. 10. Running dart test — a Failure
  14. 11. Several Tests in One File
  15. 12. What Makes a Good Test
  16. 13. A Real-World Example: Catching a Regression
  17. Code Examples
  18. Common Beginner Mistakes
  19. Best Practices
  20. Mini Challenge
  21. Practice Questions
  22. Practice Answers
  23. Quick Revision
  24. Key Takeaways
  25. What's Next?

Lecture 118 — Introduction to Testing

Category20 — Testing
Lecture118 of 140
Estimated video length8 min
TargetDart 3.12.2
LevelAdvanced

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 create and its bin/, lib/ and test/ folders (Lecture 002)
  • functions, lists and classes (Categories 6, 7 and 9)
  • assert (Lecture 086)
  • package: imports from outside lib/ (Lecture 103)
  • dev_dependencies in pubspec.yaml (Lecture 104)
  • running dart commands in the terminal (Lecture 113)

Connection With Previous Lectures

So far we have checked our code like this:

dart
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 test package in a new project's pubspec.yaml
  • Name and place test files correctly (test/…_test.dart)
  • Write a test with test() and expect()
  • Run tests with dart test and 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:

KindWhat it checksExample
Unit testOne small piece — a function or a class — on its owngrade(80) returns 'A'
Integration testSeveral pieces working togetherread marks from a file, then calculate the report
End-to-end testThe whole program, the way a user runs itrun 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:

yaml
dev_dependencies:
  lints: ^6.0.0
  test: ^1.25.6

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

output
marks_app/
  bin/marks_app.dart
  lib/marks_app.dart
  test/marks_app_test.dart

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

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

output
$ dart run
Total: 240
Average: 80.0
Grade: A

8. Your First Test: test() and expect()

Replace test/marks_app_test.dart with:

dart
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 test and expect.
  • 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:

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

output
00:00 +1: All tests passed!

10. Running dart test — a Failure

Change the expected value to 250 on purpose and run again:

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

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

output
$ 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 — 80 and 79, not just 95 — 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:

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

dart
import 'package:test/test.dart';

int twice(int number) => number * 2;

void main() {
  test('twice doubles a number', () {
    expect(twice(4), 8);
  });
}
output
00:00 +1: All tests passed!

Example 2 — Several Checks in One Test

dart
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);
  });
}
output
00:00 +1: All tests passed!

Three expect calls, one test: +1. The count is tests, not checks.

Example 3 — Testing a String

dart
import 'package:test/test.dart';

String greet(String name) => 'Hello, $name!';

void main() {
  test('greet builds a greeting', () {
    expect(greet('Sara'), 'Hello, Sara!');
  });
}
output
00:00 +1: All tests passed!

Example 4 — Testing a bool

dart
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);
  });
}
output
00:00 +2: All tests passed!

Example 5 — Testing a List

dart
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([]), []);
  });
}
output
00:00 +2: All tests passed!

expect compares two lists element by element.

Example 6 — A Test That Finds a Bug

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

dart
String initials(String first, String last) =>
    '${first[0]}${last[0]}'.toUpperCase();
output
00:00 +2: All tests passed!

Example 7 — Student Grades at the Boundaries

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

dart
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);
  });
}
output
00:00 +3: All tests passed!

The expected values were worked out by hand: 750 − 75 = 675.

Example 9 — Bank Balance

dart
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);
  });
}
output
00:00 +2: All tests passed!

Each test creates its own account, so neither depends on the other.

Example 10 — Temperature Conversion

dart
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);
  });
}
output
00:00 +3: All tests passed!

Common Beginner Mistakes

1. Forgetting the _test.dart ending

A file named test/marks_check.dart is ignored:

output
$ dart test
No tests ran.
No tests were found.

2. Forgetting to import package:test/test.dart

dart
// import 'package:test/test.dart';   ← missing
// void main() {
//   test('total adds all marks', () {
//     expect(total([80, 75, 85]), 240);
//   });
// }
output
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

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

output
  Expected: <240>
    Actual: <250>

Keep the order expect(actual, expected).

5. Printing instead of expecting

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

  1. Which file should hold the tests for lib/bank.dart?
  2. In which section of pubspec.yaml is test listed, and why there?
  3. Write a test that checks add(2, 3) returns 5.

Application

  1. Write two tests for bool isAdult(int age) => age >= 18; — one for 18, one for 17.
  2. A run prints Expected: <100> and Actual: <90>. What happened?
  3. Write a test that checks discounted(1000) returns 900.0, where discounted takes 10% off a price.
  4. 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.
  5. Write int highest(List<int> marks) and three tests: best mark in the middle, best mark first, and a single mark.

Challenge

  1. 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.
  2. Write a BankAccount with balance starting at 0 and bool deposit(int amount) that refuses amounts of 0 or 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

dart
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);
  });
}
output
00:00 +1: All tests passed!

Question 4 — Solution

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

dart
import 'package:test/test.dart';

double discounted(double price) => price * 0.9;

void main() {
  test('discounted takes 10% off', () {
    expect(discounted(1000), 900.0);
  });
}
output
00:00 +1: All tests passed!

Question 7 — Solution

(a) unit, (b) end-to-end, (c) integration.

Question 8 — Solution

dart
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);
  });
}
output
00:00 +3: All tests passed!

Question 9 — Solution

With the original function, the 1900 test fails:

output
00:00 +2 -1: 1900 is not a leap year [E]
  Expected: <false>
    Actual: <true>

The fixed function and tests:

dart
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);
  });
}
output
00:00 +4: All tests passed!

Question 10 — Solution

dart
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);
  });
}
output
00:00 +2: All tests passed!

Quick Revision

  1. What is a regression?
  2. Name the three kinds of tests.
  3. Where do test files go, and how must their names end?
  4. What two arguments does test() take?
  5. In expect(a, b), which is the actual value?
  6. What do +3 -1 mean in dart test output?
  7. Why does a test with only print always 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 create already adds test under dev_dependencies.
  • Test files live in test/ and end in _test.dart.
  • test('description', () { … }) groups checks; expect(actual, expected) makes them.
  • dart test reports 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.


Video lecture

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