CUBA 应用程序中的中间件集成测试

测试自动化对于所有应用程序开发过程的成败都是至关重要的,对于质量保证来说它是一项高回报的投资。CUBA 应用程序构建于 Java 生态之上,其具有强大且成熟的自动化测试功能,本指南将带您了解如何利用这些功能。

首先,您将了解在 CUBA 应用程序中的常规测试工作,然后本指南将着重介绍中间件中的集成测试。

将要构建的内容

本指南对 宠物诊所示例进行了增强,以演示如何以自动化的方式测试现有功能:

  • 根据给定的宠物身份号创建一条就诊(Visit)信息

  • 对有生命危险的宠物自动发送 "疾病警告邮件(Disease Warning Mailing)"

开发环境要求

您的开发环境需要满足以下条件:

下载 并解压本指南的源码,或者使用 git 克隆下来:

额外的测试依赖

本指南使用 JUnit 5 和 AssertJ 开发并运行自动化集成测试。因此,应该在三个模块中(global, core and web)添加下列依赖:

build.gradle
configure([globalModule, coreModule, webModule]) {
    // ...
    dependencies {
            testCompile('org.junit.jupiter:junit-jupiter-api:5.5.2')
            testCompile('org.junit.jupiter:junit-jupiter-engine:5.5.2')
            testCompile('org.junit.vintage:junit-vintage-engine:5.5.2')

            testCompile('org.assertj:assertj-core:3.11.1')

    }
    // ...
}

示例: CUBA 宠物诊所

这个示例是以 CUBA 宠物诊所项目为基础,而这个项目的基础是众所周知的 Spring 宠物诊所项目。CUBA 宠物诊所应用程序涉及到了宠物诊所的领域模型及与管理一家宠物诊所相关的业务流程。

这个应用程序的领域模型如下:

领域模型

主要的实体是 PetVisit。 Pet 到诊所就诊,就诊时(Vist) 会有一名兽医(Vet)负责照顾它。每个宠物都有主人,一个主人可以有多个宠物。一次就诊(Vist)即是一个宠物在主人的帮助下到诊所诊治的活动。

简介

多年来,测试自动化是软件开发行业中被普遍认可的最佳实践。通过编写一个小型测试应用程序,该应用程序执行 “生产” 应用程序的特定部分并验证其行为是否正确。

在本指南中使用的术语 “生产” 是指应用程序的功能性代码,用于将其与定义测试用例的代码(“测试代码”)区分开来。 这里的“生产” 并不 “生产环境” 中的生产。

在 Java 生态中,测试自动化历史悠久, 这方面的代表作是 JUnit 框架,它是市场上出现最早的测试框架之一。

与其他所有 Java 应用程序一样,在 CUBA 应用程序中,支持编写自动化测试用例来对应用程序的特定部分进行测试,我们也建议这么做。

在测试用例(一般是一个位于特定测试源码目录的另外一个Java类)中,将实例化生产类并执行其方法。然后,通过将预期结果与实际结果进行比较来验证结果。根据比较结果,测试用例将被标识为 “成功” 或 “失败”。

CUBA 应用程序分为两部分:中间件部分和客户端部分。中间件由项目中的 coreglobal 模块组成。它通常包含了大部分业务逻辑、与其他系统的集成以及与数据库相关的交互。

本指南仅关注测试 CUBA 应用程序的中间件部分,对于客户端而言,需要使用不同的模式和测试策略。因此,我们会在另外一个专门的指南中介绍客户端的测试。

测试环境

测试环境需要根据生产代码的执行要求来准备一些特定的条件。比如要测试与数据库交互的生产类,则在生产环境必须配置数据库,且生产代码可以正确的连接到这个数据库。

如果只是测试汇总客户订单额的生产类,则测试环境可能可以简单一些,不需要依赖其它部分。

在 CUBA 中间件,可以很方便地建立这两种测试的环境,以执行测试类。需要运行时依赖项(如数据库)的第一种测试一般被称为 “集成测试”。不需要任何外部环境支持就可执行的测试称为 “单元测试”。

在下一章节中,您将了解这两种环境类型的区别,而本指南将着重介绍集成测试部分。

中间件集成测试

在这种环境下,生产应用程序将部分启动。 CUBA 中间件提供的集成测试环境是运行测试用例的常见方式。这意味着所有 CUBA 平台类的工作方式都与应用程序的生产代码一样。

