---
title: 'Test Types'
source: 'https://academia.sh/en/courses/flutter/test-types'
course: Flutter
language: en
updated: '2026-10-21T05:02:53+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Distinguishes what a unit test, a widget test, and an integration test each check'
    - 'Writes a check that compares the expected sizing with the measured record'
    - 'Reads the output of a check that passes and a check that fails'
    - 'Shows that the same screen can be sized differently on two test surfaces'
---

# Test Types

A test compares the expected sizing with the measured record. The lesson measures the same screen on two test surfaces and shows what a test cannot see.

The earlier lessons built the structure of the app: widgets were arranged into a tree, and state and a platform channel were connected. There is a working screen, but one person looking at it and saying that it works is not enough. This lesson explains how to write down the expected behavior of a screen and have the machine check it on every change. The subject is test types; the question that drives the course, who determines this widget's width, also becomes a test expectation in this lesson.

## Three Test Types

Tests are separated by what they bring together. A **unit test** takes a single function or class, isolates it from its surroundings, and checks the output for a given input. There is no tree, no screen, and no platform; the test is fast, and it is easy for it to say why it failed. A **widget test** builds a single widget or a small widget tree with the real framework, but does not open a real device. The test builds the tree, simulates a tap, and then queries the tree for the presence or the value of a widget. An **integration test** runs the whole app, or a large part of it, on a device or an emulator and walks the path the user would follow from start to finish.

The trade-off among the three is simple. A unit test runs quickly but does not see how widgets fit together. An integration test sees everything but is slow, its setup is heavy, and when it fails it does not say where the error is. A widget test sits between the two: it sees the tree and does not see the platform. Sizing, the axis of this course, lives in exactly this middle layer. The width of a widget depends on the constraint from its parent and on its own request; these two inputs appear when a tree is built, not by calling a single function and not by walking the app from start to finish.

## The Measured Layer

Every lesson of the course uses the same plain Dart model. The model gives a widget tree the screen width and records who determines each node's width with four names: **forced**, **chosen**, **clamped**, **stretched**. The file below is the core of this lesson; it produces no output, and the measured blocks import it.

