项目档案

使用JUnit进行测试

我们已经看到可以使用Postman来测试REST api方法。正如您无疑经历过的那样,通过Postman测试每个交互是一个有点乏味的过程。在这些笔记中,我将演示如何在Spring Tool Suite赢博体育程序中直接编写自动化测试来测试我们的api。

在Spring中对代码运行测试的基本工具是流行的JUnit框架。使用JUnit的自动化测试是Spring Boot开发中不可或缺的一部分,我们创建的每个项目都将通过这个依赖项包含对JUnit的支持:

<依赖> < groupId > org.springframework。boot</groupId> <artifactId>spring-boot-start -test</artifactId> <scope>test</scope> </dependency>

编写和运行单元测试

可以用JUnit设置的最基本的测试类型是单元测试。这种类型的测试用于测试单个方法的正确性。

这里有一个例子。下面的代码设置了一个JUnit测试类,用于测试我们项目中的两个方法,UserService.save()和UserService.findByNameAndPassword()方法:

@SpringBootTest @ActiveProfiles("test")公共类UnitTestExamples {@Autowired私有UserService UserService;静态私有UserDTO;静态私有字符串userid;static private User User;@BeforeAll public static void init() {userdto = new userdto ();userdto。setName(“测试用户”);userdto.setPassword(“hello”);} @Test public void saveNewUser()抛出异常{userid = userService.save(userdto);assertTrue(userid.length() >0 0);system . out。println(“已创建的用户id ”+ userid);} @Test public void fetchUser() {user = userService.findByNameAndPassword(userdto.getName(), userdto.getPassword());assertNotNull(用户);assertequal (user.getUserid () .toString (), userid);}}

以下是关于这个例子需要注意的一些重要事项:

为了使我们能够将这个测试类作为JUnit测试运行,我将测试类放在src/test/java目录中,而不是通常的src/main/java目录中。要运行测试,我们在项目资源管理器中右键单击测试类,并选择run As/JUnit test。

使用测试数据库

为了支持赢博体育程序中的测试,我所做的另一件特殊的事情是设置一个单独的测试数据库。如果你查看我上面提供的项目文件夹,你会看到现在有两个数据库文件夹:一个名为“database”,另一个名为“test database”。这些代码设置了两个独立的数据库模式,名为“auction”和“auctiontest”。我今天要展示的赢博体育测试代码都是为使用“auctiontest”数据库而设计的。这样,我们的测试就可以自由地将一堆测试数据转储到数据库中,而不会弄乱主拍卖数据库。

为了告诉Spring Boot使用不同的数据库进行测试,我们创建了一个名为“application-test”的新属性文件。Properties’,然后把下面的代码放进去:

spring.application.name = jpaauction服务器。= 8085 spring.datasource港。url = jdbc: mysql: / / localhost: 3306 / auctiontest spring.datasource。用户名=学生spring.datasource.password = Cmsc250 !spring.datasource.driver-class-name = com.mysql.cj.jdbc.Driver

注意,这里的设置将赢博体育程序指向auctiontest数据库,而不是通常的拍卖数据库。我们告诉Spring Boot通过切换到不同的配置文件来切换到这些可选属性。我们通过附加注释来实现这一点

@ActiveProfiles(“测试”)

我们的测试班。

当我们的测试运行时,JUnit应该报告两个测试都成功了。我们还可以在运行测试后查看auctiontest数据库,以设置现在有一个新用户插入到测试数据库中。

嘲笑

下面是包含单个单元测试的测试类的第二个例子:

