activatory 1.2.1

  • Readme
  • Changelog
  • Example
  • Installing
  • 51

Activatory #

Build Status Coverage Status Pub

Test data generator for Dart ecosystem. Simplifies unit testing and Test-Driven Development.

This project is inspired by .NET Autofixture library.

Overview #

While writing tests developers need some way to create random data. It starts from using Random class to receive some int/double/bool and continues to handwritten helper methods with a big amount of optional parameters to create complex object graphs. Helpers become more complex and require more maintenance while time passing. This makes them not flexible, but inferrable and annoying.

Activatory allows you to use ready-from-the-box class which can do the same as handwritten boilerplate code and don't waste your time on writing them.

Tests with activatory are:

  • more readable because requires less boilerplate code;
  • more sustainable to changes because all required objects in test data graph are created and wired automatically;
  • more maintainable because all relevant logic is placed in test. No more complex helpers common for all tests;
  • more flexible because activatory contains features that are hard to implement in handwritten helpers.

For more information please see "Why activatory does matter" section below.

Basic usage #

To create activatory instance just call a constructor

import 'package:activatory/activatory.dart';
final activatory = new Activatory();

After it you can create almost every type of objects using activatory instance

final int randomInt = activatory.get<int>();
final AlmostAnyClass instance = activatory.get<AlmostAnyClass>();

Supported types #

Activatory can be used to create:

  • random values of primitive types (String, int, double, bool, DateTime, enums, Null);
  • random values of complex types using all Dart constructor types;
  • List, Set, Iterable and Map;
  • random values of recursive types (trees, linked lists and etc);
  • random values of generic classes;
  • any not listed above type with a bit setup code.

Any constructor arguments and public fields/setter values will be created, filled with random data and passed automatically.

Typed VS untyped API #

Activatory support both styles of passing type parameters:

  • with a generic class argument,
  • with a regular Type object method parameter. In general, typed API is preferable because of generic code robustness and readability.

Suppose to use

final AlmostAnyClass instance = activatory.get<AlmostAnyClass>();

instead of

final AlmostAnyClass instance = activatory.getUntyped(AlmostAnyClass) as AlmostAnyClass;

Untyped API can be helpful in case when class type itself is resolved at runtime (for instance, it could be taken from array of types in foreach loop).

Customization #

Activation behaviors can be customized in different ways.

Replacing underlying factories per type #

With user defined function

One of the following situations can occur:

  • creating of instance has some logic behind (e.g. filling with special values),
  • type is not supported by activatory directly right now.

In this situation default factories can be replaced with explicit user defined function call by useFunction method.

activatory.useFunction((ActivationContext ctx) => 42); // Imagine some logic behind 42 receiving, e.g. custom code.
final int goodNumber = activatory.get<int>(); // 42

The ActivationContext is object that allows you to create any type instance inside callback.

activatory
  ..useFunction((ActivationContext ctx) => 42);
  ..useFunction((ActivationContext ctx) => ctx.get<int>().toString());
final int goodNumber = activatory.get<String>(); // "42"

With user defined value

If test scenario requires multiple instances of one class with the same value useSingleton method can be used to pin single value for all activations of given type.

activatory.useSingleton(42);
final int fourtyTwo1 = activatory.get<int>(); // 42
final int fourtyTwo2 = activatory.get<int>(); // 42

This can be helpful if you test object graph contains repeating data, e.g. customer name, object ids, contacts and so on.

Suppose we are testing some single user scenario:

class ContactInfo { int id; String name;}
class ReportItem { ContactInfo contact; int amount; String itemName;}
...
final contact = new ContactInfo()
  ..id = 10 // or activatory.get<int>()
  ..name = 'Joe'; // or activatory.get<String>()
activatory.useSingleton(contact);
final List<ReportItem> report = activatory.getMany<ReportItem>();
final contactsCount = report.map((r) => r.contact).toSet().length; // 1
final reportItemsCount = report.map((r) => r.itemName).toSet().length; // 3

In this example activatory will create 3 instances of ReportItem each of which will receive different values for itemName. But all 3 instances will share one ContactInfo instance.

With user defined values collection

If test scenario requires to pass one of predefined values as activation result useOneOf method can be used. Also, useOneOf is great to use with types, that have predefined list of instances, e.g. enum-like classes.

class Gender{
  final String value;
  const Gender._(this.value);
  static const Gender male = Gender._('M');
  static const Gender female = Gender._('W');
  static const Gender other = Gender._('Unknown');
  // Other members are skipped for brief
  static const List<Gender> values => [male, female, other];
}
...
activatory.useOneOf(Gender.values);
final gender = activatory.get<Gender>();
gender.value; // M, W or Unknown

With activatory created random value