```dart
// sizing.dart — classifies who decides a widget's width
const sizings = ['forced', 'chosen', 'clamped', 'stretched'];

class Limit {
  final int min;
  final int? max; // null: unbounded
  const Limit(this.min, this.max);

  bool get tight => max == min;
  Limit loosen() => Limit(0, max);
  Limit deflate(int by) => Limit(_floor(min - by), max == null ? null : _floor(max! - by));
  String bounds() => '$min..${max ?? 'inf'}';
}

int _floor(int value) => value < 0 ? 0 : value;

class Part {
  final String kind;
  final String name;
  final int? width; // null: as wide as it can get
  final List<Part> children;
  final int factor;
  final bool tightFlex;
  const Part(this.kind, this.name, this.width, this.children,
      {this.factor = 0, this.tightFlex = true});
}

Part leaf(String name, int width) => Part('leaf', name, width, const []);
Part fill(String name) => Part('leaf', name, null, const []);
Part pad(String name, int padding, Part child) => Part('pad', name, padding, [child]);
Part center(String name, Part child) => Part('center', name, null, [child]);
Part fixed(String name, int width, Part child) => Part('fixed', name, width, [child]);
Part column(String name, List<Part> children) => Part('column', name, null, children);
Part row(String name, List<Part> children) => Part('row', name, null, children);
Part flex(String name, int factor, Part child, {bool tight = true}) =>
    Part('flex', name, null, [child], factor: factor, tightFlex: tight);

class Sizing {
  final String sizing;
  final String path;
  final int width;
  final Limit limit;
  const Sizing(this.sizing, this.path, this.width, this.limit);
}

Sizing classify(String path, int? desire, Limit limit) {
  if (limit.tight) return Sizing('forced', path, limit.min, limit);
  if (desire == null) {
    return limit.max == null
        ? Sizing('clamped', path, limit.min, limit)
        : Sizing('stretched', path, limit.max!, limit);
  }
  if (desire < limit.min) return Sizing('clamped', path, limit.min, limit);
  if (limit.max != null && desire > limit.max!) {
    return Sizing('clamped', path, limit.max!, limit);
  }
  return Sizing('chosen', path, desire, limit);
}

int layout(Part part, Limit limit, List<Sizing> log, [String path = '']) {
  final here = '$path/${part.name}';
  final below = <Sizing>[];
  if (part.kind == 'fixed') {
    final own = classify(here, part.width, limit);
    layout(part.children.first, Limit(own.width, own.width), below, here);
    log..add(own)..addAll(below);
    return own.width;
  }
  int? desire;
  switch (part.kind) {
    case 'leaf':
      desire = part.width;
    case 'pad':
      final space = 2 * part.width!;
      desire = layout(part.children.first, limit.deflate(space), below, here) + space;
    case 'center':
      final inner = layout(part.children.first, limit.loosen(), below, here);
      desire = limit.max == null ? inner : null;
    case 'flex':
      desire = layout(part.children.first, limit, below, here);
    case 'column':
      var widest = 0;
      for (final child in part.children) {
        final width = layout(child, limit.loosen(), below, here);
        if (width > widest) widest = width;
      }
      desire = widest;
    case 'row':
      final logs = [for (final _ in part.children) <Sizing>[]];
      final bounded = limit.max != null;
      var used = 0;
      var factors = 0;
      for (var i = 0; i < part.children.length; i++) {
        final child = part.children[i];
        if (child.kind == 'flex' && bounded) {
          factors += child.factor;
        } else {
          used += layout(child, const Limit(0, null), logs[i], here);
        }
      }
      final left = bounded ? _floor(limit.max! - used) : 0;
      var given = 0;
      var seen = 0;
      for (var i = 0; i < part.children.length; i++) {
        final child = part.children[i];
        if (child.kind != 'flex' || !bounded) continue;
        seen += child.factor;
        final share = (seen == factors ? left : left * seen ~/ factors) - given;
        given += share;
        final room = child.tightFlex ? Limit(share, share) : Limit(0, share);
        used += layout(child, room, logs[i], here);
      }
      for (final entries in logs) {
        below.addAll(entries);
      }
      desire = bounded ? null : used;
  }
  final own = classify(here, desire, limit);
  log..add(own)..addAll(below);
  return own.width;
}

List<Sizing> measure(Part root, int screen) {
  final log = <Sizing>[];
  layout(root, Limit(screen, screen), log);
  return log;
}

void printSizes(List<Sizing> log) {
  for (final entry in log) {
    print('${entry.sizing}: ${entry.path} width=${entry.width} constraint=${entry.limit.bounds()}');
  }
}

void countSizings(List<Sizing> log) {
  for (final name in sizings) {
    print('$name: ${log.where((entry) => entry.sizing == name).length}');
  }
  print('parent: ${log.where((entry) => entry.sizing != 'chosen').length}');
}
```

## Unit Test: No Tree

The snippet below is a unit test. It checks a counter function and asks the framework for only a test definition and an expectation. Because it is framework code, this snippet does not run; the lesson claims no output for it.

```dart
// Platform code: unit test
void main() {
  test('counter increments by one', () {
    final counter = Counter();
    counter.increment();
    expect(counter.value, 1);
  });
}
```

The test has three parts: setup, action, expectation. A `Counter` is built, `increment` is called, and the value is expected to be one. No width, constraint, or screen appears anywhere. A unit test cannot answer the question of this course, because the question requires a widget to stand inside a parent.

## Widget Test: A Tree, No Platform

A widget test builds the tree. The framework offers a test environment for this: the widget is placed on a fake screen, one frame is drawn, and the queries of the test walk the tree.

```dart
// Platform code: widget test
void main() {
  testWidgets('title is visible', (tester) async {
    await tester.pumpWidget(const ExampleScreen());
    expect(find.text('Report'), findsOneWidget);
    expect(tester.getSize(find.byType(Chart)).width, 300);
  });
}
```

The last line matters. The test asks for the width of a widget and expects 300. This expectation assumes that nobody will change the widget's own request of 300 units. The screen width on which the test runs carries this assumption, but the test does not write it down. The test surface, that is, the width of the fake screen of the test, is the hidden input of a widget test.