@SpringBootTest @AutoConfigureMockMvc公共类MockingTestExamples {@MockBean私有UserService UserService;@Autowired私有MockMvc mvc;静态私有UserDTO;@BeforeAll public static void init() {userdto = new userdto ();userdto。setName(“测试用户”);userdto.setPassword(“hello”);} @Test public void testCreateUser()抛出Exception {when(userService.save(any())).thenReturn("fakeUUID");MvcResult result = mvc .perform(post("/users") .contentType(MediaType.APPLICATION_JSON) .content(new ObjectMapper().writeValueAsString(userdto)) . anddexpect (status().isOk()) .andReturn();字符串内容= result.getResponse().getContentAsString();System.out.println(内容);}}

这个特殊的测试设计用于测试我们的一个控制器方法,特别是发布新用户的方法。

由于我们这里的目标只是测试一个控制器方法,因此我们将使用一种称为mock的特殊技巧。我们正在测试的控制器方法通常会通过调用UserService类中的方法来处理实际保存新用户的工作。由于我们这里的目标是测试控制器方法,而不是测试UserService.save()方法,因此我们将用一个称为模拟服务的假对象替换真正的UserService对象。模拟对象提供与原始对象相同的方法,但是模拟对象中的方法什么也不做,并且返回假结果。

要设置我们的假UserService,我们需要做两件事。首先,我们用@MockBean注释UserService成员变量。这告诉JUnit,我们将使用服务的模拟版本。第二件事是,我们在测试中调用一些代码来告诉系统,当我们调用模拟服务上的特定方法时,它应该返回一个特定的假结果。的代码

当(userService.save (())) .thenReturn(“fakeUUID”);

这段代码本质上说“当有人以任何对象作为参数调用save()方法时,您应该返回结果“fakeUUID”。

我们在这里要处理的另一个模拟对象是MockMvc类。这实现了服务器赢博体育程序的一个限制版本,允许我们只测试其中一个控制器。在上面的测试代码中,您可以看到我告诉MockMvc类,我们希望使用UserDTO对象对URL /users执行post。MockMvc类将调用UsersController类中的方法来处理post。在该方法返回结果之后,我们可以断言该结果返回一个特定的状态码,并且我们可以要求测试代码打印该方法应该返回的JWT。

端到端测试与REST放心

现在我们已经看到了几个JUnit单元测试的例子。如果我们愿意,我们可以继续编写单元测试来测试整个项目中的每个方法。

与其采用这种细粒度的方法进行测试,我们将采用一种通常被称为“端到端测试”的替代策略。在这种类型的测试中,我们测试整个系统的整体功能。这意味着向我们的控制器发送请求,让控制器与真实服务(而不是模拟服务)通信,并让服务与数据库交互。

要做这种类型的测试我们会写一些测试去测试单个控制器功能。在下面的例子中,我将测试拍卖系统中的每一种控制器方法。

为了进行端到端测试,我将使用一个名为“REST Assured”的库。这个库从头开始设计,用于对REST服务器赢博体育程序进行端到端测试。

要使用这个测试库,我们首先要在项目中添加一个依赖项:

<依赖> < groupId > io。rest-assured</groupId> <artifactId>rest-assured</artifactId> </dependency>

从REST保证开始

由于我将测试许多控制器方法,因此我将端到端测试代码分解为三个不同的类。

第一个类使用REST Assured为我们后面的测试做一些基本的设置。我将调用在系统中创建一些用户所需的控制器方法。

@SpringBootTest(classes=JpaauctionApplication.class,webEnvironment = webEnvironment . defined_port) @ActiveProfiles("test")公共类APISetupTests{私有静态UserDTO testSeller;私有静态UserDTO testBuyerOne;私有静态UserDTO testBuyerTwo;@BeforeAll公共静态无效设置(){RestAssured。端口= 8085;RestAssured。baseURI = "http://localhost";testSeller = new UserDTO();testSeller.setName(“TestSeller”);testSeller.setPassword(“hello”);testBuyerOne = new UserDTO();testBuyerOne.setName(“BuyerOne”);testBuyerOne.setPassword(“hello”);testBuyerTwo = new UserDTO();testBuyerTwo.setName(“BuyerTwo”);testBuyerTwo.setPassword(“hello”);} @Test public void postSeller() {given() .contentType("application/json") .body(testSeller) .when().post("/users") .then() .statusCode(anyOf(is(201),is(409))));} @Test public void postBuyers() {given() .contentType("application/json") .body(testBuyerOne) .when().post("/users") .then() .statusCode(anyOf(is(201),is(409)));given() .contentType("application/json") .body(testBuyerTwo) .when().post("/users") .then() .statusCode(anyOf(is(201),is(409)));}}

