【JUnit实战3_17】第九章:容器内测试(下)——Arquillian 框架的用法简介

《JUnit in Action》全新第3版封面截图

写在前面

本篇重点介绍容器内测试的专用框架------Arquillian。作者成书之时该框架还没能全面支持 JUnit 5,因此只能沿用 JUnit 4。最新消息据说已经实现了 JUnit 5 的兼容(待学完本书后验证)。Arquillian 框架貌似解了容器场景下的燃眉之急,但从这几年的爆冷也暴露了一些问题,让其团队尝到了热脸贴冷屁股的滋味......

(接上篇)

9.4 Arquillian 框架用法简介

Arquillianhttps://arquillian.org/)是一款针对 Java 的测试框架。它利用了 JUnitJava 容器中执行测试用例。

Arquillian 框架主要分为三个核心部分:

  • 测试运行器(Test runners) :由 JUnit 测试框架提供;
  • 容器(Containers) :如 WildFlyTomcatGlassFishJetty 等;
  • 测试增强工具(Test enrichers) :负责将容器资源和各种 Bean 直接注入到测试类中。

遗憾的是,该书出版五年后的今天,Arquillian 框架仍然没有与 JUnit 5 实现完美集成,相关演示只能在 JUnit 4 中进行。

Arquillian 框架使用 ShrinkWrap 这一外部依赖提供的流畅 API 接口完成归档文件的组装工作(如组装成 jarwarear 文件等),并在测试期间由 Arquillian 直接部署。

本节演示了一个航班与乘客管理的模拟场景,航班对象可以动态添加或删除乘客集合中的元素,并通过该航班的总座位数对乘客总数进行限制。航班中的乘客数据以 HashSet<Passenger> 的形式存在,并从一个 CSV 文件中完成初始化。具体情况如下。

首先添加所需的 Maven 依赖:

xml 复制代码
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.jboss.arquillian</groupId>
            <artifactId>arquillian-bom</artifactId>
            <version>1.4.0.Final</version>
            <scope>import</scope>
            <type>pom</type>
        </dependency>
    </dependencies>
</dependencyManagement>
<dependencies>
    <dependency>
        <groupId>org.jboss.spec</groupId>
        <artifactId>jboss-javaee-7.0</artifactId>
        <version>1.0.3.Final</version>
        <type>pom</type>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>org.junit.vintage</groupId>
        <artifactId>junit-vintage-engine</artifactId>
        <version>5.9.2</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.jboss.arquillian.junit</groupId>
        <artifactId>arquillian-junit-container</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.jboss.arquillian.container</groupId>
        <artifactId>arquillian-weld-ee-embedded-1.1</artifactId>
        <version>1.0.0.CR9</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.jboss.weld</groupId>
        <artifactId>weld-core</artifactId>
        <version>2.4.8.Final</version>
        <scope>test</scope>
    </dependency>
</dependencies>

注意 :由于本地实测距图书出版时相隔近五年,为了消除 IDEA 提示的易遭攻击风险,JUnit 版本最好升至 5.9.2weld-core 的版本提升到 2.4.8.Final。同时为了消除 JDK11 限制使用 Java 反射机制的警告,可以按照运行提示修改如下插件配置:

xml 复制代码
<plugins>
    <plugin>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>2.22.2</version>
        <configuration>
            <argLine>
                --add-opens java.base/java.lang=ALL-UNNAMED
                --add-opens java.base/java.security=ALL-UNNAMED
                --add-opens java.base/java.io=ALL-UNNAMED
                --add-opens java.base/java.util=ALL-UNNAMED
            </argLine>
        </configuration>
    </plugin>
</plugins>

Passenger 乘客实体类:

java 复制代码
public class Passenger {

    private String identifier;
    private String name;

    public Passenger(String identifier, String name) {
        this.identifier = identifier;
        this.name = name;
    }

    public String getIdentifier() {
        return identifier;
    }

    public String getName() {
        return name;
    }

    @Override
    public String toString() {
        return "Passenger " + getName() + " with identifier: " + getIdentifier();
    }
}

Flight 航班实体类:

java 复制代码
public class Flight {

    private String flightNumber;
    private int seats;
    Set<Passenger> passengers = new HashSet<>();

    public Flight(String flightNumber, int seats) {
        this.flightNumber = flightNumber;
        this.seats = seats;
    }

    public String getFlightNumber() {
        return flightNumber;
    }

    public int getSeats() {
        return seats;
    }

    public void setSeats(int seats) {
        if (passengers.size() > seats) {
            throw new RuntimeException("Cannot reduce seats under the number of existing passengers!");
        }
        this.seats = seats;
    }

    public int getNumberOfPassengers() {
        return passengers.size();
    }

    public boolean addPassenger(Passenger passenger) {
        if (passengers.size() >= seats) {
            throw new RuntimeException("Cannot add more passengers than the capacity of the flight!");
        }
        return passengers.add(passenger);
    }

    public boolean removePassenger(Passenger passenger) {
        return passengers.remove(passenger);
    }

