Course outline

Flutter / Asynchronous application boundaries / Lesson 12

Unit, widget, and integration evidence

Choose tests that expose ownership and timing contracts, and keep their claims within the exercised boundary.

About 5 min readContent reviewed
On this page

Lesson goals

  • Match the test to the uncertainty
  • Use explicit input and expected value
  • Control frames around interaction

Match the test to the uncertainty

A unit test exercises a function or model without rendering a screen. A widget test builds a tree in Flutter’s test environment and drives controlled interactions. An integration test exercises a larger application path on a target host. Each provides different evidence and cost. A passing decoder test cannot prove that an error is announced accessibly, and a screenshot cannot prove request ordering. Identify the behavior that could fail, then choose the smallest test that exposes it reliably. Reserve broader tests for contracts that actually cross the broader boundary.

Use explicit input and expected value

The first fragment tests a pure parser at two meaningful boundaries. It accepts an ordinary nonnegative integer and rejects negative input. The test checks behavior rather than reproducing the implementation’s branches with a second parser. Add cases because they represent distinct contracts, such as blank input, not merely to increase the number of assertions. The package:test host and parser import are required; this is not a standalone Dart program. The authoring environment does not execute it, so the expected pass is an instructional prediction rather than a test-run claim.

dart Illustrative Dart or Flutter fragment; requires the declared SDK host.
import 'package:test/test.dart';
int? parseSeats(String text) {
  final value = int.tryParse(text.trim());
  return value != null && value >= 0 ? value : null;
}
void main() {
  test('seat count preserves valid values and rejects negatives', () {
    expect(parseSeats(' 4 '), 4);
    expect(parseSeats('-1'), isNull);
  });
}
Expected output
Expected: the declared test passes for both assertions.

The range rule is visible in the expected outcomes. This test does not verify a form, keyboard, or server rule.

Control frames around interaction

A widget test usually pumps a host, finds a meaningful control, performs an action, and pumps the next frame before inspecting the resulting tree. pump advances a controlled frame; pumpAndSettle repeatedly pumps until scheduled frames settle and can be inappropriate for an endless animation or unresolved operation. Avoid relying on arbitrary real-time delays. If an interaction starts a fake asynchronous operation, resolve that fake intentionally and advance only the work needed for the assertion. Clear frame coordination makes a timing failure understandable rather than intermittent.

Assert the visible contract

The second fragment tests the earlier DetailsPanel under a MaterialApp host. It verifies the initial affordance, activates it, then checks both the new label and the disclosed content. This catches a state change that fails to update presentation, unlike calling a private toggle method directly. Text finders are suitable for these fixed synthetic labels; localized applications may use semantic labels or stable keys where appropriate. A key should identify behaviorally meaningful controls, not expose every implementation wrapper and make harmless refactoring break tests.

dart Illustrative Dart or Flutter fragment; requires the declared SDK host.
testWidgets('details are disclosed after activation', (tester) async {
  await tester.pumpWidget(const MaterialApp(home: Scaffold(body: DetailsPanel())));
  expect(find.text('Show'), findsOneWidget);
  expect(find.text('Local details'), findsNothing);
  await tester.tap(find.text('Show'));
  await tester.pump();
  expect(find.text('Hide'), findsOneWidget);
  expect(find.text('Local details'), findsOneWidget);
});
Expected output
Expected: hidden details become visible after one activation and frame.

Requires flutter_test, Material imports, and the DetailsPanel implementation. No widget test was executed in this authoring environment.

Use fakes at boundaries, not everywhere

A fake repository can return known values, fail predictably, or defer completion to expose a race. Keep its behavior simple enough that it does not become an untested second application. Avoid mocking every private method because that couples tests to structure instead of outcomes. For a platform plugin, a host-side fake can verify application decisions, while real permission dialogs, filesystem semantics, or native callbacks still require target integration evidence. State this limitation when reporting results so a fast isolated test is not presented as device certification.

Keep failures reproducible and reports precise

Record the SDK, package lockfile, test command, target host, and relevant fixture when a failure matters. A golden image test can detect visual drift but may depend on fonts, pixel ratio, renderer, and platform configuration. Review those inputs before updating expected images, and inspect whether the difference is a real regression. Keep private data out of fixtures. Report unexecuted tests as proposed checks, not successful runs. A useful validation summary says exactly which boundary passed and which platform or accessibility checks remain outstanding.

Common errors and fixes

A widget assertion reads the old tree after tap

Why it happens
The test did not advance the scheduled render frame.
How to fix it
Pump the appropriate frame after interaction.
Prevent it next time
Make event, asynchronous completion, and frame steps explicit.

pumpAndSettle never completes

Why it happens
An animation or pending condition keeps scheduling frames.
How to fix it
Control the relevant time or completion and pump deliberately.
Prevent it next time
Avoid settling as a universal substitute for timing analysis.

A fake-plugin test is reported as a device test

Why it happens
Application logic evidence was generalized to native integration.
How to fix it
Label the tested host and retain target integration checks.
Prevent it next time
Separate test scope from desired confidence.

Check your understanding

1. What is the smallest meaningful test for an older search completing last?

Show worked solution

A model test with deferred fake responses can establish request identity without rendering. Complete the newer response first and the older response last, then assert the model retains newer data. Add a widget test only for how that model state is presented or announced.

2. Should a changed golden image always be accepted after a dependency update?

Show worked solution

No. Inspect the difference and reproduce the expected rendering inputs first. An update may intentionally change appearance or reveal a regression. Document the reason for accepting a new baseline and run behavior checks that an image alone cannot establish.

Key takeaways

  • Match the test to the uncertainty
  • Keep failures reproducible and reports precise

Official references