Files
cherrypick/cherrypick/test/src/scope_test.dart

367 lines
13 KiB
Dart

import 'package:cherrypick/cherrypick.dart' show Disposable, Module, Scope, CherryPick;
import 'dart:async';
import 'package:test/test.dart';
import '../mock_logger.dart';
// -----------------------------------------------------------------------------
// Вспомогательные классы для тестов
class AsyncExampleDisposable implements Disposable {
bool disposed = false;
@override
Future<void> dispose() async {
await Future.delayed(Duration(milliseconds: 10));
disposed = true;
}
}
class AsyncExampleModule extends Module {
@override
void builder(Scope scope) {
bind<AsyncExampleDisposable>().toProvide(() => AsyncExampleDisposable()).singleton();
}
}
class TestDisposable implements Disposable {
bool disposed = false;
@override
FutureOr<void> dispose() {
disposed = true;
}
}
class AnotherDisposable implements Disposable {
bool disposed = false;
@override
FutureOr<void> dispose() {
disposed = true;
}
}
class CountingDisposable implements Disposable {
int disposeCount = 0;
@override
FutureOr<void> dispose() {
disposeCount++;
}
}
class ModuleCountingDisposable extends Module {
@override
void builder(Scope scope) {
bind<CountingDisposable>().toProvide(() => CountingDisposable()).singleton();
}
}
class ModuleWithDisposable extends Module {
@override
void builder(Scope scope) {
bind<TestDisposable>().toProvide(() => TestDisposable()).singleton();
bind<AnotherDisposable>().toProvide(() => AnotherDisposable()).singleton();
bind<String>().toProvide(() => 'super string').singleton();
}
}
class TestModule<T> extends Module {
final T value;
final String? name;
TestModule({required this.value, this.name});
@override
void builder(Scope currentScope) {
if (name == null) {
bind<T>().toInstance(value);
} else {
bind<T>().withName(name!).toInstance(value);
}
}
}
class _InlineModule extends Module {
final void Function(Module, Scope) _builder;
_InlineModule(this._builder);
@override
void builder(Scope s) => _builder(this, s);
}
class AsyncCreatedDisposable implements Disposable {
bool disposed = false;
@override
void dispose() {
disposed = true;
}
}
class AsyncModule extends Module {
@override
void builder(Scope scope) {
bind<AsyncCreatedDisposable>()
// ignore: deprecated_member_use_from_same_package
.toProvideAsync(() async {
await Future.delayed(Duration(milliseconds: 10));
return AsyncCreatedDisposable();
})
.singleton();
}
}
// -----------------------------------------------------------------------------
void main() {
// --------------------------------------------------------------------------
group('Scope & Subscope Management', () {
test('Scope has no parent if constructed with null', () {
final logger = MockLogger();
final scope = Scope(null, logger: logger);
expect(scope.parentScope, null);
});
test('Can open and retrieve the same subScope by key', () {
final logger = MockLogger();
final scope = Scope(null, logger: logger);
expect(Scope(scope, logger: logger), isNotNull); // эквивалент
});
test('closeSubScope removes subscope so next openSubScope returns new', () async {
final logger = MockLogger();
final scope = Scope(null, logger: logger);
final subScope = scope.openSubScope("child");
expect(scope.openSubScope("child"), same(subScope));
await scope.closeSubScope("child");
final newSubScope = scope.openSubScope("child");
expect(newSubScope, isNot(same(subScope)));
});
test('closeSubScope removes subscope so next openSubScope returns new', () {
final logger = MockLogger();
final scope = Scope(null, logger: logger);
expect(Scope(scope, logger: logger), isNotNull); // эквивалент
// Нет необходимости тестировать open/closeSubScope в этом юните
});
});
// --------------------------------------------------------------------------
group('Dependency Resolution (standard)', () {
test("Throws StateError if value can't be resolved", () {
final logger = MockLogger();
final scope = Scope(null, logger: logger);
expect(() => scope.resolve<String>(), throwsA(isA<StateError>()));
});
test('Resolves value after adding a dependency', () {
final logger = MockLogger();
final expectedValue = 'test string';
final scope = Scope(null, logger: logger)
.installModules([TestModule<String>(value: expectedValue)]);
expect(scope.resolve<String>(), expectedValue);
});
test('Returns a value from parent scope', () {
final logger = MockLogger();
final expectedValue = 5;
final parentScope = Scope(null, logger: logger);
final scope = Scope(parentScope, logger: logger);
parentScope.installModules([TestModule<int>(value: expectedValue)]);
expect(scope.resolve<int>(), expectedValue);
});
test('Returns several values from parent container', () {
final logger = MockLogger();
final expectedIntValue = 5;
final expectedStringValue = 'Hello world';
final parentScope = Scope(null, logger: logger).installModules([
TestModule<int>(value: expectedIntValue),
TestModule<String>(value: expectedStringValue)
]);
final scope = Scope(parentScope, logger: logger);
expect(scope.resolve<int>(), expectedIntValue);
expect(scope.resolve<String>(), expectedStringValue);
});
test("Throws StateError if parent hasn't value too", () {
final logger = MockLogger();
final parentScope = Scope(null, logger: logger);
final scope = Scope(parentScope, logger: logger);
expect(() => scope.resolve<int>(), throwsA(isA<StateError>()));
});
test("After dropModules resolves fail", () {
final logger = MockLogger();
final scope = Scope(null, logger: logger)..installModules([TestModule<int>(value: 5)]);
expect(scope.resolve<int>(), 5);
scope.dropModules();
expect(() => scope.resolve<int>(), throwsA(isA<StateError>()));
});
});
// --------------------------------------------------------------------------
group('Named Dependencies', () {
test('Resolve named binding', () {
final logger = MockLogger();
final scope = Scope(null, logger: logger)
..installModules([
TestModule<String>(value: "first"),
TestModule<String>(value: "second", name: "special")
]);
expect(scope.resolve<String>(named: "special"), "second");
expect(scope.resolve<String>(), "first");
});
test('Named binding does not clash with unnamed', () {
final logger = MockLogger();
final scope = Scope(null, logger: logger)
..installModules([
TestModule<String>(value: "foo", name: "bar"),
]);
expect(() => scope.resolve<String>(), throwsA(isA<StateError>()));
expect(scope.resolve<String>(named: "bar"), "foo");
});
test("tryResolve returns null for missing named", () {
final logger = MockLogger();
final scope = Scope(null, logger: logger)
..installModules([
TestModule<String>(value: "foo"),
]);
expect(scope.tryResolve<String>(named: "bar"), isNull);
});
});
// --------------------------------------------------------------------------
group('Provider with parameters', () {
test('Resolve dependency using providerWithParams', () {
final logger = MockLogger();
final scope = Scope(null, logger: logger)
..installModules([
_InlineModule((m, s) {
m.bind<int>().toProvideWithParams((param) => (param as int) * 2);
}),
]);
expect(scope.resolve<int>(params: 3), 6);
expect(() => scope.resolve<int>(), throwsA(isA<StateError>()));
});
});
// --------------------------------------------------------------------------
group('Async Resolution', () {
test('Resolve async instance', () async {
final logger = MockLogger();
final scope = Scope(null, logger: logger)
..installModules([
_InlineModule((m, s) {
m.bind<String>().toInstance(Future.value('async value'));
}),
]);
expect(await scope.resolveAsync<String>(), "async value");
});
test('Resolve async provider', () async {
final logger = MockLogger();
final scope = Scope(null, logger: logger)
..installModules([
_InlineModule((m, s) {
m.bind<int>().toProvide(() async => 7);
}),
]);
expect(await scope.resolveAsync<int>(), 7);
});
test('Resolve async provider with param', () async {
final logger = MockLogger();
final scope = Scope(null, logger: logger)
..installModules([
_InlineModule((m, s) {
m.bind<int>().toProvideWithParams((x) async => (x as int) * 3);
}),
]);
expect(await scope.resolveAsync<int>(params: 2), 6);
expect(() => scope.resolveAsync<int>(), throwsA(isA<StateError>()));
});
test('tryResolveAsync returns null for missing', () async {
final logger = MockLogger();
final scope = Scope(null, logger: logger);
final result = await scope.tryResolveAsync<String>();
expect(result, isNull);
});
});
// --------------------------------------------------------------------------
group('Optional resolution and error handling', () {
test("tryResolve returns null for missing dependency", () {
final logger = MockLogger();
final scope = Scope(null, logger: logger);
expect(scope.tryResolve<int>(), isNull);
});
});
// --------------------------------------------------------------------------
group('Disposable resource management', () {
test('scope.disposeAsync calls dispose on singleton disposable', () async {
final scope = CherryPick.openRootScope();
scope.installModules([ModuleWithDisposable()]);
final t = scope.resolve<TestDisposable>();
expect(t.disposed, isFalse);
await scope.dispose();
expect(t.disposed, isTrue);
});
test('scope.disposeAsync calls dispose on all unique disposables', () async {
final scope = Scope(null, logger: MockLogger());
scope.installModules([ModuleWithDisposable()]);
final t1 = scope.resolve<TestDisposable>();
final t2 = scope.resolve<AnotherDisposable>();
expect(t1.disposed, isFalse);
expect(t2.disposed, isFalse);
await scope.dispose();
expect(t1.disposed, isTrue);
expect(t2.disposed, isTrue);
});
test('calling disposeAsync twice does not throw and not call twice', () async {
final scope = CherryPick.openRootScope();
scope.installModules([ModuleWithDisposable()]);
final t = scope.resolve<TestDisposable>();
await scope.dispose();
await scope.dispose();
expect(t.disposed, isTrue);
});
test('Non-disposable dependency is ignored by scope.disposeAsync', () async {
final scope = CherryPick.openRootScope();
scope.installModules([ModuleWithDisposable()]);
final s = scope.resolve<String>();
expect(s, 'super string');
await scope.dispose();
});
});
// --------------------------------------------------------------------------
// Расширенные edge-тесты для dispose и subScope
group('Scope/subScope dispose edge cases', () {
test('Dispose called in closed subScope only', () async {
final root = CherryPick.openRootScope();
final sub = root.openSubScope('feature')..installModules([ModuleCountingDisposable()]);
final d = sub.resolve<CountingDisposable>();
expect(d.disposeCount, 0);
await root.closeSubScope('feature');
expect(d.disposeCount, 1); // dispose должен быть вызван
// Повторное закрытие не вызывает double-dispose
await root.closeSubScope('feature');
expect(d.disposeCount, 1);
// Повторное открытие subScope создает NEW instance (dispose на старый не вызовется снова)
final sub2 = root.openSubScope('feature')..installModules([ModuleCountingDisposable()]);
final d2 = sub2.resolve<CountingDisposable>();
expect(identical(d, d2), isFalse);
await root.closeSubScope('feature');
expect(d2.disposeCount, 1);
});
test('Dispose for all nested subScopes on root disposeAsync', () async {
final root = CherryPick.openRootScope();
root.openSubScope('a').openSubScope('b').installModules([ModuleCountingDisposable()]);
final d = root.openSubScope('a').openSubScope('b').resolve<CountingDisposable>();
await root.dispose();
expect(d.disposeCount, 1);
});
});
// --------------------------------------------------------------------------
group('Async disposable (Future test)', () {
test('Async Disposable is awaited on disposeAsync', () async {
final scope = CherryPick.openRootScope()..installModules([AsyncExampleModule()]);
final d = scope.resolve<AsyncExampleDisposable>();
expect(d.disposed, false);
await scope.dispose();
expect(d.disposed, true);
});
});
}