If a test scenario requires the same value for all type instances but there is no need to create it by hand useGeneratedSingleton method can be used.

Let's rewrite sample below. In this code snipped we don't depend on contact id and name values, so them could be created without user defined code.

activatory.useGeneratedSingleton<ContactInfo>();
final List<ReportItem> report = activatory.getMany<ReportItem>();
final contactsCount = report.map((r) => r.contact).toSet().length; // 1
final reportItemsCount = report.map((r) => r.itemName).toSet().length; // 3

Existing configuration will be used while creating random value. Value is created during useGeneratedSingleton call, so any following configuration will not affect it.

Changing factory resolving strategy #

By default activatory takes first defined factory for class. This means first founded by reflection public constructor will be used if no override was provided. If any override was provided latest one will be used.

activatory.customize<MyClass>().resolvingStrategy = FactoryResolvingStrategy.TakeRandomNamedCtor;

Available strategies are:

  • TakeFirstDefined (default one),
  • TakeRandomNamedCtor,
  • TakeRandom,
  • TakeDefaultCtor.

In this section SomeClassWithConstructors class will be used as an example. Here is a definition.

class SomeClassWithConstructors {
  final String value;

  SomeClassWithConstructors.named1() : this('named1');

  SomeClassWithConstructors(this.value);

  SomeClassWithConstructors.named2() : this('named2');
}

Using TakeFirstDefined strategy

TakeFirstDefined is the default strategy. This strategy will take the first available factory. If no overrides were provided will be used one of next:

  • random value factory for primitive types;
  • random value factory enums;
  • fist defined constructor for complex type.

Example:

_activatory.customize<SomeClassWithConstructors>().resolvingStrategy = FactoryResolvingStrategy.TakeFirstDefined; // This line actually can be skipped
final firstDefinedCtorCallCount = _activatory.getMany<SomeClassWithConstructors>(count: 100).where((x) => x.value == 'named1').length; // 100

In this example firstDefinedCtorCallCount will be equals 100 because first defined constructor (named as named1) is used. This could looks strange, but it's common pattern to place more generic constructor first. Dart ecosystem even have a linter rule for constructor order validation. So usually first defined is more applicable to simulate random input.

If you override factory with any other latest one override will be used:

_activatory.useFunction((ctx) => new SomeClassWithConstructors('hello'));
final latestOverrideCallCount = _activatory.getMany<SomeClassWithConstructors>(count: 100).where((x) => x.value == 'hello').length; // 100

This behavior is used to match expected behavior on override call. So latestOverrideCallCount will be equals 100.

Using TakeRandomNamedCtor strategy

This strategy takes random named ctor for complex type. If type doesn't have public named constructor or type is not complex ActivationException will be thrown.

_activatory.customize<SomeClassWithConstructors>().resolvingStrategy = FactoryResolvingStrategy.TakeRandomNamedCtor;

final items = _activatory.getMany<SomeClassWithConstructors>(count: 100);
final firstNamedCtorCallsCount = items.where((x) => x.value == 'named1').length; // ~50
final secondNamedCtorCallsCount = items.where((x) => x.value == 'named2').length; // ~50
final totalCtorCallsCount = firstNamedCtorCallsCount + secondNamedCtorCallsCount; // 100

If you override factory with any other latest one override will be used:

_activatory.useFunction((ctx) => new SomeClassWithConstructors('hello'));
final overrideUsedCount = _activatory.getMany<SomeClassWithConstructors>(count: 100).where((x) => x.value == 'hello').length; // 100

Using TakeRandom strategy

This strategy will take a random available factory.

If no overrides are provided will be used:

  • random value factory for primitive types;
  • random value factory enums;
  • random ctor for complex type.
_activatory.customize<SomeClassWithConstructors>().resolvingStrategy = FactoryResolvingStrategy.TakeRandom;

final items = _activatory.getMany<SomeClassWithConstructors>(count: 100);
final firstNamedCtorCallsCount = items.where((x) => x.value == 'named1').length; // ~33
final secondNamedCtorCallsCount = items.where((x) => x.value == 'named2').length; // ~33
final defaultCtorCallsCount = items.where((x) => !x.value.startsWith('named')).length; // ~33
final totalCtorCallCount = firstNamedCtorCallsCount + secondNamedCtorCallsCount + defaultCtorCallsCount; // 100

If overrides were provided random one will be chosen from overrides.

