Skip to content

Expert · Responsive design and testing · topic 40 of 50 · 30 min

Unit tests

Explanation

Unit tests check plain Dart logic: models, validators, calculations and state classes. Write them in the test/ folder with test(), check results with expect() and matchers like equals, isTrue, isNull and throwsA, group related tests with group(), and prepare data in setUp(). Run them with flutter test. Use fakes or mocks (for example with mocktail) for dependencies like APIs.

Why unit tests?

A unit test runs a small piece of code (a function or a class) with known inputs and checks the output. Unit tests are fast (milliseconds), need no device, and catch logic bugs early: a wrong GST calculation, a validator that accepts a 5-digit PIN, a cart that miscounts after removing an item. Flutter projects include the flutter_test package for this.

dart
// lib/cart.dart
class Cart {
  final Map<String, int> _qty = {};
  void add(String item) => _qty[item] = (_qty[item] ?? 0) + 1;
  void remove(String item) {
    final q = _qty[item];
    if (q == null) return;
    if (q <= 1) {
      _qty.remove(item);
    } else {
      _qty[item] = q - 1;
    }
  }
  int get count => _qty.values.fold(0, (a, b) => a + b);
}
Plain Dart logic: perfect for unit tests.
dart
// test/cart_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:chai_shop/cart.dart';

void main() {
  group('Cart', () {
    late Cart cart;
    setUp(() => cart = Cart());                     // fresh cart before each test

    test('starts empty', () {
      expect(cart.count, 0);
    });

    test('adding the same item twice counts two', () {
      cart.add('chai');
      cart.add('chai');
      expect(cart.count, equals(2));
    });

    test('removing an item that is not in the cart does nothing', () {
      cart.remove('samosa');
      expect(cart.count, 0);
    });
  });
}
Each test describes one behaviour. setUp runs before every test so tests do not affect each other.
bash
flutter test                        # all tests
flutter test test/cart_test.dart    # one file
Output shows each passing test and the expected vs actual value for failures.

A good test name reads like a sentence describing behaviour: "adding the same item twice counts two". When it fails, the name alone tells you what broke. Each test follows the same shape: arrange (set up data), act (call the code), assert (check the result with expect).

What should you test first? The logic that would hurt most if it were wrong: money calculations, validators, discount rules, and state classes such as a ChangeNotifier, Notifier or Cubit. These are plain Dart classes, so they are easy to test without any widgets. Keeping logic out of widgets is what makes this possible.

Matchers

MatcherChecks
equals(x) (or just x)Value equality
isTrue / isFalse / isNull / isNotNullBooleans and nulls
contains(x) / hasLength(n)Collections and strings
closeTo(value, delta)Doubles with tolerance
throwsA(isA<ArgumentError>())That a function throws a certain error
dart
test('rejects a negative amount', () {
  expect(() => Wallet().add(-50), throwsA(isA<ArgumentError>()));
});

test('async functions can be tested too', () async {
  final price = await fetchPriceFake('chai');
  expect(price, 15);
});
Wrap code that should throw in a closure. Tests can be async.

Fakes and mocks

Code that talks to the network or a database should not do so in unit tests: slow, flaky and dependent on the internet. Depend on an abstract class (module 2), and in tests pass a fake implementation that returns fixed data. Libraries such as mocktail create mocks whose answers you configure per test and which can verify calls. Start with simple hand-written fakes.

Think of it like this: Unit tests are like a quality checker at a sweets factory weighing each box on a scale before packing: a quick, automatic check on one small thing, done every time, so a wrong batch never reaches a customer.

Common mistakes

  • Tests that depend on each other's data. Use setUp for a fresh state.
  • Comparing doubles with equals: 0.1 + 0.2 is not exactly 0.3. Use closeTo.
  • Calling real APIs in unit tests.
  • Only testing the happy path; test edges like empty input, zero and maximum values.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Unit tests · 1/6

Fast | No device

Unit tests check small pieces of Dart logic quickly, without a device.

Prefer a video by a teacher? Find free videos on “flutter unit testing test expect group setup mocktail” on YouTube ↗

In real life: Testing GST and validators

An invoicing app's GST calculator and form validators are pure functions, each covered by unit tests: 18% on Rs 1,000 is Rs 180, rounding to paise is correct, a PIN starting with 0 is rejected, and an empty GSTIN shows the right message. The whole suite runs in two seconds on every change, so a refactor that breaks tax maths is caught immediately.

dart
int gstPaise(int amountPaise, int ratePercent) => (amountPaise * ratePercent / 100).round();

void main() {
  test('18% GST on Rs 1000', () => expect(gstPaise(100000, 18), 18000));
  test('rounds to the nearest paisa', () => expect(gstPaise(999, 18), 180));
}
999 x 18 / 100 = 179.82, which rounds to 180 paise.

Practice (0/3 done)

Task 1 · Question

How do you test that a function throws an ArgumentError?

Task 2 · Question

Which matcher should compare 0.1 + 0.2 with 0.3?

Task 3 · Practice

Write unit tests for three things: the Cart class from this topic (add, remove, count, removing an absent item), a validateMobile function (empty, 9 digits, starting with 5, valid), and an EMI function with closeTo for a known value. Group them, use setUp, and run flutter test.

Quick quiz

Question 1

1.Where do unit test files go in a Flutter project?

Question 2

2.What does setUp do?

Question 3

3.Why avoid real network calls in unit tests?

Question 4

4.Which package is commonly used for mocks in Dart tests?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

Where do unit test files go in a Flutter project?

Answer: test/

flutter test runs files in test/.

What does setUp do?

Answer: Runs before each test to prepare fresh data

Keeps tests independent.

Why avoid real network calls in unit tests?

Answer: They are slow and flaky and depend on the internet

Replace the API with a fake for fast, reliable tests.

Which package is commonly used for mocks in Dart tests?

Answer: mocktail

mocktail creates configurable mocks.

Your code sends real SMS messages. How should a unit test handle this?Related

Answer: Pass in a fake SMS sender that only records messages

A fake (a test double) stands in for the slow or risky part, and the test checks what the code asked it to do.

From Unit testing: checking one piece at a time

What is a risk of over-mocking?Related

Answer: Tests mirror the implementation and break on every harmless refactor

Test behaviour and outcomes, not every internal call.

From Test doubles: fakes, stubs and mocks

Why mock fetch in a unit test?Related

Answer: So the test is fast, works offline and gets the same answer every time

Real network calls are slow and can change or fail, which makes tests flaky.

From Testing basics: unit, integration and end-to-end

What makes a test "flaky"?Related

Answer: It sometimes fails without any code change

Usually time, randomness, network or shared state.

From Unit testing with JUnit

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 2 more topics in “Responsive design and testing” before its exam: Widget tests, Integration tests and CI.
  2. Module 10 exam: Responsive design and testing · 12 questions · 25 min, without help. 4 topics of this module still to finish first.
  3. Then the next module, Publishing to the stores and the web. 50 topics left in the course before the final exam.

← Previous: Responsive and adaptive design