第一组测试遵循我们已经看到的基本模式:我们在@BeforeAll方法中进行设置,然后运行一些测试。测试的目的是在我们的系统中插入三个用户:一个卖家和两个买家。

REST Assured测试都采用以下形式:

given() //设置请求。when() //运行请求。then() //检查响应

这里的三个子部分中发生的具体情况将因示例而异。通过我在这些讲义中展示的赢博体育例子,你将对库的功能有一个很好的了解。

在上面的三个例子中,我们都在做post request。作为设置的一部分,我们将指定请求的主体以及指定请求的内容类型。在响应返回后,我们想要执行的主要检查是检查响应代码。这里有两种可能性:如果我们第一次发布一个特定的用户,我们将期望返回一个状态码201,CREATED。因为我们很可能多次运行这个测试,所以也有可能生成重复的帖子。对于这些,控制器应该响应状态码409,DUPLICATE。

要断言返回的状态码是201或409,我们将使用Hamcrest匹配器。这些是由Hamcrest库提供的特殊匹配函数(REST Assured自动包含了该库)。这些匹配函数允许我们断言一个特定的值(本例中的状态码)满足一组特定的条件。

有关Hamcrest匹配器的更多详细信息,请访问hamcrest.org。

我们的第一轮测试

现在我们已经测试了是否可以将一组用户发布到赢博体育程序中,接下来是进行更广泛的测试的时候了。我创建的下一个测试类将测试基本的操作,例如

下面是我们下一个测试类的代码:

@SpringBootTest(classes=JpaauctionApplication.class,webEnvironment = webEnvironment . defined_port) @ActiveProfiles("test") @TestMethodOrder(MethodOrderer.OrderAnnotation.class)公共类APIBasicTests{私有静态UserDTO testSeller;私有静态UserDTO testBuyerOne;私有静态UserDTO testBuyerTwo;私有静态字符串sellerToken;私有静态字符串buyerOneToken;私有静态字符串buyerTwoToken;private static String auctionID;@BeforeAll公共静态无效设置(){RestAssured。端口= 8085;RestAssured。baseURI = "http://localhost";testSeller = new UserDTO();testSeller.setName(“TestSeller”);testSeller.setPassword(“hello”);testBuyerOne = new UserDTO();testBuyerOne.setName(“BuyerOne”);testBuyerOne.setPassword(“hello”);testBuyerTwo = new UserDTO();testBuyerTwo.setName(“BuyerTwo”);testBuyerTwo.setPassword(“hello”);} @Test @Order(1) public void testLogin() {sellerToken = given() .contentType("application/json") .body(testSeller) .when().post("/users/login") .then() .statusCode(200) .extract().asString();buyerOneToken = given() .contentType("application/json") .body(testBuyerOne) .when().post("/users/login") .then() .statusCode(200) .extract().asString();buyerTwoToken = given() .contentType("application/json") .body(testBuyerTwo) .when().post("/users/login") .then() .statusCode(200) .extract().asString();} @Test @Order(2) public void testPostProfile() {ShippingDTO testSellerShipping = new ShippingDTO();testSellerShipping。setDisplayname(“测试卖方”);testSellerShipping。setAddressone(" 711e Boldt Way");testSellerShipping.setCity(“阿普尔顿”);testSellerShipping.setState (WI);testSellerShipping.setZip(“54911”);testSellerProfile = new ProfileDTO();testSellerProfile。setFullname(“测试卖方”);testSellerProfile。setBio(“我是一名测试销售员”);testSellerProfile.setEmail(“seller@sales.com”);testSellerProfile.setPhone(“9205551212”);testSellerProfile.setShipping (testSellerShipping);given() .header("Authorization","Bearer "+sellerToken) .contentType("application/json") .body(testSellerProfile) .when().post("/users/profile") .then() .statusCode(anyOf(is(201),is(409)));} @Test @Order(3) public void testpostuction () {AuctionDTO auction = new AuctionDTO();auction.setItem(“笔记本电脑”);拍卖。setDescription(“少量使用的笔记本电脑”);auction.setReserve (2000);.toString auction.setOpens (LocalDate.now () ());auction.setCloses (LocalDate.now () .plusDays (4) .toString ());given() .header("Authorization","Bearer "+sellerToken) .contentType("application/json") .body(auction) .when().post("/auctions") .then() .statusCode(anyOf(is(201),is(409)));} @Test @Order(4) public void testGetAuctions() {auctionID = when() .get("/auctions") .then() .statusCode(200) .extract() .path("[0].auctionid");System.out.println (auctionID);} @Test @Order(5) public void testPlaceBids() {BidDTO bid = new BidDTO();bid.setBid (2500);given() .header("Authorization","Bearer "+buyerOneToken) .contentType("application/json") .body(bid) .when() .post("/auctions/"+auctionID+"/bids") .then() .statusCode(201);bid.setBid (3000);given() .header("Authorization","Bearer "+buyerTwoToken) .contentType("application/json") .body(bid) .when() .post("/auctions/"+auctionID+"/bids") .then() .statusCode(201);}}