_activatory.useFunction((ctx) => new SomeClassWithConstructors('hello'));
final items2 = _activatory.getMany<SomeClassWithConstructors>(count: 100);
final overrideUsedCount = items2.where((x) => x.value == 'hello').length; // ~25
final firstNamedCtorCallsCount2 = items2.where((x) => x.value == 'named1').length; // ~25
final secondNamedCtorCallsCount2 = items2.where((x) => x.value == 'named2').length; // ~25
final defaultCtorCallsCount2 = items2.where((x) => x.value != 'hello' && !x.value.startsWith('named')).length; // ~25
final totalOverrideCallsCount = overrideUsedCount + firstNamedCtorCallsCount2 + secondNamedCtorCallsCount2 + defaultCtorCallsCount2; // 100

Using TakeDefaultCtor strategy

Take default ctor for complex type. Default ctor is the one called during evaluating new T() expression. This can be factory, const or usual ctor. If type doesn't have public default ctor or type is not complex [ActivationException] will be thrown.

_activatory.customize<SomeClassWithConstructors>().resolvingStrategy = FactoryResolvingStrategy.TakeDefaultCtor;

final items = _activatory.getMany<SomeClassWithConstructors>(count: 100);
final defaultCtorCallsCount = items.where((x) => !x.value.startsWith('named')).length; // 100

If overrides were provided they will be ignored.

_activatory.useFunction((ctx) => new SomeClassWithConstructors('hello'));
final overrideUsedCount = _activatory.getMany<SomeClassWithConstructors>(count: 100).where((x) => x.value == 'hello').length; // 0

Changing default values usage #

activatory.customize<MyClass>().defaultValuesHandlingStrategy = DefaultValuesHandlingStrategy.ReplaceAll;

Available strategies are:

  • ReplaceNulls. Default values will be used while they not equals null.
  • ReplaceAll. Value will be created and passed regardless default value.
  • UseAll. Default value will be used regardless it value.

Changing public fields and setters filling #

activatory.customize<MyClass>().fieldsAutoFillingStrategy = FieldsAutoFillingStrategy.FieldsAndSetters;

Available strategies are:

  • Fields (default). Public fields will be filled with random data;
  • None. Public fields and setters will be not filled;
  • FieldsAndSetters. Public fields and setters will be filled with random data.

Changing Array size #

The default used array size is 3.

final length1 = activatory.getMany<MyClass>().length; // 3
final length2 = activatory.get<List<MyClass>>().length; // 3
activatory.customize<MyClass>().arraySize = 100500;
final length3 = activatory.getMany<MyClass>().length; // 100500
final length4 = activatory.get<List<MyClass>>().length; // 100500

Please note that any activation of multiple instances of MyClass will respect this configuration.

Replacing subclass with superclass #

Suppose we need have a class that expect abstract class as constructor parameter.

abstract class User {}
class ReportItem { User user;}

In this case we can create subclass of User and register it as substitution in activatory.

class VipUser extends User {}
...
activatory.replaceSupperClass<User, VipUser>();
final userType = activatory.get<ReportItem>().user.runtimeType; // VipUser

Customization levels #

Customization can be defined on multiple levels:

  • on global level for all types, using activatory.defaultTypeCustomization;
  • on type level, using activatory.customize<T>();
  • on specific scope level by passing key argument (which can be an instance of any type) by calling activatory.customize<T>(key: 'some key'). Key should be the same for all setup and activation calls to share setup.

Factories can't be overridden on a global level, only type and scope levels are supported.

Helpers #

Activatory allows to simplify test code by providing useful helpers.

  • selecting one or more random items from array
final fromOneToFive = activatory.take([1, 2, 3, 4, 5]);
final threeItemsFromOneToFive = activatory.takeMany(3, [1, 2, 4, 5]);
  • creating list of Objects of given type
final List<int> intArray = activatory.getMany<int>();

Samples #

For up-to-date list of samples see example folder on project GitHub or example section on pub package page.

Supported platforms #

Current implementation depends on reflection (Mirrors package). So, Activatory supports only VM platform for now.

If you have a package depending on other platforms (e.g. Flutter or Dart Web) you should move testing required business logic to separate platform-independent package to be able to use Activatory in tests. Platform independent package of Activatory will be implemented in the future, but it will require much more setup from a user (due to Mirrors unavailability). Also, keep in mind, that moving business logic to a separate package is almost always a good decision.

Further improvements #

For planned features and more see enhancements on github.

Known limitations #

  • Current version supports only VM platform. For more details see "Supported platforms" section above.
  • Private classes (e.g. class with name prefixed by underscore) can't be activated without explicit setup due to private constructor unavailability for external package.

Why activatory does matter #

Suppose we are writing unit test for following sample classes

class UserDto {
  String name;
  int id;
  bool isActive;
  DateTime birthDate;
}

class UserId {
  final int value;

  UserId(this.value);
}

class UserViewModel {
  final String name;
  final UserId id;
  final DateTime birthDate;

  UserViewModel(this.name, this.id, this.birthDate);
}