The sections below make this input visible. In the model, a test is a small function that finds a path in the measured record list and compares the sizing name with the expected one.

## The Screen Under Test

The screen to be checked is a column of two widgets: a title that requests 120 units and a chart whose width can be set. The screen is built by a function that takes the width the chart requests; so the tree depends on state, and each measurement builds its own tree. The function that builds the tree sits in the model file.

```dart
// screen_model.dart — the widget tree of the screen under test
import 'sizing.dart';

Part buildScreen(int chartWant) => pad('Screen', 16, column('Body', [
      leaf('Title', 120),
      leaf('Chart', chartWant),
    ]));
```

The function that compares the expectation also sits in a separate file. It looks for the path in the measured record list; if the path exists, it compares the sizing name with the expected one, and if it does not exist, it says so. The expected value is not given to `printSizes`; only this function writes the output.

```dart
// check_sizing.dart — compares the expected sizing with the measured record
import 'sizing.dart';

void checkSizing(List<Sizing> log, String path, String expected) {
  final found = log.where((entry) => entry.path == path).toList();
  if (found.isEmpty) {
    print('FAIL: $path no record');
    return;
  }
  final actual = found.first.sizing;
  if (actual == expected) {
    print('PASS: $path $expected');
  } else {
    print('FAIL: $path expected=$expected measured=$actual');
  }
}
```

## A Test That Passes on a 400-Unit Surface

First the screen is placed on a test surface of 400 units. The chart requests 300 units, and the test expects the chart's request to be **chosen**. The block below prints the whole record and then runs the check.

```dart
// main.dart
import 'sizing.dart';
import 'screen_model.dart';
import 'check_sizing.dart';

void main() {
  final log = measure(buildScreen(300), 400);
  printSizes(log);
  checkSizing(log, '/Screen/Body/Chart', 'chosen');
}
```

```text
forced: /Screen width=400 constraint=400..400
forced: /Screen/Body width=368 constraint=368..368
chosen: /Screen/Body/Title width=120 constraint=0..368
chosen: /Screen/Body/Chart width=300 constraint=0..368
PASS: /Screen/Body/Chart chosen
```

The root receives the tight screen constraint, the padding takes 16 units from each side, and the body sees a tight constraint of 368 units. The column loosens the constraint; the chart's request of 300 units fits the range 0..368. The chart chose its own width, and the test passes.

## A Test That Fails on a 320-Unit Surface

The same screen and the same expectation, with only the test surface at 320 units. A new developer wants to run the same test for a narrower device; nothing changes in the code.

```dart
// main.dart
import 'sizing.dart';
import 'screen_model.dart';
import 'check_sizing.dart';

void main() {
  final log = measure(buildScreen(300), 320);
  printSizes(log);
  checkSizing(log, '/Screen/Body/Chart', 'chosen');
}
```

```text
forced: /Screen width=320 constraint=320..320
forced: /Screen/Body width=288 constraint=288..288
chosen: /Screen/Body/Title width=120 constraint=0..288
clamped: /Screen/Body/Chart width=288 constraint=0..288
FAIL: /Screen/Body/Chart expected=chosen measured=clamped
```

The body now sees a tight constraint of 288 units. The chart requests 300, but the most it is allowed is 288; the resulting width is 288 and the sizing is **clamped**. The expectation was "**chosen**", so the test fails. The failing test has found an error: on the narrow surface, the screen shows its chart smaller than its request. Where the error is can also be read from the output, because the `expected` and `measured` fields stand side by side.

There are two ways forward here, and which one to take is a design decision. The screen can shrink the chart on the narrow surface; if this is the intended behavior, the expectation changes. Or the screen can put the chart into a scrollable area; in this case the tree changes and the test passes again. The test does not say which is right; it only makes the difference between the two cases visible.

## Correcting the Expectation

If being **clamped** on the narrow surface is the intended behavior, the expectation is written accordingly. The block below checks two expectations together. The first correctly expects **clamped**. The second asks for a node that does not exist in the tree.