这里我必须使用的一个重要的JUnit选项涉及对测试进行排序。通常,JUnit不保证测试类中的测试将以任何特定的顺序运行。对于我们在这里所做的,我们需要某些事情以一个小心有序的顺序发生。例如,在你第一次发布拍卖之前,你不能对拍卖发布出价。为了控制测试的顺序,我给每个测试附加了一个@Order注释,它告诉JUnit运行测试的精确顺序。为了使测试排序正常工作,我们还需要附加注释

@TestMethodOrder (MethodOrderer.OrderAnnotation.class)

对整个班级来说。

上面的测试包括我必须在REST Assured中执行的一些特殊操作。一个特殊的操作是将响应的主体作为字符串获取。您将在testLogin()测试中看到一个示例。为了获得响应的主体,我们在测试的then()部分使用extract()方法。如果我们在调用extract()之后再调用asString(),我们将得到整个响应的字符串。

从响应体提取数据的另一个示例出现在testGetAuctions()方法中。本例中的测试获取当前可用的赢博体育拍卖的列表。我们不需要使用回复中返回的完整列表;相反,我们只想获取主体中一个拍卖的id。为了从主体中只提取一段数据,我们首先使用extract()方法获取主体,然后调用path()方法从主体中提取一段特定的数据。path()方法使用特殊的JsonPath语法从主体中挑选出一项。正如您在示例中看到的,我们使用了路径说明符“[0]”。Auctionid”,指定我们只想在返回的拍卖列表中挑选出第一个拍卖的Auctionid属性。REST Assured使用JsonPath规范的实现GPath。关于GPath完整语法的更多细节以及示例可以在这里获得。

这里的另一个重要示例是在请求中插入授权标头。正如您在上面的几个示例中看到的那样,我们可以通过在测试的给定()部分中使用header()方法来实现这一点。

测试购买机制

拍卖服务器中最复杂的部分是处理购买中涉及的赢博体育步骤的逻辑。为了测试这个更复杂的步骤序列,我编写了第二个测试类:

@SpringBootTest(classes=JpaauctionApplication.class,webEnvironment = webEnvironment . defined_port) @ActiveProfiles("test") @TestMethodOrder(MethodOrderer.OrderAnnotation.class)公共类APIPurchaseTests{私有静态UserDTO testSeller;私有静态UserDTO testBuyer;私有静态字符串sellerToken;私有静态字符串buyerToken;私有静态字符串purchaseID;@BeforeAll公共静态无效设置(){RestAssured。端口= 8085;RestAssured。baseURI = "http://localhost";testSeller = new UserDTO();testSeller.setName(“TestSeller”);testSeller.setPassword(“hello”);testBuyer = new UserDTO();testBuyer.setName(“BuyerTwo”);testBuyer.setPassword(“hello”);} @Test @Order(1) public void先决条件(){sellerToken = given() .contentType("application/json") .body(testSeller) .when().post("/users/login") .then() .statusCode(200) .extract().asString();buyerToken = given() .contentType("application/json") .body(testBuyer) .when().post("/users/login") .then() .statusCode(200) .extract().asString();given() .header("Authorization","Bearer "+sellerToken) .when().get("/purchases/ condations ") .then().statusCode(200);} @Test @Order(2) public void processOffer(){//获取买方的报价并获取第一个的购买id purchaseID = given() .header("Authorization","Bearer "+ buyerToken) .when().get("/users/offers") .then() .statusCode(200) .extract() .path("[0].purchaseid");//发送买家的收货地址shipping = new ShippingDTO();航运。setDisplayname(“急切的买家”);航运。setAddressone(" 711e Boldt Way");shipping.setCity(“阿普尔顿”);shipping.setState (WI);shipping.setZip(“54911”);given() .header("Authorization","Bearer "+ buyerToken) .contentType("application/json") .body(shipping) .when().post("/users/shipping") .then() .statusCode(201);//获取买方的送货地址并获取第一个的id int shippingid = given() .header("Authorization","Bearer "+ buyerToken) .when().get("/users/shipping") .then() .statusCode(200) .extract() .path("[0].shippingid");//发布一个offer响应,接受offer OfferResponse or = new OfferResponse();or.setPurchaseid (purchaseID);or.setAccept(真正的);or.setShippingid (shippingid);given() .header("Authorization","Bearer "+ buyerToken) .contentType("application/json") .body(or) .when().post("/purchases/offerresponse") .then() .statusCode(202);} @Test @Order(3) public void chargeShipping(){//获取卖家的销售并获取第一个的购买id purchaseID = given() .header("Authorization","Bearer "+ sellerToken) .when().get("/users/accepted") .then() .statusCode(200) .extract() .path("[0].purchaseid");//发送运费ShippingCharge sc = new ShippingCharge();sc.setPurchaseid (purchaseID);sc.setCharge (599);given() .header("Authorization","Bearer "+ sellerToken) .contentType("application/json") .body(sc) .when().post("/purchases/billshipping") .then() .statusCode(202);} @Test @Order(4) public void processBill(){//获取买方的账单并获取第一个的购买id purchaseID = given() .header("Authorization","Bearer "+ buyerToken) .when().get("/users/billed") .then() .statusCode(200) .extract() .path("[0].purchaseid");//发送一个接受账单的账单响应BillResponse br = new BillResponse();br.setPurchaseid (purchaseID);br.setAccepted(真正的);given() .header("Authorization","Bearer "+ buyerToken) .contentType("application/json") .body(br) .when().post("/purchases/billresponse") .then() .statusCode(202);} @Test @Order(5) public void ship(){//获取卖方的账单并获取第一个的购买id purchaseID = given() .header("Authorization","Bearer "+ sellerToken) .when().get("/users/sold") .then() .statusCode(200) .extract() .path("[0].purchaseid");//发送发货确认ShippingConfirmation sc = new ShippingConfirmation();sc.setPurchaseid (purchaseID);sc.setTracking(“ZZZ-ZZ-ZZZ”);given() .header("Authorization","Bearer "+ sellerToken) .contentType("application/json") .body(sc) .when().post("/purchases/confirmshipping") .then() .statusCode(202);//确认有一个发货条目给定().header("Authorization","Bearer "+ buyerToken) .when().get("/users/shipped") .then() .statusCode(200) .body("$.size()", greaterThan(0));}}

这里的REST保证代码使用了我在第一组测试中使用的相同操作。在最后的测试中出现了一件新事情。该测试使用JsonPath断言函数来检查响应体是否为非空数组。为了测试这个断言,我们使用body()方法。该方法接受两个参数,一个是JsonPath表达式,它指示我们想要查看主体中的哪个值,另一个是matcher表达式,它指定我们想要断言该值的内容。