abstract class UsersAPI {
  Future<List<UserDto>> getAll();
}

class UsersManager {
  final UsersAPI _api;

  UsersManager(this._api);

  Future<List<UserViewModel>> getActiveUsers() async {
    final allItems = await _api.getAll();
    return allItems.where((x) => x.isActive).map(_convert).toList(growable: false);
  }

  UserViewModel _convert(UserDto x) => new UserViewModel(x.name, new UserId(x.id), x.birthDate);

  Future<UserViewModel> getById(UserId id) async {
    final allItems = await _api.getAll();
    final userDto = allItems.firstWhere((x) => x.id == id.value);
    return _convert(userDto);
  }
}

class _UsersAPIMock extends Mock implements UsersAPI {}

bool _isViewModelMatchUserDto(UserViewModel x, UserDto user) =>
    x.id.value == user.id && x.name == user.name && x.birthDate == user.birthDate;

UserAPI class implementation is skipped for brevity. Its contract includes one method returning all available UserDto's. UserManager is responsible for providing UserViewModel to views.

_UsersAPIMock is mock class created with mockito library.

_isViewModelMatchUserDto is a helper for asserts, defined to brief samples.

Lets now write unit tests for UserManager: one forgetById method, another one for getAll method.

Attempt 1: using Random class inside test #

group('Attempt #1: using Random class inside test', () {
    Random _random;
    _UsersAPIMock _apiMock;
    UsersManager _manager;
    setUp(() {
      _random = new Random(DateTime.now().millisecondsSinceEpoch);
      _apiMock = new _UsersAPIMock();
      _manager = new UsersManager(_apiMock);
    });

    test('can find single user by id', () async {
      // arrange
      final userDtoItems = List.generate(
        10,
        (i) => new UserDto()
          ..id = i
          ..isActive = _random.nextBool()
          ..birthDate = new DateTime(
            _random.nextInt(100) + 1900, //1900-2000
            _random.nextInt(12),
            _random.nextInt(29), // minimal count of days in month - 28
            _random.nextInt(24),
            _random.nextInt(60),
            _random.nextInt(60),
          )
          ..name = 'username $i',
      );
      final user = userDtoItems[_random.nextInt(10)];
      final userId = new UserId(user.id);
      when(_apiMock.getAll()).thenAnswer((_) => Future.value(userDtoItems));
      // act
      final result = await _manager.getById(userId);
      // assert
      expect(result, predicate<UserViewModel>((x) => _isViewModelMatchUserDto(x, user)));
    });

    test('can find all active users', () async {
      // arrange
      final userDtoItems = List.generate(
        10,
        (i) => new UserDto()
          ..id = i
          ..isActive = false
          ..birthDate = new DateTime(
            _random.nextInt(100) + 1900, //1900-2000
            _random.nextInt(12),
            _random.nextInt(29), // minimal count of days in month - 28
            _random.nextInt(24),
            _random.nextInt(60),
            _random.nextInt(60),
          )
          ..name = 'username $i',
      );
      final user = userDtoItems[_random.nextInt(10)];
      user.isActive = true;
      when(_apiMock.getAll()).thenAnswer((_) => Future.value(userDtoItems));
      // act
      final result = await _manager.getActiveUsers();
      // assert
      expect(result, hasLength(1));
      expect(result.first, predicate<UserViewModel>((x) => _isViewModelMatchUserDto(x, user)));
    });
  });

Obviously, there are a lot of duplication in this sample. Most of developers will try to extract useful helpers to reduce duplication.

Attempt 2: using handwritten helpers #

 group('Attempt #2: using handwriten helpers', () {
    Random _random;
    _UsersAPIMock _apiMock;
    UsersManager _manager;

    setUp(() {
      _random = new Random(DateTime.now().millisecondsSinceEpoch);
      _apiMock = new _UsersAPIMock();
      _manager = new UsersManager(_apiMock);
    });

    UserDto _createRandomUserDto(int id, {bool isActive = false}) {
      return new UserDto()
        ..id = id
        ..isActive = isActive ?? _random.nextBool()
        ..birthDate = new DateTime(
          _random.nextInt(100) + 1900, //1900-2000
          _random.nextInt(12),
          _random.nextInt(29), // minimal count of days in month - 28
          _random.nextInt(24),
          _random.nextInt(60),
          _random.nextInt(60),
        )
        ..name = 'username $id';
    }

    test('can find single user by id', () async {
      // arrange
      final userDtoItems = List.generate(10, _createRandomUserDto);
      final user = userDtoItems[_random.nextInt(10)];
      final userId = new UserId(user.id);
      when(_apiMock.getAll()).thenAnswer((_) => Future.value(userDtoItems));
      // act
      final result = await _manager.getById(userId);
      // assert
      expect(result, predicate<UserViewModel>((x) => _isViewModelMatchUserDto(x, user)));
    });

    test('can find all active users', () async {
      // arrange
      final userDtoItems = List.generate(10, (i) => _createRandomUserDto(i, isActive: false));
      final user = userDtoItems[_random.nextInt(10)];
      user.isActive = true;
      when(_apiMock.getAll()).thenAnswer((_) => Future.value(userDtoItems));
      // act
      final result = await _manager.getActiveUsers();
      // assert
      expect(result, hasLength(1));
      expect(result.first, predicate<UserViewModel>((x) => _isViewModelMatchUserDto(x, user)));
    });
  });

