【译】Flutter整洁架构构建与测试驱动开发(三)
# 【译】Flutter
整洁架构构建与测试驱动开发(三)
我们的Number Trivia
应用程序进展顺利。 在上一部分中,我们使用测试驱动的开发创建了一个实体,存储库合同和第一个用例-GetConcreteNumberTrivia
。 今天,我们将添加另一个用例。
# 可调用类
您是否知道在Dart
中,既可以通过调用object.call()
,同样也可以通过object()
来运行名为call
的方法。 这是在用例中使用的完美方法! 毕竟,它们的类名已经是诸如GetConcreteNumberTrivia
之类的动词,因此将它们用作“伪方法”非常合适。
如果类实现了
call()
方法,则该类的对象可以作为方法使用
本着TDD
的精神,我们将首先修改测试get_concrete_number_test.dart
,使其不再调用execute
方法:
final result = await usecase(number: tNumber);
而且由于代码甚至无法编译,因此我们可以立即修改GetConcreteNumberTrivia
类:
Future<Either<Failure, NumberTrivia>> call({ ...
# 添加另一个用例
除了获取具体数字之外,我们的应用还将获取随机数字。 这意味着我们需要另一个用例-GetRandomNumberTrivia
。 实际上,我们使用的Numbers API
对于具体的数字和随机数具有不同的API
,因此我们不会自己生成数字。我们将在GetRandomNumberTrivia
用例中在域层内部精确执行数字生成代码。 毕竟,生成数字是一种业务逻辑。
# 用例基类
作为干净的编码人员,我们当然希望代码具有可预测的接口。基本上具有相同功能的类的公共方法和属性应具有标准化名称。
对于用例,每个用例都应该有一个调用方法。用例中的逻辑让我们获得随机数字或向月球发送航天飞机,这些都无关紧要,界面应该相同,以免造成任何混乱。
防止一个类具有call
方法而另一个具有execute
方法的另一种方法是提供一个显式的接口(在Dart
的情况下为一个抽象类),该接口是令人难忘的。例如,类似于UseCase
基类。
由于可以在应用程序的多个功能之间共享此类,因此以下代码将进入核心/用例。当然,测试抽象类没有任何意义,因此我们可以立即编写它。
// usecases.dart
import 'package:clean_architecture_tdd/core/error/failures.dart';
import 'package:dartz/dartz.dart';
import 'package:equatable/equatable.dart';
// 必须将参数放入容器对象中,以便可以包含在此抽象基类方法定义中。
abstract class UseCase<Type, Params> {
Future<Either<Failures, Type>> call(Params params);
}
// 每当用例不接受任何参数时,调用用例的代码将使用此方法。
class NoParams extends Equatable {}
2
3
4
5
6
7
8
9
10
11
12
13
# 继承基类
如您所见,我们向UseCase
类添加了两个类型参数。 一种是针对“无错误”的返回类型,在我们的应用中,该类型为NumberTrivia
实体。 另一个类型参数Params
将在已经存在的GetConcreteNumberTrivia
用例中引起一些小的代码更改。
每个UseCase
子类都将在同一文件内单独定义参数类,用于向call
方法传递。处理类工作的其他更改只有在更新测试后才会出现。
// get_concrete_number_trivia.dart
class GetConcreteNumberTrivia extends UseCase<NumberTrivia, Params> {
...
}
class Params extends Equatable {
final int number;
Params({ this.number}) : super([number]);
}
2
3
4
5
6
7
8
9
10
现在我们知道调用必须接受一个Params
对象,而不是直接将整数作为参数。 因此,由于我们在上一部分中编写了一个测试,因此我们要做的就是:
- 更新测试以使用参数。
- 它不会编译。
// get_concrete_number_trivia_test.dart
...
test(
'should get trivia for the number from the repository',
() async {
// arrange
when(mockNumberTriviaRepository.getConcreteNumberTrivia(any))
.thenAnswer((_) async => Right(tNumberTrivia));
// act
final result = await usecase(Params(number: tNumber));
// assert
expect(result, Right(tNumberTrivia));
verify(mockNumberTriviaRepository.getConcreteNumberTrivia(tNumber));
verifyNoMoreInteractions(mockNumberTriviaRepository);
},
);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- 更新生产代码。 使用
Params
作为调用方法的参数。 - 运行测试-它应该通过,代码置信度可以通过。
// get_concrete_number_trivia.dart
class GetConcreteNumberTrivia extends UseCase<NumberTrivia, Params> {
...
Future<Either<Failure, NumberTrivia>> call(Params params) async {
return await repository.getConcreteNumberTrivia(params.number);
}
}
...
2
3
4
5
6
7
8
9
# GetRandomNumberTrivia
现在,添加这个新用例非常简单-我们已经统一了每个UseCase
必须具有的接口。 同样,由于Number Trivia App
的简单性质,此新用例将仅从存储库中获取数据。
我们再次从编写测试开始-在test /.../ usecases
文件夹中创建一个新文件。 对于先前的用例,大多数代码是从测试中复制的。
// get_random_number_trivia_test.dart
import 'package:clean_architecture_tdd/core/usecases.dart';
import 'package:clean_architecture_tdd/features/number_trivia/domain/entities/number_trivia.dart';
import 'package:dartz/dartz.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:clean_architecture_tdd/features/number_trivia/domain/repositories/number_trivia_repository.dart';
class MockNumberTriviaRepository extends Mock
implements NumberTriviaRepository {}
void main() {
GetRandomNumberTrivia usecase;
MockNumberTriviaRepository mockNumberTriviaRepository;
setUp(() {
mockNumberTriviaRepository = MockNumberTriviaRepository();
usecase = GetRandomNumberTrivia(mockNumberTriviaRepository);
});
final tNumberTrivia = NumberTrivia(number: 1, text: 'test');
test(
'should get trivia from the repository',
() async {
when(mockNumberTriviaRepository.getRandomNumberTrivia())
.thenAnswer((_) async => Right(tNumberTrivia));
// 由于随机数不需要任何参数,因此我们传入NoParams。
final result = await usecase(NoParams());
expect(result, Right(tNumberTrivia));
verify(mockNumberTriviaRepository.getRandomNumberTrivia());
verifyNoMoreInteractions(mockNumberTriviaRepository);
},
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
当然,现在该测试失败,因为GetRandomNumberTrivia
还没有实现,实现如下:
// get_random_number_trivia.dart
import 'package:clean_architecture_tdd/core/error/failures.dart';
import 'package:clean_architecture_tdd/core/usecases.dart';
import 'package:clean_architecture_tdd/features/number_trivia/domain/entities/number_trivia.dart';
import 'package:clean_architecture_tdd/features/number_trivia/domain/repositories/number_trivia_repository.dart';
import 'package:dartz/dartz.dart';
class GetRandomNumberTrivia extends UseCase<NumberTrivia, NoParams> {
final NumberTriviaRepository repository;
GetRandomNumberTrivia(this.repository);
Future<Either<Failures, NumberTrivia>> call(NoParams params) async {
return await repository.getRandomNumberTrivia();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
现在,该测试将通过,并且我们已经完全实现了Number Trivia App
的域层。 在下一部分中,我们将开始在包含Repository
实现和数据源的数据层上工作。