例如,当调用 dataManager.load(Customer.class).id(123).one() 时,DataManager 接口会真正地去查询数据库并获取 ID 为 123 的客户。应用程序中的生产代码: CustomerCreationService 也是一样的,它会在数据库中创建一个 Customer。

集成测试环境包含以下环境相关的部分:

  • Spring 应用程序上下文

  • 到外部数据库的连接

  • 平台 API 与生产代码的工作方式相同

环境在测试用例可以使用之前,需要几秒钟的时间去启动,然后才能开始测试。

数据库连接

CUBA 集成测试使用的数据库连接是 CUBA 应用程序运行时建立的连接。如果使用的是 HSQLDB,在执行测试用例时,CUBA Studio 启动数据库服务。 默认情况下,测试环境和本地应用程序之间 “共享” 数据库实例。这意味着在测试用例可以使用在本地运行应用程序时产生的数据,测试用例如果更改了的数据库,这些更改也会体现在本地应用程序。

在两个环境之间共享数据确实很方便、高效,但也会导致一定程度的不确定性。不同环境之间数据库数据可能会互相干扰,这会使测试用例中的断言很难表达出来,因为您不能确定哪些数据可以更改、可以修改多少数据。在两种环境各自使用独立的数据库可以避免这一问题。

测试容器

测试容器是测试用例中的一个对象,主要负责启动和管理集成测试环境。在测试用例中测试容器一般被用来设置测试环境:

  • 启动 Spring 应用程序上下文

  • 建立数据库连接

  • 实例化所有 CUBA 平台 API

  • 激活所有应用程序组件及其配置

CUBA Studio 会在 core 模块的 Test 源码目录生成 TestContainer 类。下面您将看到 PetclinicTestContainer 的示例用法:

SampleIntegrationTest.java
public class SampleIntegrationTest {

    @RegisterExtension (1)
    public static PetclinicTestContainer testContainer =
            PetclinicTestContainer.Common.INSTANCE; (2)

    @Test
    public void persistenceAPI_canBeRetrieved_fromTheTestContainer() {

        Persistence persistence = testContainer.persistence(); (3)

        assertNotNull(persistence);
    }
}
1 JUnit 集成测试使用 PetclinicTestContainer 扩展
2 petclinic 测试容器实例定义为测试类的一个字段
3 在测试用例中容器用于和测试环境进行交互
本指南的示例使用 JUnit5 作为测试库。CUBA 支持多种测试框架,比如 Spock,默认情况下 JUnit 已经搭载了 Spock。

中间件测试

除了集成测试环境之外,还可以创建完全不启动 Spring 应用程序上下文和任何 CUBA 平台 API 的测试用例。 上述用例(汇总订单额),也不必启动测试环境。

不使用集成测试环境而以独立的方式编写单元测试两个明显的好处:

  • 测试环境局部化

  • 测试执行速度

但是使用单元测试也有缺点和限制。我们会在另外一个主题对此进行讨论。

功能性测试

在下面的章节中您将学习如何将前面的理论应用到实践,我们实现一些集成测试来验证 在 CUBA 中使用数据 这个指南中描述的功能。

在 CUBA Studio 中使用 JUnit 测试

Petclinic 示例中的测试用例可以通过 Gradle 执行,也可以直接在 CUBA Studio 中通过 UI 来执行。

相应的 gradle task 是: ./gradlew test (Linux、Mac)或 gradlew.bat test (Windows)

要在项目的文件列表中查看测试用例,需要在 Project 工具窗口中将项目视图切换 到 Project 视图。

studio intellij switch project view

在 CUBA Studio 中, 通过为测试用例创建运行配置(Configuration)并执行此运行配置来执行测试。测试用例或类可以通过快捷方式 CRTL+F5(Windows)/CMD+R(Mac)执行,也可以使用测试用例/测试类上的 Run 图标来执行:

running test via cuba studio

CUBA Studio 在测试方面使用了标准的 IntelliJ IDEA 功能。可以在 IDEA文档 中找到更多信息。

自动创建就诊(Visit)记录

第一个测试用例是有关 Petclinic 应用程序中就诊记录的自动创建。 在 Visit 浏览界面中,用户可以通过输入宠物身份号码来快速创建就诊信息。软件会为输入的宠物创建一条就诊记录。