We move UserDto instance creation logic to _createRandomUserDto and arrange section is read much easier. This is good but not enough. Lets rewrite this test using Activatory.

Attempt #3: using Activatory #

With Activatory we can not waste our time and use out-of-the-box helpers to create random instances of objects.

group('Attempt #3: using Activatory', () {
    Activatory _activatory;
    _UsersAPIMock _apiMock;
    UsersManager _manager;
    setUp(() {
      _activatory = new Activatory();
      _apiMock = new _UsersAPIMock();
      _manager = new UsersManager(_apiMock);
    });

    test('can find single item by id', () async {
      // arrange
      final userDtoItems = _activatory.getMany<UserDto>(count: 10);
      final user = _activatory.take(userDtoItems);
      final userId = new UserId(user.id);
      when(_apiMock.getAll()).thenAnswer((_) => Future.value(userDtoItems));
      // act
      final result = await _manager.getById(userId);
      // assert
      expect(result, predicate<UserViewModel>((x) => _isViewModelMatchUserDto(x, user)));
    });

    test('can find all active users', () async {
      // arrange
      final userDtoItems = _activatory.getMany<UserDto>(count: 10);
      userDtoItems.forEach((x) => x.isActive = false);
      final user = _activatory.take(userDtoItems);
      user.isActive = true;
      when(_apiMock.getAll()).thenAnswer((_) => Future.value(userDtoItems));
      // act
      final result = await _manager.getActiveUsers();
      // assert
      expect(result, hasLength(1));
      expect(result.first, predicate<UserViewModel>((x) => _isViewModelMatchUserDto(x, user)));
    });
  });

No handwritten helpers are required. There is no need to write code describing how to create random instance of UserDto.

With activatory test code becomes more readable, brief and maintainable.

But it is important to mention fact that tests are most useful when we are extending existing functionality.

Attempt 4: Extending UserDto with UserSettingsDto #

Lets extends our UserDto with extra data field UserSettingsDto:

class UserDto {
  // other fields skipped for brief
  UserContactsDto userContacts;
}

class UserContactsDto {
  String email;
  bool notificationsEnabled;
}

class UserViewModel {
  // other fields skipped for brief
  final String email;

  UserViewModel(this.name, this.id, this.birthDate, this.email);
}

class UsersManager {
  // other members skipped for brief
  UserViewModel _convert(UserDto x) => new UserViewModel(x.name, new UserId(x.id), x.birthDate, x.userContacts.email);
}

If we run tests now we will found that tests written without Activatory are failing with cryptic errors, e.g. NoSuchMethodError: The getter 'email' was called on null.

That's because we forgot to update our tests! In "Attemp 2" sample we need to update _createRandomUserDto method implementation to create randomly filled UserSettingsDto and pass it to UserDto. So most of UserDto extending will require the helper to be updated!

Activatory helps you to make your tests more reliable and more sustainable to changes.

Adding new data to models is just half of a story. Let's add complexity - another consumer of UsersAPI.

Attempt 5: Adding more consumers to UsersAPI #

Let's add MailingRecipientsManager class responsible for providing an email list for mailing.

class MailingRecipientsManager {
  final UsersAPI _api;

  MailingRecipientsManager(this._api);

  Future<List<String>> getRecipientsList() async {
    final items = await _api.getAll();
    return items.map((x) => x.userContacts.email).toList(growable: false);
  }
}

If we write tests for this class we will hit the following problems with _createRandomUserDto helper:

  • method is private for file,
  • helper didn't allow to control creation of UserSettingsDto.

The first problem can be resolved by moving the helper method to a separate file named user_dto_helper.dart. The second one can be resolved by passing UserSettingsDto or email as a parameter to the helper. Both fixes will make our test code more and more unmaintainable. The helper will grow to match all corner cases. So in the future, the helper will become troublemaker - it will be changed for every business logic change, changes will flow from different branches and it will harder and harder to merge them.

Activatory helps you to make your tests code more flexible and maintainable.