    @Override
    public String toString() {
        return "Flight " + getFlightNumber();
    }
}

乘客集合的初始化通过一个静态工具方法实现,需要从一个 CSV 文件 flights_information.csv 读取:

markdown 复制代码
1236789; John Smith
9006789; Jane Underwood
1236790; James Perkins
9006790; Mary Calderon
1236791; Noah Graves
9006791; Jake Chavez
1236792; Oliver Aguilar
9006792; Emma McCann
1236793; Margaret Knight
9006793; Amelia Curry
1236794; Jack Vaughn
9006794; Liam Lewis
1236795; Olivia Reyes
9006795; Samantha Poole
1236796; Patricia Jordan
9006796; Robert Sherman
1236797; Mason Burton
9006797; Harry Christensen
1236798; Jennifer Mills
9006798; Sophia Graham

对应的工具类代码如下:

java 复制代码
public class FlightBuilderUtil {
    public static Flight buildFlightFromCsv() throws IOException {
        Flight flight = new Flight("AA1234", 20);
        try (BufferedReader reader = new BufferedReader(new FileReader("src/test/resources/flights_information.csv"))) {
            String line = null;
            do {
                line = reader.readLine();
                if (line != null) {
                    String[] passengerString = line.toString().split(";");
                    Passenger passenger = new Passenger(passengerString[0].trim(), passengerString[1].trim());
                    flight.addPassenger(passenger);
                }
            } while (line != null);

        }
        return flight;
    }
}

最终的 Arquillian 测试类如下:

java 复制代码
@RunWith(Arquillian.class)
public class FlightWithPassengersTest {

    @Deployment
    public static JavaArchive createDeployment() {
        return ShrinkWrap.create(JavaArchive.class)
                .addClasses(Passenger.class, Flight.class, FlightProducer.class)
                .addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml");
    }

    @Inject
    Flight flight;

    @Test(expected = RuntimeException.class)
    public void testNumberOfSeatsCannotBeExceeded() throws IOException {
        assertEquals(20, flight.getNumberOfPassengers());
        flight.addPassenger(new Passenger("1247890", "Michael Johnson"));
    }

    @Test
    public void testAddRemovePassengers() throws IOException {
        flight.setSeats(21);
        Passenger additionalPassenger = new Passenger("1247890", "Michael Johnson");
        flight.addPassenger(additionalPassenger);
        assertEquals(21, flight.getNumberOfPassengers());
        flight.removePassenger(additionalPassenger);
        assertEquals(20, flight.getNumberOfPassengers());
        assertEquals(21, flight.getSeats());
    }
}

上述代码中,相关组件的打包通过 @Deployment 注解的方法完成,具体由 ShrinkWrap 相关 API 实现。最初没有 FlightProducer.class 这个类(L7),但由于首次运行时 Arquillian 无法顺利注入 Flight 实例(仅支持无参构造函数):

因此需要利用 JavaEE 中的 CDIContext & Dependency Injection)机制,手动注入 Flight 实例,通过新增一个带 @Produces 注解方法的普通工具类:

java 复制代码
// FlightProducer.java
import javax.enterprise.inject.Produces;

public class FlightProducer {
    @Produces
    public Flight createFlight() throws IOException {
        return FlightBuilderUtil.buildFlightFromCsv();
    }
}

最后再将这个 FlightProducer 类一并打包到归档文件中即可(L4):

java 复制代码
@Deployment
public static JavaArchive createDeployment() {
    return ShrinkWrap.create(JavaArchive.class)
            .addClasses(Passenger.class, Flight.class, FlightProducer.class)
            .addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml");
}

最终实测截图:

后话
Arquillian 官方文档貌似很长时间没有更新了,里面的一些示例还用的是 Eclipse 作展示,可见近年来并没有想象中的那么受欢迎。出发点很好、但好心办坏事的情况也比比皆是,本就不受重视的测试环节,为了贴近容器的真实环境还得搭一堆脚手架一样的东西,使用时又得改配置又得创建工具类,实在是不讨喜。因此本章只作为了解基本理念的拓展阅读即可,不必过于纠结。

相关推荐
晷昃15 小时前
抓包工具:proxyman的使用方法
测试工具
程序员杰哥19 小时前
Pytest之收集用例规则与运行指定用例
自动化测试·软件测试·python·测试工具·职场和发展·测试用例·pytest
平淡是真_1 天前
软件测试(五)--自动化测试Selenium(一)
selenium·测试工具
Run Freely9371 天前
selenium_web自动化测试_02_元素操作
selenium·测试工具
虫无涯1 天前
解锁 Playwright 自动化测试:一篇教程入门WebUI自动化测试【入门级】
python·单元测试·测试
newxtc1 天前
【重庆政务服务网-注册_登录安全分析报告】
人工智能·selenium·测试工具·安全·政务
安冬的码畜日常1 天前
【JUnit实战3_09】第五章:软件测试的基本原则简介
功能测试·测试工具·junit·单元测试·junit5