这个功能使用了一个 CUBA 服务: VisitService,从 UI 调用这个服务。服务包含一个方法 Visit createVisitForToday(String identificationNumber) ,这个方法就是测试用例的测试对象。

尽管该功能包括 UI 部分,但这不属于自动化测试的一部分,独立地对功能实现的一部分进行测试是很普遍的。话虽如此,仍然需要一个真正的端到端测试用例来验证所有部分是否可以正常工作,但是这超出了本指南的范围。

功能实现

在研究第一个测试用例之前,我们先回顾一下 VisitService 的功能:

  1. 根据指定的身份号码查找宠物

    1. 如果找到宠物,则使用它

    2. 如果没有找到宠物,则不创建就诊(Visit)记录

  2. 使用正确的属性创建一个 Visit 实例

    1. 找到正确的宠物实例,并将给赋给 Visit实例

    2. 使用 today 作为 visitDate

  3. 存储新创建的实例

功能实现如下:

VisitServiceBean.java
@Service(VisitService.NAME)
public class VisitServiceBean implements VisitService {
  @Inject
  protected DataManager dataManager;
  @Inject
  protected TimeSource timeSource;
  //...
  @Override
  public Visit createVisitForToday(String identificationNumber) {
    Optional<Pet> pet = loadPetByIdentificationNumber(identificationNumber);  (1)

    if (!pet.isPresent()) {
      return null;
    }

    return saveVisit( (2)
        createVisitForPet(pet.get())
    );
  }

  private Visit createVisitForPet(Pet pet) {
    Visit visit = dataManager.create(Visit.class);
    visit.setPet(pet);
    visit.setVisitDate(timeSource.currentTimestamp());
    return visit;
  }

  private Visit saveVisit(Visit visit) {
    return dataManager.commit(visit);
  }

  private Optional<Pet> loadPetByIdentificationNumber(String identificationNumber) {
    return dataManager.load(Pet.class)
        .query("e.identificationNumber = ?1", identificationNumber)
        .optional();
  }
  //...
}
1 根据指定的身份号码搜索宠物,返回 Optional<Pet>
2 如果找到了宠物,将创建对应的就诊信息并存储

测试类设置

在实现定义的测试用例前,让我们看一下集成测试用例的一般结构。这个结构基于上述的 SampleIntegrationTest 。 在此测试中,有一些值得注意的附加功能可以简化测试用例。

VisitServiceTest.java
public class VisitServiceTest {
    @RegisterExtension
    public static PetclinicTestContainer testContainer = PetclinicTestContainer.Common.INSTANCE;
    private static VisitService visitService; (1)
    private static PetclinicVisitDb db;

    private Visit visit;
    private Pet pikachu;

    @BeforeAll
    public static void setupEnvironment() {
        visitService = AppBeans.get(VisitService.class);

        db = new PetclinicVisitDb( (2)
            AppBeans.get(DataManager.class),
            testContainer
        );
    }

    @BeforeEach
    public void loadPikachu() { (3)
        pikachu = db.petWithName("Pikachu", "pet-with-owner-and-type");
    }

    // different test cases...

    @AfterEach
    public void cleanupVisit() { (4)
        db.remove(visit);
    }
}
1 VisitService 在此测试用例中是受测系统 (system under test -SUT)
2 PetclinicVisitDb 类作为所有数据库相关交互的容器,比如测试数据和验证
3 loadPikachu 是一个 @BeforeEach 方法示例,这个类中所有的测试方法执行前都要这个方法
4 cleanupVisit 每个测试用例执行完成后执行此方法来清理数据库

JUnit 注解 @BeforeEach@AfterEach 用于指定在测试用例执行前或执行后要执行的特定代码

测试

这里有一个重要的代码段是 PetclinicVisitDb 类及其通过 db 变量来使用的方法。 这个类封装了在 VisitServiceTest 类中用到的数据库交互操作,包括以下 API:

PetclinicVisitDb.java
public class PetclinicVisitDb {

  public Pet petWithName(String name, String view) { /* ... */ }

  public void remove(Entity<UUID> entity) { /* ... */ }

  public Optional<Pet> petWithIdentificationNumber(
          String identificationNumber
  ) { /* ... */ }

  public Long countVisitsFor(Pet pet) { /* ... */ }

  public Long countVisits() { /* ... */ }

}

在内部,这个类调用 dataManager 和其它 API 来为测试用例提供特定 API。这个类是 测试工具方法模式 - Test Utility Method pattern后门维护模式 - Back Door Manipulation Pattern 的组合实现。