v1.2.1 #

  • Added unit test for #42 issue, docs adjusted

v1.2.0 #

  • Implemented support of Set class
  • #45 bugfix - iterable is no more substitution for every iterable subclass

v1.1.0 #

  • except parameter added to take and takeUntyped methods
  • Implemented correct activation for Duration type
  • Changed implementation of DateTime factory - now it uses same range as Duration
  • Changed implementation of int factory - now it can return negative number
  • Random instance used inside Activatory can be now accessed from outside
  • Added useOneOf factory overriding method to setup available collection of instances

v1.0.1 #

  • Code style fixes to increase scoring on pub.dev

v1.0.0 #

  • Public release!

v0.0.25 #

  • Docs and samples update
  • Key parameter is allays used as named now
  • pedantic included as default analyzer config
  • fromRandom constructor added
  • Seed can now be provided from outside for repeatability

v0.0.24 #

  • README.md huge update
  • Extra tests added

v0.0.23 #

  • Analyser options configured. Analyzer issues fixed.
  • Activation contexts mismatch fixed
  • Few refactorings applied
  • Bug #30 fixed

v0.0.22 #

  • Implemented take and takeTyped methods to select random item from Iterable
  • Public API documentation improved
  • Entry points refactored
  • Fixed issues with list, iterable, map, Params and generic class activation. Pre configuration is not required any more.
  • Removed params object and arg customization functionality
  • Huge refactoring performed
  • README.md project description refactored

v0.0.21 #

  • Bugfix and pubspec update
  • Changelog sorting reverted

v0.0.20 #

  • Added fields and setter values activation
  • Added customization for fields and setters activation

v0.0.19 #

  • Added default argument values usage customization
  • Added per key customization

v0.0.18 #

  • Added getMany and getManyTyped for effortless array creation
  • Added type aliases support

v0.0.17 #

  • Implemented ctor parameter overloading by type and (optionally) by name

v0.0.16 #

  • Bugfix and documentation improvements
  • Added support for Map<K,V> generating

v0.0.15 #

  • Const ctor supported

v0.0.14 #

  • Fixed wrong counting of types in recursion limiter
  • Added customization mechanism for recursion limiter and array backend
  • Added Null type support

v0.0.13 #

  • Added ctor resolution customization mechanism

v0.0.12 #

  • Added test for generics support with explicit overriding
  • Added parameterizable custom factories support

v0.0.11 #

  • Added recursion handling
  • Performed large refactoring

v0.0.10 #

  • Added custom exception type for all library internal errors

v0.0.9 #

  • Added effortless support for primitive types arrays generating (except of enums arrays)
  • Added support for complex types array generating with explicit array registration
  • Fixed enum generation bug (generated values was not real enum values from values list)

v0.0.8 #

  • Back-ends now can be registered and lookuped using any object as a key
  • useSingleton renamed to pin, useValue renamed to pinValue

v0.0.7 #

  • Added ability to register generated singleton
  • Added ability to register pre defined value

v0.0.6 #

  • Added ability to register factory explicitly

v0.0.5 #

  • Named arguments supported
  • Complex object activation now respects default non null values

v0.0.1-0.0.4 #

  • Added support for primitive objects activation: int, double, String, bool, DateTime
  • Added support for complex object activation by calling ctors, named ctors, factories and using positional arguments

example/activatory_example.dart

import 'package:activatory/activatory.dart';
// ignore_for_file: omit_local_variable_types

