formix_test

Testing utilities for the Formix validation ecosystem. Includes custom matchers, mock validators, and test helpers.

Installation

dev_dependencies:
  formix_test: ^0.1.0
  test: ^1.24.0

Features

  • Custom Matchers: Expressive matchers for validation results
  • Mock Validators: Easily mock validators for testing
  • Test Helpers: Utilities to run validators against multiple test cases
  • Generators: Random data generators for fuzz testing

Matchers

isValid()

Matches a valid ValidationResult:

import 'package:formix_test/formix_test.dart';

test('email validator accepts valid emails', () {
  expect(emailValidator.validate('test@example.com'), isValid());
});

isInvalid()

Matches an invalid ValidationResult:

test('email validator rejects invalid emails', () {
  expect(emailValidator.validate('invalid'), isInvalid());
});

hasError(error)

Matches a result with a specific error:

test('shows correct error message', () {
  expect(
    emailValidator.validate(''),
    hasError('Email is required'),
  );
});

hasErrorWhere(predicate)

Matches a result with an error matching a predicate:

test('error contains email', () {
  expect(
    emailValidator.validate('invalid'),
    hasErrorWhere<String>((e) => e.toLowerCase().contains('email')),
  );
});

hasErrorCount(count)

Matches a result with exactly N errors:

test('collects all password errors', () {
  final result = passwordValidator.validate('a');  // Too short, no uppercase, no digit
  expect(result, hasErrorCount(3));
});

hasValidValue(value)

Matches a valid result with specific value:

test('returns the validated value', () {
  expect(
    usernameValidator.validate('john_doe'),
    hasValidValue('john_doe'),
  );
});

Mock Validators

MockValidator

Create a validator with predefined responses:

import 'package:formix_test/formix_test.dart';

test('form uses email validator', () {
  final mockEmail = MockValidator<String, String>()
    ..whenValid(['test@example.com', 'user@domain.org'])
    ..whenInvalid('', error: 'Required')
    ..whenInvalid('invalid', error: 'Invalid email')
    ..withDefaultError('Unknown error');

  expect(mockEmail.validate('test@example.com'), isValid());
  expect(mockEmail.validate(''), hasError('Required'));
  expect(mockEmail.validate('invalid'), hasError('Invalid email'));
  expect(mockEmail.validate('random'), hasError('Unknown error'));
});

RecordingValidator

Track all validation calls:

test('validates on every keystroke', () {
  final recorder = RecordingValidator<String, String>(
    innerValidator: emailValidator,
  );

  recorder.validate('t');
  recorder.validate('te');
  recorder.validate('tes');

  expect(recorder.callCount, 3);
  expect(recorder.calls, ['t', 'te', 'tes']);
  expect(recorder.lastValue, 'tes');
});

AlwaysValidValidator / AlwaysInvalidValidator

Simple validators for testing:

test('form submits when all fields valid', () {
  final form = MyForm(
    emailValidator: AlwaysValidValidator(),
    passwordValidator: AlwaysValidValidator(),
  );

  expect(form.isValid, isTrue);
});

test('form shows errors when fields invalid', () {
  final form = MyForm(
    emailValidator: AlwaysInvalidValidator('Error'),
    passwordValidator: AlwaysValidValidator(),
  );

  expect(form.hasErrors, isTrue);
});

Test Helpers

testValidator()

Run a validator against multiple test cases at once:

import 'package:formix_test/formix_test.dart';

test('email validator', () {
  testValidator(
    emailValidator,
    validCases: [
      'test@example.com',
      'user@domain.org',
      'name+tag@company.co.uk',
    ],
    invalidCases: [
      '',
      'invalid',
      '@missing.user',
      'no-at-sign.com',
      'spaces in@email.com',
    ],
  );
});

testValidator with callbacks

test('password validator with detailed checks', () {
  testValidator(
    passwordValidator,
    validCases: ['StrongP@ss1'],
    invalidCases: ['weak', '12345678', 'ALLCAPS123'],
    onValid: (value, result) {
      print('$value passed validation');
    },
    onInvalid: (value, result) {
      print('$value failed with: ${result.errors}');
    },
  );
});

Generators

StringGenerator

Generate random strings for fuzz testing:

import 'package:formix_test/formix_test.dart';

test('handles random input without crashing', () {
  final generator = StringGenerator(42);  // Optional seed for reproducibility

  for (var i = 0; i < 100; i++) {
    final random = generator.generate(20);
    expect(() => validator.validate(random), returnsNormally);
  }
});