这些逻辑不一定要放到一个专门的类中去实现,但是这样做有助于将测试类从各种工具方法或内联代码中解放出来,这些内联代码、工具方法增加了测试用例的"噪音",会对测试的重点形成干扰。

Arrange Act Assert

在本指南中定义的大部分测试用例遵循 Arrange-Act-Assert 模式:

首先是测试数据设置或测试数据验证阶段 - (Arrange),确保测试用例有稳定的测试数据。接下来使用期望的输入参数来执行验证代码 - ACT 。最后一阶段,验证代码是否按期望的方式更改了系统 - Assert

第一个测试用例

基于对功能描述,我们可以识别并定义出一组测试用例,这些用例可以反映出上述行为:

  1. createsANewVisit_forTheCorrectPet - 验证 1.a2.a3.

  2. createsANewVisit_withTheCorrectVisitInformation - 验证 1.a2.b3.

  3. createsNoVisit_forAnIncorrectIdentificationNumber - 验证 1.b

我们来看看这个测试用例,这个测试用例将验证新的就诊记录(Visit) 是否关联了正确的宠物 Pet :

VisitServiceTest.java
class VisitServiceTest {
    // ...
    private Visit visit;
    private Pet pikachu;

    @BeforeEach
    public void loadPikachu() {
        pikachu = db.petWithName("Pikachu", "pet-with-owner-and-type");
    }

    @Test
    public void createVisitForToday_createsANewVisit_forTheCorrectPet() { (1)
        // given: there is one visit associated to pikachu
        assertThat(db.countVisitsFor(pikachu))
            .isEqualTo(1);
        // when: logic is executed for pikachus identification number
        visit = visitService.createVisitForToday(
                pikachu.getIdentificationNumber()
        );
        // then: there are two visits associated to pikachu
        assertThat(db.countVisitsFor(pikachu)) (2)
            .isEqualTo(2);
    }
}
1 JUnit 测试用例的方法名,这个命名从语义级别描述出测试用例的作用
2 使用一个合适的 AssertJ assertion 来表达断言

如上所述,测试用例使用了包含三个步骤的 Arrange-Act-Assert 模式, 在这个测试用例中通过 givenwhenthen 注释描述了这三个步骤。

使用传递的 Pikachu 的身份号码,然后断言 DB 中的就诊记录数量,这种情况下可以确定地说,VisitService 正确地执行了按身份号码查找宠物的行为(1.a)。

更进一步,我们也可以断定 2.a3. 也能被正确处理,因为我们根据 Pikachu 外键对就诊主记录(Visits)进行了另外一次 DB 查找,验证了就诊记录增加了一条。

assertThat 语句比较两个值:db.countVisitsFor(pikachu)2 。如果他们相等,测试被认为是成功的,否则它会被视为失败。

数据库种子数据(Seed Data)

这里要注意的一个问题是数据库中已经有一些数据。不仅已经存储了诸如 “Pikachu” 之类的宠物,还存储了就诊(Visit)信息。这是因为在 Petclinic 应用程序中,30.create-db.sql 文件包含用于演示的种子数据。在测试自动化方面,这既有好处也有缺点。

一方面,由于已经有了一些数据,它使测试数据的设置更加容易。在执行任何测试用例之前,无需通过编程方式创建所需的 PetPetTypeVisit 实体。这使测试用例的创建更加容易和快捷。

另一方面,依赖于在测试用例之外定义的测试数据也会带来维护成本。通常,这会使测试用例对测试数据的更改的适应性降低。因为这是所有测试用例(以及正在运行的应用程序)共享的 “中心数据”,所以情况可以会更糟。同样,由于很难全面了解已有数据之间的各种关系及隐藏的假设,测试目标会变地 “模糊”。

为了改善这种情况,第一个测试用例包含一个 卫戍断言(Guard Assertion)assertThat(db.countVisitsFor(pikachu)).isEqualTo(1);。它可以确保测试数据设置符合期望。如果未提供此前提条件,则测试将立即失败,并指出该问题。如果没有此前提条件,发生下游错误时会很难确定测试用例失败的真正原因。除了测试用例本身,它还有助于用例的读者更好地理解用例。

可以进一步改善这个用例,同时仍保留 “中心种子数据” 设置。实际上,对于测试用例,在数据库中是否有 1条、2条或85条就诊记录是无关紧要的。唯一需要确定的是,在执行之后,该数量是否准确地增加了1条。