void main() {
  final activatory = Activatory();

  // Activatory can create primitive types instances. No pre-configuration required.
  final int randomInt = activatory.get<int>();
  assert(randomInt != null);
  final String randomString = activatory.get<String>();
  assert(randomString != null);
  final DateTime randomDateTime = activatory.get<DateTime>();
  assert(randomDateTime != null);
  final bool randomBool = activatory.get<bool>();
  assert(randomBool != null);
  final MyEnum randomEnumValue = activatory.get<MyEnum>();
  assert(randomEnumValue != null);

  // Activatory can create custom types instances. No pre-configuration required.
  final MyClass randomMyClass = activatory.get<MyClass>();
  // Fields with public setters are automatically filled with random data.
  assert(randomMyClass.intFieldWithPublicSetter != null);
  // Constructor parameters are automatically filled with random data.
  assert(randomMyClass.finalStringFieldFilledWithCtor != null);

  // Activatory can create complex graph of objects. No pre-configuration required.
  final ComplexGraphClass myComplexGraphClass =
      activatory.get<ComplexGraphClass>();
  // Activatory supply random data to constructor parameters and public setters.
  assert(myComplexGraphClass.dateTimeFieldWithPublicSetter != null);
  // If constructor parameter or setter is user defined class it will be created in the same way.
  assert(myComplexGraphClass
          .myClassFieldWithPublicSetter.intFieldWithPublicSetter !=
      null);

  // Recursive graphs are also supported.
  final LinkedNode<int> myLinkedList = activatory.get<LinkedNode<int>>();
  assert(myLinkedList.next.next.value != null); // Default recursion limit is 3.

  // Activatory can create multiple objects at one call.
  final List<int> intArray = activatory.getMany<int>();
  assert(intArray.length == 3); //default array length is 3

  // List, iterables and map parameters or fields are also supported.
  final MyClassWithArrayIterableAndMapParameters collectionsSample =
      activatory.get<MyClassWithArrayIterableAndMapParameters>();
  assert(collectionsSample.intArray.length == 3); //default array length is 3
  assert(
      collectionsSample.intIterable.length == 3); //default iterable length is 3
  assert(
      collectionsSample.intToStringMap.length == 3); //default map length is 3

  // Generics are also supported. No pre-configuration required.
  final MyGenericClass<MyGenericClass<int>> genericClassInstance =
      activatory.get<MyGenericClass<MyGenericClass<int>>>();
  assert(genericClassInstance.value != null);

  // Activatory support all constructor types:
  // 1. Default constructor
  final MyClassWithDefaultConstructor myClassWithDefaultConstructor =
      activatory.get<MyClassWithDefaultConstructor>();
  assert(myClassWithDefaultConstructor.someData != null);
  // 2. Named constructor
  final MyClassWithNamedConstructor myClassWithNamedConstructor =
      activatory.get<MyClassWithNamedConstructor>();
  assert(myClassWithNamedConstructor.someData != null);
  // 3. Factory constructors
  final MyClassWithFactoryConstructor myClassWithFactoryConstructor =
      activatory.get<MyClassWithFactoryConstructor>();
  assert(myClassWithFactoryConstructor.someData != null);

  // Default parameter values are used while they are not nulls. But this behavior can be customized (see below).
  // 1. Named parameters
  final MyClassWithNamedParameter withNamedParameters =
      activatory.get<MyClassWithNamedParameter>();
  assert(withNamedParameters.namedArgumentWithDefaultValue ==
      MyClassWithNamedParameter.namedArgumentDefaultValue);
  assert(withNamedParameters.namedArgumentWithoutDefaultValue != null);
  // 2. Position parameters
  final MyClassWithPositionalParameters withPositionParameters =
      activatory.get<MyClassWithPositionalParameters>();
  assert(withPositionParameters.positionalArgumentWithDefaultValue ==
      MyClassWithPositionalParameters.positionalArgumentDefaultValue);
  assert(withPositionParameters.positionalArgumentWithoutDefaultValue != null);

  // Default object creation strategy can be customized.
  // 1. With explicit function to completely control object creation
  activatory.useFunction((ActivationContext ctx) =>
      42); // Imagine some logic behind 42 receiving, e.g. custom code.
  int goodNumber = activatory.get<int>();
  assert(goodNumber == 42);
  // Function accept activation context that can be used in complex scenarios to activate other types, read settings and etc.
  activatory.useFunction(
      (ActivationContext ctx) => 'This string contains 34 characters');
  activatory
      .useFunction((ActivationContext ctx) => ctx.create<String>().length);
  final int thirtyFour = activatory.get<int>();
  assert(thirtyFour == 34);
  // 2.1. With singleton value generated automatically
  activatory.useGeneratedSingleton<int>();
  final int randomSingletonInt = activatory.get<int>();
  assert(randomSingletonInt != null);
  // 2.2. With singleton value defined by user
  activatory.useSingleton(42);
  goodNumber = activatory.get<int>();
  assert(goodNumber == 42);

  // In some cases you may require different strategies for different activation calls.
  // This can be accomplished with using key parameter of customization/activation methods.
  activatory.useSingleton(42, key: 'good number');
  activatory.useSingleton(13, key: 'not good number');
  activatory.useFunction((ctx) => ctx.create<int>().toString());
  final String goodNumberStr = activatory.get<String>(key: 'good number');
  assert(goodNumberStr == '42');
  final String notGoodNumberStr =
      activatory.get<String>(key: 'not good number');
  assert(notGoodNumberStr == '13');
  // Key can be any type of object. It's value can be accessed through ActivationContext if required.
  final DateTime now = DateTime.now();
  activatory.useFunction((ctx) => ctx.key as DateTime, key: now);
  final DateTime today = activatory.get<DateTime>(key: now);
  assert(today == now);

  // Super classes can be replaced with subtypes.
  activatory.replaceSupperClass<AbstractClass, AbstractClassInheritor>();
  final notSoAbstractInstance = activatory.get<AbstractClass>();
  assert(notSoAbstractInstance.runtimeType == AbstractClassInheritor);

  // Activation behavior can be customized for all types or per type.
  final TypeCustomization defaultCustomization =
      activatory.defaultCustomization;
  defaultCustomization
    ..arraySize = 100500
    ..defaultValuesHandlingStrategy = DefaultValuesHandlingStrategy.ReplaceAll
    ..fieldsAutoFillingStrategy = FieldsAutoFillingStrategy.FieldsAndSetters
    ..maxRecursionLevel = 10
    ..resolvingStrategy = FactoryResolvingStrategy.TakeRandomNamedCtor;

  // Activatory contains helpful methods
  // To take random item(s) from array
  final fromOneToFive = activatory.take([1, 2, 3, 4, 5]);
  assert(fromOneToFive >= 1 && fromOneToFive <= 5);
  final threeItemsFromOneToFiveButNotOne =
      activatory.takeMany([1, 2, 4, 5], count: 3, except: [1]);
  assert(threeItemsFromOneToFiveButNotOne.length == 3);
  assert(threeItemsFromOneToFiveButNotOne.contains(1) == false);

  //See activatory_test.dart for more examples
}