```dart
// main.dart
import 'sizing.dart';
import 'screen_model.dart';
import 'check_sizing.dart';

void main() {
  final log = measure(buildScreen(300), 320);
  checkSizing(log, '/Screen/Body/Chart', 'clamped');
  checkSizing(log, '/Screen/Body/Legend', 'chosen');
}
```

```text
PASS: /Screen/Body/Chart clamped
FAIL: /Screen/Body/Legend no record
```

The second line shows an important rule: a node that is not in the tree has no record. A silent situation in which a test is written but checks nothing arises from here. A test that misspells the name of a node would end with "no record" instead of "**chosen**"; this is why the check function does not count a missing path as passed but as failed.

## What an Integration Test Adds

The test surface of a widget test is the screen width in the model; it is a number, and the author of the test chooses it. An integration test runs on a real device and uses the device's own screen width, font size, and safe-area inset. The same screen runs on two different devices with two different widths; an integration test sees whichever one it runs on.

This does not mean that an integration test replaces a widget test. A widget test chooses the surface on purpose: two numbers such as 400 and 320 represent two classes. An integration test takes the surface from the device and checks whatever device is at hand. Running the two together checks the tree on a few meaningful surfaces and the whole app on a few real devices.

## Common Misconception

**If the tests pass, the screen is correct under every condition.** This is not so. The two measurements of this lesson show it: a test that passes at 400 units fails at 320 units. The test is the same and the screen is the same; the surface changed. A passing test does not show that the screen is correct under every condition; it shows only that it is correct under the condition the test chose.

This question must not be confused with the one in M12/K04. There, the question of a test was "did this piece run on this request", and the answer was coverage. Here, the question is who determines the width of a widget that ran. Every measured node has entered the layout, that is, it has run; sizing classifies only a node that enters the layout, and having run does not mean having been sized correctly.

## The Bounding Measurement

The last measurement shows what a test cannot see. The test surface is given to the widget tree of the screen as an input; the test cannot change this input, and it cannot know in advance what will happen when the input changes. The block below looks only at the chart's record on the 320-unit surface and then gives the count of the whole record.

```dart
// main.dart
import 'sizing.dart';
import 'screen_model.dart';

void main() {
  final log = measure(buildScreen(300), 320);
  printSizes(log.where((entry) => entry.path == '/Screen/Body/Chart').toList());
  countSizings(log);
}
```

```text
clamped: /Screen/Body/Chart width=288 constraint=0..288
forced: 2
chosen: 1
clamped: 1
stretched: 0
parent: 3
```

The focus node is `/Screen/Body/Chart`. On the 320-unit surface the chart came out **clamped**; in three of the four records, the width is determined not by the widget itself but by the parent or the range. The title is still **chosen**, but the chart can no longer fulfill its request. This lesson's sizing: **clamped**. The narrow test surface is the place where the chart's request does not fit the range.

**QR1.** The model builds this sizing's rule; the model is not the framework itself. In the model, a test finds a path in the measured record list and compares the sizing name. A real widget test queries the size of a widget with the real measurements of the device and is affected by inputs the model does not have, such as the font size. The expectations measured in the model do not match the queries of a real test one to one.

## Summary

- This lesson's sizing: **clamped**. On the narrow test surface, `/Screen/Body/Chart` fell short of its request, and the expected "**chosen**" did not hold for this reason.
- A unit test isolates a function, a widget test builds a tree, and an integration test walks the app from start to finish on a device.
- Sizing is a property of the tree; so a unit test cannot see it, and a widget test sees it by choosing the test surface.
- `checkSizing` compares the expected name with the measured record and gives `PASS` or `FAIL`; a path that is not in the tree is not counted as passed.
- The same screen passes at 400 units and fails at 320 units: the test surface is the hidden input of a widget test.

## Next Step

When a test fails, saying where the error is is one thing; seeing why it happens is another. The next lesson, `Developer Tools`, covers the tools that inspect the tree while it runs, and shows the constraint a widget received, its width, and which widget a piece of output came from. The third lesson of the topic, `Performance Tuning`, explains how to reduce unnecessary changes in the tree.