为了使测试更独立于 “就诊” 数据的数量,可以在测试执行之前先获取数量,如下所示:

class VisitServiceTest {
    @Test
    public void createVisitForToday_createsANewVisit_forTheCorrectPet() {
        // given: the amount of visits for pikachu is captured
        Long visitAmountBefore = db.countVisitsFor(pikachu);

        // when: logic is executed for the pikachus identification number
        visit = visitService.createVisitForToday(
                pikachu.getIdentificationNumber()
        );
        // then: there is one more visit associated to pikachu

        assertThat(db.countVisitsFor(pikachu))
            .isEqualTo(visitAmountBefore + 1);
    }
}

通过这种更改,无论环境增加了了多少次 pikachu 的就诊记录,测试都会很有弹性。这种方式是 Delta Assertion 模式的实现。

CreateVisitForToday 的其它测试用例

此示例还有另外两个测试用例,本指南中不讨论这两个示例,因为它们与上面的测试用例非常相似。您可以在相应的示例项目中找到它们:

疾病警告邮件

下一个应该测试的功能是为有生命危险的宠物发出 “疾病警告邮件” 的功能。 与以前的功能一样,此功能在名为 DiseaseWarningMailingService 的服务的 warnAboutDisease 方法中定义。

该功能的实现包含以下部分: 1. 发现所有有生命危险的宠物: a. 是指定类型 b. 住在对应的城市 c. 其主人有一个有效的 EMail 2. 将为每只宠物发送一封电子邮件,其中包含有关疾病的信息

我们将使用标准的 CUBA 功能发送电子邮件: EmailerAPI

测试类设置

这个测试用例的设置与之前展示的测试类非常相似。在测试用例旁边,有一个包含测试工具方法的类: PetclinicMailingDb。 另外,在这个用例中,一些测试数据会在每次测试开始时从数据库加载,这个用例还包含了用于确认数据存在的断言(像前面一样)。

DiseaseWarningMailingServiceTest.java
public class DiseaseWarningMailingServiceTest {

    private final String ALABASTIA = "Alabastia";
    private final String ELECTRICAL_OVERCHARGING = "Electrical overcharging";
    // ...
    private PetType electricType; (1)
    private List<Pet> electricPetsFromAlabastia;

    @BeforeEach
    public void loadTestDataAndVerifyItsCorrectness() {  (2)
        // given: there is an 'Electric' pet type
        electricType = db.petTypeWithName("Electric");

        assertThat(electricType)
            .isNotNull();

        // and: there is exactly one electric pet from Alabastia
        electricPetsFromAlabastia = db.petsWithTypeFromCity(
            electricType,
            ALABASTIA,
            "pet-with-owner-and-type"
        );

        assertThat(electricPetsFromAlabastia)
            .hasSize(1);
    }

    // different test cases...

    private int warnAboutElectricalOverchargingIn(String city) { (3)
        return diseaseWarningMailingService.warnAboutDisease(
            electricType,
            ELECTRICAL_OVERCHARGING,
            city
        );
    }

    @AfterEach
    public void clearOutgoingEmails() { (4)
        db.outgoingEmails()
            .forEach(db::removeEntity);
    }
}
1 在这个测试类中 electricTypeelectricPetsFromAlabastia 是公共测试数据的容器
2 @BeforeEach 方法加载公共测试数据并且验证其正确性
3 将对受测试系统(SUT)的调用提取到一个测试工具方法(Test Utility Method)
4 每个测试用例执行后,清除生成的 Email 实例

如上面最后一个步所示(4),对验证 Email 发送功能使用了一个特定的方式。

状态验证 vs. 行为验证

在发送电子邮件时,要验证电子邮件是否已正确发送给接收者时,我们可以想象到,这种验证是比较困难的。 测试用例无法确保电子邮件实际上已经发送出去,因为它没有直接参与邮件发送过程。有以下几种验证方式:

  1. 通过程序在真实存在的邮箱账户中检测收到的邮件

  2. 在测试用例中模拟 SMTP 服务器并且验证从应用程序中接收的特定消息

  3. 通过 Spring 配置使用另外一个实现来 Mock EmailerAPI ,验证它是否能被正确调用