class ComplexGraphClass {
  final DateTime dateTimeFieldWithPublicSetter;
  MyClass myClassFieldWithPublicSetter;

  ComplexGraphClass(
      this.dateTimeFieldWithPublicSetter, this.myClassFieldWithPublicSetter);
}

class MyClass {
  final String finalStringFieldFilledWithCtor;

  int intFieldWithPublicSetter;

  MyClass(this.finalStringFieldFilledWithCtor);
}

class MyClassWithDefaultConstructor {
  final DateTime someData;

  MyClassWithDefaultConstructor(this.someData);
}

class MyClassWithNamedConstructor {
  final DateTime someData;

  MyClassWithNamedConstructor(this.someData);
}

class MyClassWithFactoryConstructor {
  final DateTime someData;

  MyClassWithFactoryConstructor(this.someData);
}

class MyClassWithPositionalParameters {
  static const positionalArgumentDefaultValue = 'Hello positional';
  final String positionalArgumentWithDefaultValue;
  final String positionalArgumentWithoutDefaultValue;

  MyClassWithPositionalParameters(
    this.positionalArgumentWithoutDefaultValue, [
    this.positionalArgumentWithDefaultValue = positionalArgumentDefaultValue,
  ]);
}

class MyClassWithNamedParameter {
  static const namedArgumentDefaultValue = 'Hello named';
  final String namedArgumentWithDefaultValue;
  final String namedArgumentWithoutDefaultValue;

  MyClassWithNamedParameter(
    this.namedArgumentWithoutDefaultValue, {
    this.namedArgumentWithDefaultValue = namedArgumentDefaultValue,
  });
}

class MyClassWithArrayIterableAndMapParameters {
  final List<int> intArray;
  final Iterable<int> intIterable;
  final Map<int, String> intToStringMap;

  MyClassWithArrayIterableAndMapParameters(
      this.intArray, this.intIterable, this.intToStringMap);
}

class MyGenericClass<T> {
  final T value;

  MyGenericClass(this.value);
}

enum MyEnum { A, B, C }

class LinkedNode<T> {
  final T value;
  final LinkedNode<T> next;

  LinkedNode(this.value, this.next);
}

abstract class AbstractClass {}

class AbstractClassInheritor extends AbstractClass {}

Use this package as a library

1. Depend on it

Add this to your package's pubspec.yaml file:


dependencies:
  activatory: ^1.2.1

2. Install it

You can install packages from the command line:

with pub:


$ pub get

Alternatively, your editor might support pub get. Check the docs for your editor to learn more.

3. Import it

Now in your Dart code, you can use:


import 'package:activatory/activatory.dart';
  
Popularity:
Describes how popular the package is relative to other packages. [more]
1
Health:
Code health derived from static analysis. [more]
100
Maintenance:
Reflects how tidy and up-to-date the package is. [more]
100
Overall:
Weighted score of the above. [more]
51
Learn more about scoring.

We analyzed this package on Apr 7, 2020, and provided a score, details, and suggestions below. Analysis was completed with status completed using:

  • Dart: 2.7.1
  • pana: 0.13.6

Dependencies

Package Constraint Resolved Available
Direct dependencies
Dart SDK >=2.0.0 <3.0.0
uuid ^2.0.0 2.0.4
Transitive dependencies
charcode 1.1.3
collection 1.14.12
convert 2.1.1
crypto 2.1.4
typed_data 1.1.6
Dev dependencies
mockito ^4.0.0
pedantic ^1.9.0
test ^1.0.0