TA的每日心情 | 无聊 昨天 09:05 |
---|
签到天数: 1050 天 连续签到: 1 天 [LV.10]测试总司令
|
当应用六边形架构(端口和适配器)访问数据库等基础设施元素时,可以通过适配器的方式实现。适配器只是域定义的接口(端口)的实现。本文将提供同一存储库端口的两个实现,一个在内存中,另一个基于JPA。其重点是如何使用相同的测试集测试这两个实现。
场景
许多在企业场景中开发的软件解决方案都有一些状态,需要保存在持久存储设备中以供以后访问。根据特定的功能性需求和非功能性需求,选择正确的持久性解决方案可能很难,而且很可能需要一份架构决策记录(ADR),其中详细说明了选择的基本原理,包括替代方案和权衡。为了持久保持应用程序状态,用户需要参考CAP定理来做出最适当的决策。
这个决策过程不应该延迟应用程序域模型的设计和开发。工程团队应该专注于交付(业务)价值,而不是维护一堆DDL脚本和开发一个高度变化的数据库模式,在几周(或几个月)之后,他们会意识到使用文档数据库而不是关系数据库可能会更好。
同样,关注交付域值也会阻止工程团队基于过早采取的技术或基础设施相关决策(例如在本例中是数据库技术)的约束而做出与域相关的决策。正如行业专家所说,其架构应该允许延迟框架决策(以及基础设施决策)。
推迟与基础设施相关的决策
回到数据库技术的例子,一种推迟基础设施决策的方法是决定使用哪种数据库技术应使用,它将从存储库的简单内存实现开始,其中域实体可以存储在内存中的列表中。这种方法加速了特性和领域用例的发现、设计和实现,使利益相关者能够快速反馈重要事项:域值。
现在,有人可能会想,“但是,我并没有交付一个端到端工作的特性”,或者“我如何使用存储库的内存适配器验证这一特性?”在这里,像六边形架构(也称为端口和适配器)这样的架构模式和像域驱动设计(DDD)这样的方法(对于拥有干净的架构和最终干净的代码来说不是强制性的)开始发挥作用。
六边形架构
许多应用程序是按照经典的三层架构设计的:
(1)演示/控制器
(2)服务(业务逻辑)
(3)持久层
这种架构倾向于将域定义(例如,域实体和值对象)与表(例如,ORM实体)混合在一起,通常表示为简单的数据传输对象。如下图所示:
与其相反,在六边形架构中,实际的持久性相关类都是基于域模型定义的。
通过使用存储库的端口 (它被定义为域模型的一部分),可以定义与底层技术无关的集成测试,它验证了对存储库的域期望。以下了解在用于管理学生的简单域模型中的代码是什么样子的。
展示代码
作为域的一部分,这个存储库端口看起来如何呢?它本质上定义了域对存储库的期望,并根据域泛在语言定义了所有方法:
Java
public interface StudentRepository {
Student save(Student student);
Optional<Student> retrieveStudentWithEmail(ContactInfo contactInfo);
Publisher<Student> saveReactive(Student student);
}
基于存储库端口规范,可以创建集成测试定义。该定义仅依赖于端口,并且不知道为持久化域状态而做出的任何底层技术决策。这个测试类将有一个属性作为验证期望的存储库接口(端口)的实例。以下显示了这些测试的样子:
Java
public class StudentRepositoryTest {
StudentRepository studentRepository;
@Test
public void shouldCreateStudent() {
Student expected = randomNewStudent();
Student actual = studentRepository.save(expected);
assertAll("Create Student",
() -> assertEquals(0L, actual.getVersion()),
() -> assertEquals(expected.getStudentName(), actual.getStudentName()),
() -> assertNotNull(actual.getStudentId())
);
}
@Test
public void shouldUpdateExistingStudent() {
Student expected = randomExistingStudent();
Student actual = studentRepository.save(expected);
assertAll("Update Student",
() -> assertEquals(expected.getVersion()+1, actual.getVersion()),
() -> assertEquals(expected.getStudentName(), actual.getStudentName()),
() -> assertEquals(expected.getStudentId(), actual.getStudentId())
);
}
}
一旦存储库测试定义完成,就可以为内存存储库创建一个测试运行时(集成测试): ?
Java
public class StudentRepositoryInMemoryIT extends StudentRepositoryTest {
@BeforeEach
public void setup() {
super.studentRepository = new StudentRepositoryInMemory();
}
}
或者使用Postgres对JPA进行更详细的集成测试: ?
Java
@Testcontainers
@ContextConfiguration(classes = {PersistenceConfig.class})
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class StudentRepositoryJpaIT extends StudentRepositoryTest{
@Autowired
public StudentRepository studentRepository;
@Container
public static PostgreSQLContainer container = new PostgreSQLContainer("postgres:latest")
.withDatabaseName("students_db")
.withUsername("sa")
.withPassword("sa");
@DynamicPropertySource
public static void overrideProperties(DynamicPropertyRegistry registry){
registry.add("spring.datasource.url", container::getJdbcUrl);
registry.add("spring.datasource.username", container::getUsername);
registry.add("spring.datasource.password", container::getPassword);
registry.add("spring.datasource.driver-class-name", container::getDriverClassName);
}
@BeforeEach
public void setup() {
super.studentRepository = studentRepository;
}
}
两个测试运行时都扩展了相同的测试定义,因此可以确定,当从内存适配器切换到最终的全功能JPA持久性时,不会有任何测试受到影响,因为它只需要配置相应的测试运行时。
这种方法将允许用户在不依赖于框架的情况下定义存储库端口的测试,并在域定义更好、更稳定,以及团队决定使用更好地满足解决方案质量属性的数据库技术时重用这些测试。
项目的整体结构如下图所示:
项目的结构介绍:
·student-domain:域定义模块,包括实体、值对象、域事件、端口等。这个模块不依赖于框架,尽可能使用[url=]Java[/url]。
· student-application:目前,这个模块没有代码,因为它超出了本文的范围。遵循六边形架构,该模块编排对域模型的调用,成为域用例的入口点。
· student-repository-test:这个模块包含存储库测试定义,不依赖于框架,只验证所提供的存储库端口的期望。
· student-repository-inmemory:域定义的存储库端口的内存实现。它还包含集成测试,该测试为学生存储库测试的测试定义提供了端口的内存适配器。
· student-repository-JPA:域定义的存储库端口的JPA实现。它还包含集成测试,该测试为学生存储库测试的测试定义提供了端口的内存适配器。这个集成测试设置有点复杂,因为它将一个基本的Spring场景和一个Postgres容器一起启动。
· student-shared-kernel:这个模块不在本文讨论范围之内;它为设计项目的其余部分提供了一些实用程序类和接口。
结论
在项目中使用这种架构风格可以促进域模型和基础设施元素之间的良好分离,确保后者不会影响前者,同时促进良好的代码质量(干净的代码)和高可维护性。
|
|