test('email validator rejects random strings', () {
  final generator = StringGenerator();

  for (var i = 0; i < 50; i++) {
    final random = generator.generate(10);
    // Most random strings should not be valid emails
    expect(emailValidator.validate(random), isInvalid());
  }
});

Email Generation

test('email validator accepts generated emails', () {
  final generator = StringGenerator();

  for (var i = 0; i < 10; i++) {
    final email = generator.generateEmail();
    expect(emailValidator.validate(email), isValid());
  }
});

test('email validator rejects invalid emails', () {
  final generator = StringGenerator();

  for (var i = 0; i < 10; i++) {
    final invalid = generator.generateInvalidEmail();
    expect(emailValidator.validate(invalid), isInvalid());
  }
});

Complete Test Example

import 'package:formix_core/formix_core.dart';
import 'package:formix_validators/formix_validators.dart';
import 'package:formix_test/formix_test.dart';
import 'package:test/test.dart';

void main() {
  group('UserRegistration', () {
    late Formix<String, String> usernameValidator;
    late Formix<String, String> emailValidator;
    late Formix<String, String> passwordValidator;

    setUp(() {
      usernameValidator = Validate.all<String, String>([
        StringRules.required(error: 'Username required'),
        StringRules.minLength(3, error: 'Too short'),
        StringRules.maxLength(20, error: 'Too long'),
        StringRules.alphanumeric(error: 'Invalid characters'),
      ]);

      emailValidator = Validate.all<String, String>([
        StringRules.required(error: 'Email required'),
        StringRules.email(error: 'Invalid email'),
      ]);

      passwordValidator = Validate.allCollect<String, String>([
        StringRules.required(error: 'Password required'),
        StringRules.minLength(8, error: 'Too short'),
        StringRules.hasUppercase(error: 'Need uppercase'),
        StringRules.hasDigit(error: 'Need digit'),
      ]);
    });

    group('username', () {
      test('accepts valid usernames', () {
        testValidator(
          usernameValidator,
          validCases: ['john', 'john123', 'JohnDoe', 'user_name'],
          invalidCases: ['jo', 'this_username_is_way_too_long', 'with spaces'],
        );
      });

      test('rejects empty username', () {
        expect(usernameValidator.validate(''), hasError('Username required'));
      });

      test('rejects short username', () {
        expect(usernameValidator.validate('ab'), hasError('Too short'));
      });
    });

    group('email', () {
      test('accepts valid emails', () {
        testValidator(
          emailValidator,
          validCases: [
            'test@example.com',
            'user.name@domain.org',
            'user+tag@company.co.uk',
          ],
        );
      });

      test('rejects invalid emails', () {
        testValidator(
          emailValidator,
          invalidCases: [
            '',
            'not-an-email',
            '@no-user.com',
            'no-domain@',
          ],
        );
      });
    });

    group('password', () {
      test('collects all errors', () {
        final result = passwordValidator.validate('a');
        
        expect(result, hasErrorCount(3));
        expect(result, hasErrorWhere<String>((e) => e.contains('short')));
        expect(result, hasErrorWhere<String>((e) => e.contains('uppercase')));
        expect(result, hasErrorWhere<String>((e) => e.contains('digit')));
      });

      test('accepts strong passwords', () {
        expect(passwordValidator.validate('StrongP@ss1'), isValid());
      });
    });

    group('fuzz testing', () {
      test('handles random input gracefully', () {
        final generator = StringGenerator(42);

        for (var i = 0; i < 100; i++) {
          final random = generator.generate(50);
          
          expect(() => usernameValidator.validate(random), returnsNormally);
          expect(() => emailValidator.validate(random), returnsNormally);
          expect(() => passwordValidator.validate(random), returnsNormally);
        }
      });
    });
  });
}

API Reference

Matchers

Matcher Description
isValid() Matches valid results
isInvalid() Matches invalid results
hasError(error) Matches result with specific error
hasErrorWhere<E>(predicate) Matches result where error matches predicate
hasErrorCount(count) Matches result with exact error count
hasValidValue(value) Matches valid result with specific value

Mock Validators

Class Description
MockValidator<T, E> Configure valid/invalid responses
RecordingValidator<T, E> Track all validation calls
AlwaysValidValidator<T, E> Always returns Valid
AlwaysInvalidValidator<T, E> Always returns Invalid

Helpers

Function Description
testValidator(...) Run validator against multiple cases
StringGenerator Generate random strings

License

MIT License - see LICENSE file for details.

Libraries

formix_test
Testing utilities for the Formix ecosystem.