这三种不同的方法可以被归类为 状态验证行为验证行为验证 着眼于两个协作者(类、模块甚至系统),并且基于特定交互的事实,得出验证正确的结论。而状态验证不考虑系统与其他协作者的交互方式,而是查看执行后的状态并检查是否达到预期状态。

因此, 1. 可视为状态验证, 2.3. 是行为验证。

CUBA Email 功能的验证方式

EmailerAPI 有一个特殊的方法实现 List<SendingMessage> sendEmailAsync(EmailInfo info); 。需要注意的是,此异步方法的处理方式是:将 EmailInfo 对象存储到名为 EmailSending 的数据库表,并且将 SendingStatus 属性设置为 QUEUE 。CUBA 框架会定期运行一个计划任务,它会提取这些电子邮件并尝试将其发送出去。 因为这个过程独立于 emailerAPI.sendEmailAsync(emailInfo) 的调用线程,在另外一个线程中执行,所以此过程是异步的。

这些信息使我们能够想出第四种验证方法:

4. 验证 EmailerAPI 使用的 EmailSending 数据库表中的表记录,这些记录代表 CUBA Email 功能的邮件发送请求。

这种方式也是状态验证。 通常,应该优先使用状态验证,从维护的角度来看,状态验证的维护成本更低,因为不需要花费时间去 Mock 一些 API。

与方式 1. 相比,方式 4. 只需要准备很少地基础设施。而方式 1. 需要以下系统环境:

  • 具有 IMAP 访问权限的真实的电子邮件帐户

  • 在 CUBA Email 子系统中正确的 SMTP 服务器配置

  • 在测试用例中以编程方式与 IMAP 收件箱进行交互的方法

  • 可用的互联网连接

  • 在测试用例中实现一种机制,以编程的方式检测收件箱,并且需要支持等待并重试

进一步考虑,实际上所有这些要点都是在验证 CUBA Email 功能的正确性。由于已经在框架本身中对其进行了测试,因此没有必要重复进行验证。您可以依赖 CUBA Email 功能的正确性,所以我们应该将对邮件发送功能的测试从测试工作中排除。

尽量避免在集成测试中对框架提供的功能进行重复测试。因为它们已经被框架本身进行了充分测试,是可信赖的。

回到前面的测试用例:方式 4. 看起来最合理,因为它是状态验证,基本上体现出了 Petclinic 应用程序与 EmailerAPI 之间的契约。

其它测试用例

接下来,我们将研究一下 DiseaseWarningMailingService 的其它具体的测试用例。

可以识别并定义下测试用例:

  • warnAboutDisease_createsAnEmail_forEachEffectedPet

    检查传递给 EmailerAPI 的电子邮件数量是否正确(happy path)

  • warnAboutDisease_createsNoEmails_forACityWithoutOwners

    如果在特定城市中没有宠物主人可以通知,则验证否定代码路径(negative code path)

  • warnAboutDisease_createsEEmail_withTheRightEmailInformation

    检查要发送的电子邮件的详细信息是否与宠物主人的信息匹配

  • warnAboutDisease_createsNoEmail_whenTheOwnerDoesNotHaveAnEmailAddress

    检查是否在发送电子邮件之前过滤掉了未提供电子邮件地址的拥有者宠物

我们看一下第一个测试用例,该用例验证发送的电子邮件数量是否正确:

DiseaseWarningMailingServiceTest.java
public class DiseaseWarningMailingServiceTest {
    // ...
    @Test
    public void warnAboutDisease_createsAnEmail_forEachEffectedPet() {
        // given: there is only one electric pet from Alabastia
        assertThat(electricPetsFromAlabastia)
            .hasSize(1); (1)
        // when: a warning is send out for Alabastia
        int effectedPets = warnAboutElectricalOverchargingIn(ALABASTIA);  (2)
        // then: the amount of effected Pets match the expected amount of one
        assertThat(effectedPets)
            .isEqualTo(1);
        // and: there is one outgoing Email for CUBAs Email Sending Functionality
        assertThat(db.outgoingEmails())
            .hasSize(1);  (4)
    }
    // ...
}
1 另外一个卫戍断言,用于确保对于 Alabastia 应该有一个匹配的 Pet 实例
2 Alabastia 的 SUT(受测系统)执行

第二个测试用例与第一个相反。 对于没有宠物的城市,不应发送电子邮件:

DiseaseWarningMailingServiceTest.java
public class DiseaseWarningMailingServiceTest {
    // ...
    @Test
    public void warnAboutDisease_createsNoEmails_forACityWithoutOwners() {

        // given: there is no owner in the Cerulean City
        assertThat(db.ownersFromCity("Cerulean City")) (1)
            .isEmpty();
        // when: a warning is send out for Cerulean City
        int effectedPets = warnAboutElectricalOverchargingIn("Cerulean City");
        // then: no Pet should be effected
        assertThat(effectedPets)
            .isEqualTo(0);
        // and: no outgoing Email should be stored in the DB
        assertThat(db.outgoingEmails())
            .hasSize(0);
    }
    // ...
}
1 一个卫戍验证,用于确保对于 Cerulean City 在数据库中没有宠物

这里讨论的最后一个测试用例是 warnAboutDisease_createsNoEmail_whenTheOwnerDoesNotHaveAnEmailAddress ,因为它与其他测试用例不同。该测试用例利用了 嵌套测试的功能。它允许您在测试类中对某些测试用例进行分组,并为它们提供共享的上下文。 在这个用例中,它用于创建和清除用于嵌套类内的一些测试数据(额外的宠物主人和宠物),这些数据是匹配 Alabastia 条件的数据。

DiseaseWarningMailingServiceTest.java
public class DiseaseWarningMailingServiceTest {
    // ...
  @Nested
  class WarnAboutDiseaseWithAdditionalElectricPetAndOwner { (1)

        private Owner falkner; (2)
        private Pet zapdos;

        @BeforeEach
        void createAdditionalElectricPetAndOwner() {
            // given: there is a second owner in Alabastia with an electric pet without an Email Address
            falkner = db.createOwner("Falkner", null, "Miastreet 234", ALABASTIA);
            zapdos = db.createPet("123", "Zapdos", electricType, falkner);
        }

        @Test
        public void warnAboutDisease_createsNoEmail_whenTheOwnerDoesNotHaveAnEmailAddress() {

        // given: there is two pets with type 'electric' from Alabastia
        List<Pet> electricPetsFromAlabastia =
            db.petsWithTypeFromCity(electricType, ALABASTIA);
        assertThat(electricPetsFromAlabastia)
            .hasSize(2);
        // when: a warning is send out for Alabastia
        int effectedPets = warnAboutElectricalOverchargingIn(ALABASTIA);
        // then: only one email was send out for the pet with an owner that has an email
        assertThat(effectedPets)
              .isEqualTo(1);
        assertThat(db.outgoingEmails())
              .hasSize(1);
        }

        @AfterEach
        void removeAdditionalElectricPetAndOwner() { (3)
            db.removeEntity(zapdos);
            db.removeEntity(falkner);
        }
    }
    // ...
}
1 卫戍验证,以确保 Cerulean City 在 DB 中没有宠物
2 falknerzapdosFresh Fixture 测试数据,仅在内部 @Nested 类中可用
3 @Nested 类中的每个测试用例完成后执行对 fixture 的清理

可以在相应的测试类 DiseaseWarningMailingServiceTest.java 中找到此功能的其余测试用例。

总结

本指南涵盖了许多说明和概念。作为总结,在这里重述要点。

本指南从测试自动化的一般概述开始。我们简要提到了测试自动化的好处,并描述了 Java 生态系统中自动化测试的一般设置。 之后,我们介绍了不同的测试环境分类:单元测试集成测试。 您还了解了一些 CUBA 细节,例如 CUBA 中间件测试、如何在测试中建立数据库连接以及测试容器的概念。

然后,指南重点介绍了 Petclinic 示例应用程序中的两个示例。

在根据给定的宠物身份号自动创建 就诊(Visit) 时,我们展示了测试类的设置。它还包含了对 “测试工具方法” 的说明,以及在本指南的测试用例中是如何体现的。通过展示一个具体的测试用例,我们解释了如何设置 Arrange-Act-Assert 模式的测试用例。该示例还包含了对测试/种子数据设置的说明,以及本指南中使用的共享 fixture 测试数据的优缺点。

在给宠物自动发送 "疾病警告邮件" 的示例中引出了对依赖项进行测试的问题 ,这个功能中调用的 EmailerAPI 即为依赖项。我们讨论了对依赖项进行测试的不同的方法,并将它们分为 状态验证行为验证 两类。我们在示例测试用例中选择了状态验证方式来检查电子邮件功能是否符合预期。