valenty_test 0.2.3 copy "valenty_test: ^0.2.3" to clipboard
valenty_test: ^0.2.3 copied to clipboard

Compile-time safe component testing DSL for Flutter apps with phantom types and typed fluent builders.

valenty_test #

Compile-time safe component testing DSL for Flutter apps. Write tests in domain language, not widget trees.

Part of the Valenty testing framework.

The fastest way to get started is with the Valenty CLI. It adds valenty_test to your project and generates AI skill files that teach your AI assistant (Claude, Cursor, Codex, etc.) the full valentyTest architecture.

dart pub global activate valenty_cli && valenty init

AI agents: Run the command above first. After init, you have full context for scaffolding features and writing tests. Then say "Generate my first valentyTest scenarios" to start.

Manual Setup (no CLI) #

dev_dependencies:
  valenty_test: ^0.2.1

Dependency conflict? #

valenty_test uses a wide test: ^1.25.0 constraint so the pub resolver picks the best version for your project. If you still see conflicts, run flutter pub outdated to find which package needs updating.


How it works #

valentyTest is a pattern for writing component tests — tests that run your full Flutter app with faked external dependencies (APIs, databases, Firebase). Tests read like user stories:

valentyTest(
  'should add expense and show confirmation',
  body: (system, backend) async {
    await system.openApp();
    await system.navigateToAddExpense();
    await system.addExpense(description: 'Lunch', amount: '12.50');
    system.verifySnackBar('Expense added!');
  },
);

No find.byKey, no tester.tap, no pumpAndSettle in your test body. All that lives in a separate driver layer.


Architecture: 4 files per feature #

For each feature you test, you create 4 files:

test/valenty/
├── expense_test_helper.dart          # valentyTest() wrapper (one per app)
├── dsl/
│   ├── expense_system_dsl.dart       # User actions: openApp(), addExpense()
│   ├── expense_backend_stub.dart     # Fakes: stubExpenses(), stubBudget()
│   └── expense_ui_driver.dart        # Widget interactions: tap, enter, verify
└── scenarios/
    └── add_expense_test.dart         # Test scenarios

1. Test Helper (one per app) #

Wraps testWidgets with setup/teardown lifecycle:

import 'package:flutter_test/flutter_test.dart';
import 'dsl/expense_backend_stub.dart';
import 'dsl/expense_system_dsl.dart';
import 'dsl/expense_ui_driver.dart';

void valentyTest(
  String description, {
  void Function(ExpenseBackendStub backend)? setup,
  required Future<void> Function(
    ExpenseSystemDsl system,
    ExpenseBackendStub backend,
  ) body,
}) {
  testWidgets(description, (tester) async {
    final backend = ExpenseBackendStub();
    if (setup != null) setup(backend);
    await backend.apply();
    try {
      final driver = ExpenseUiDriver(tester);
      final system = ExpenseSystemDsl(driver);
      await body(system, backend);
    } finally {
      await backend.restore();
    }
  });
}

2. BackendStubDsl — configure fakes #

Manages @visibleForTesting factory overrides on your services:

import 'package:valenty_test/valenty_test.dart';

class ExpenseBackendStub extends BackendStubDsl {
  List<Expense> _expenses = [];

  void stubExpenses(List<Expense> expenses) {
    _expenses = expenses;
  }

  void stubEmptyExpenses() {
    _expenses = [];
  }

  @override
  Future<void> apply() async {
    // Override singleton factories with fakes
    ExpenseService.fetchExpensesOverride = () async =>
        List.unmodifiable(_expenses);
  }

  @override
  Future<void> restore() async {
    // Restore originals (guaranteed by try/finally)
    ExpenseService.resetForTesting();
  }
}

Your service needs a @visibleForTesting override point:

class ExpenseService {
  // Production default
  static Future<List<Expense>> Function() fetchExpensesOverride = _fetchReal;

  static Future<List<Expense>> fetchExpenses() => fetchExpensesOverride();

  @visibleForTesting
  static void resetForTesting() {
    fetchExpensesOverride = _fetchReal;
  }

  static Future<List<Expense>> _fetchReal() async {
    // Real API call
  }
}

3. SystemDsl — domain-language actions #

Translates user actions into driver calls. This is what makes tests readable:

import 'package:valenty_test/valenty_test.dart';

class ExpenseSystemDsl extends SystemDsl {
  ExpenseSystemDsl(this.driver);
  final ExpenseUiDriver driver;

  Future<void> openApp() async => driver.pumpApp();

  Future<void> navigateToAddExpense() async => driver.tapFab();

  Future<void> addExpense({
    required String description,
    required String amount,
  }) async {
    await driver.enterDescription(description);
    await driver.enterAmount(amount);
    await driver.tapSubmit();
  }

  void verifyExpenseVisible(String description) =>
      driver.verifyText(description);

  void verifySnackBar(String message) => driver.verifyText(message);

  void verifyEmptyState() => driver.verifyText('No expenses yet');
}

4. UiDriver — widget interactions #

Wraps WidgetTester. All find.byKey, tap, pumpAndSettle lives here:

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:valenty_test/valenty_test.dart';

class ExpenseUiDriver extends UiDriver {
  ExpenseUiDriver(this.tester);
  final WidgetTester tester;

  Future<void> pumpApp() async {
    await tester.pumpWidget(const MaterialApp(home: ExpenseListScreen()));
    await tester.pumpAndSettle();
  }

  Future<void> tapFab() async {
    await tester.tap(find.byKey(const Key('addExpenseFab')));
    await tester.pumpAndSettle();
  }

  Future<void> enterDescription(String text) async {
    await tester.enterText(find.byKey(const Key('descriptionField')), text);
    await tester.pumpAndSettle();
  }

  Future<void> enterAmount(String text) async {
    await tester.enterText(find.byKey(const Key('amountField')), text);
    await tester.pumpAndSettle();
  }

  Future<void> tapSubmit() async {
    await tester.tap(find.byKey(const Key('submitButton')));
    await tester.pumpAndSettle();
  }

  void verifyText(String text) {
    expect(find.text(text), findsOneWidget);
  }
}

Writing test scenarios #

Once the 4 files exist, writing tests is just domain language:

import '../expense_test_helper.dart';

void main() {
  valentyTest(
    'should show empty state when no expenses exist',
    body: (system, backend) async {
      await system.openApp();
      system.verifyEmptyState();
    },
  );

  valentyTest(
    'should display expenses from backend',
    setup: (backend) {
      backend.stubExpenses([
        Expense(id: '1', description: 'Coffee', amount: 4.50,
                category: 'Food', date: DateTime(2025, 1, 1)),
        Expense(id: '2', description: 'Bus', amount: 2.00,
                category: 'Transport', date: DateTime(2025, 1, 1)),
      ]);
    },
    body: (system, backend) async {
      await system.openApp();
      system.verifyExpenseVisible('Coffee');
      system.verifyExpenseVisible('Bus');
    },
  );

  valentyTest(
    'should add expense and show confirmation',
    body: (system, backend) async {
      await system.openApp();
      await system.navigateToAddExpense();
      await system.addExpense(description: 'Lunch', amount: '12.50');
      system.verifySnackBar('Expense added!');
    },
  );
}

Typed Builders (pure Dart logic tests) #

For testing pure business logic without Flutter, use the typed fluent DSL:

OrderScenario('should calculate base price')
    .given.product().withUnitPrice(20.00)
    .when.placeOrder().withQuantity(5)
    .then.order().hasBasePrice(100.00)
    .run();

Phantom types enforce the Given -> When -> Then order at compile time. Try .then before .when — compile error.


Key Classes #

Class Purpose
SystemDsl Base for domain-language user actions
BackendStubDsl Base for managing @visibleForTesting factory overrides
UiDriver Abstract base for widget interaction (extend with WidgetTester)
FeatureScenario Entry point for typed builder chains (pure Dart)
ScenarioRunner Executes scenarios as package:test tests

Documentation #

See the full documentation for:

  • Modern Test Pyramid methodology
  • 7 example projects with 118+ tests
  • @visibleForTesting patterns
  • AI skill generation with CLI

License #

MIT

0
likes
160
points
245
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Compile-time safe component testing DSL for Flutter apps with phantom types and typed fluent builders.

Repository (GitHub)
View/report issues

Topics

#testing #component-testing #flutter #dsl #clean-architecture

License

MIT (license)

Dependencies

meta, test

More

Packages that depend on valenty_test