Project Archive

Testing with JUnit

We have seen that we can use Postman to test our REST api methods. As you have no doubt experienced, testing every interaction through Postman is a bit of tedious process. In these notes I am going to demonstrate how we can write automated tests directly in the Spring Tool Suite application to test our api.

The basic tool to run tests on code in Spring is the popular JUnit framework. Automated testing with JUnit is such an integral part of Spring Boot development that every project we create will already include support for JUnit through this dependency:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-test</artifactId>
  <scope>test</scope>
</dependency>

Writing and running unit tests

The most basic type of test you can set up with JUnit is the unit test. This type of test is designed to test a single method for correctness.

Here is an example. The code below sets up a JUnit test class that is designed to test two methods in our project, the UserService.save() and the UserService.findByNameAndPassword() methods:

@SpringBootTest
@ActiveProfiles("test")
public class UnitTestExamples {
  @Autowired
  private UserService userService;
  
  static private UserDTO userdto;
  static private String userid;
  static private User user;
  
  @BeforeAll
  public static void init() {
    userdto = new UserDTO();
    userdto.setName("test user");
    userdto.setPassword("hello");
  }
  
  @Test
  public void saveNewUser() throws Exception {
    userid = userService.save(userdto);
    assertTrue(userid.length() > 0);
    System.out.println("Created user with id "+ userid);
  }
  
  @Test
  public void fetchUser() {
    user = userService.findByNameAndPassword(userdto.getName(), userdto.getPassword());
    assertNotNull(user);
    assertEquals(user.getUserid().toString(),userid);
  }
  
}

Here are some important things to note about this example:

To enable us to run this test class as a JUnit test, I have placed the test class in the src/test/java directory in place of the usual src/main/java directory. To run the test we right-click on the test class in the project explorer and select Run As/JUnit test.

Working with a test database

Another special thing that I have done to support testing in my application is to set up a separate testing database. If you look inside the project folder I have provided above you will see that there are now two database folders: one named 'database' and the other named 'test database'. These set up two separate database schemas named 'auction' and 'auctiontest'. All of the test code I am going to show today is designed to work with the 'auctiontest' database. That way our tests will be free to dump a bunch of test data into the database without messing up the main auction database.

To tell Spring Boot to use a different database for tests, we create a new properties file named 'application-test.properties' and put this code in it:

spring.application.name=jpaauction
server.port=8085
spring.datasource.url=jdbc:mysql://localhost:3306/auctiontest
spring.datasource.username=student
spring.datasource.password=Cmsc250!
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

Note that the settings here point the application at the auctiontest database and not the usual auction database. We tell Spring Boot to switch to these alternative properties by switching to a different profile. We do this by attaching the annotation

@ActiveProfiles("test")

to our test class.

When our test runs JUnit should report that both tests have succeeded. We can also look in the auctiontest database after running the test to set that there is now a new user inserted in the test database.

Mocking

Here is a second example of a test class containing a single unit test:

@SpringBootTest
@AutoConfigureMockMvc
public class MockingTestExamples {
  @MockBean
  private UserService userService;

  @Autowired
  private MockMvc mvc;

  static private UserDTO userdto;

  @BeforeAll
  public static void init() {
    userdto = new UserDTO();
    userdto.setName("test user");
    userdto.setPassword("hello");
  }

  @Test
  public void testCreateUser() throws Exception {
    when(userService.save(any())).thenReturn("fakeUUID");

    MvcResult result = mvc
        .perform(post("/users")
            .contentType(MediaType.APPLICATION_JSON)
            .content(new ObjectMapper().writeValueAsString(userdto)))
        .andExpect(status().isOk())
        .andReturn();

    String content = result.getResponse().getContentAsString();
    System.out.println(content);
  }
}

This particular test is designed to test one of our controller methods, specifically the method to post a new user.

Since our goal here is to just test that one controller method, we are going to be using a special trick called mocking. The controller method we are testing will typically do its work by calling a method in the UserService class to handle actually saving the new user. Since our goal here is to test the controller method but not test the UserService.save() method, we are going to replace the real UserService object with a fake object called a mock service. A mock object offers the same methods as the original, but the methods in the mock object do nothing and return fake results.

To set up our fake UserService we do two things. First, we annotate the UserService member variable with the @MockBean annotation. This tells JUnit that we are going to be working with a mock version of our service. The second thing is that we call some code inside the test to tell the system that when we call a particular method on our mock service it should return a particular fake result. The code

when(userService.save(any())).thenReturn("fakeUUID");

This code essentially says "When someone calls the save() method with any object as its parameter you should return the result "fakeUUID".

The other mock object we are going to be working with here is a MockMvc class. This implements a limited version of our server application that allows us to test just one of the controllers. In the test code above you see me telling the MockMvc class that we want to perform a post to the URL /users with a UserDTO object. The MockMvc class will call our method in the UsersController class to process the post. After that method returns a result we can assert that the result returns a particular status code and we can ask the test code to print the JWT that this method should return.

End to End testing with REST Assured

We have now seen a couple of examples of JUnit unit tests. If we wanted to, we could go ahead and write unit tests to test every single method across our entire project.

Rather than take such a fine-grained approach to testing, we are going to pursue an alternative strategy known commonly as "end-to-end testing". In this type of testing we test the functionality of our entire system as a whole. This means sending requests to our controllers, letting the controllers talk to real services instead of mock services, and letting the services interact with the database.

To do this type of testing we are going to be writing a bunch of tests that test individual controller methods. In the course of the examples below I am going to end up testing just about every controller method in the auction system.

To do end-to-end testing I am going to be using a library named "REST Assured". This library was designed from the ground up to do end-to-end testing with REST server applications.

To use this testing library we start by adding a dependency to our project:

<dependency>
      <groupId>io.rest-assured</groupId>
      <artifactId>rest-assured</artifactId>
</dependency>

Getting started with REST assured

Since I am going to be testing a lot of controller methods, I have broken my end-to-end test code down into three distinct classes.

The first class uses REST Assured to do some basic setup for our later tests. I am going to call the controller methods needed to create a few users in my system.

@SpringBootTest(classes=JpaauctionApplication.class,webEnvironment = WebEnvironment.DEFINED_PORT)
@ActiveProfiles("test")
public class APISetupTests {
  private static UserDTO testSeller;
  private static UserDTO testBuyerOne;
  private static UserDTO testBuyerTwo;
  
  @BeforeAll
  public static void setup() {
    RestAssured.port = 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)));
  }

}

This first set of tests follows the same basic pattern we have seen already: we do setup in a @BeforeAll method, and then run some tests. The tests are designed to insert three users into our system: a seller and two buyers.

REST Assured tests all take the following form:

given()
// Set up the request
.when()
// Run the request
.then()
// Check the response

The specifics of what happens in each of the three sub-parts here will differ from example to example. Over the course of all of the examples I will show in these notes you will be getting a pretty good sense for what the library can do.

In all three of the examples above we are doing post requests. As part of the setup we will specify the body of the request along with specifying the content type for the request. The main check we will want to perform after the response comes back is to check the response code. There are two possibilities here: if we are posting a particular user for the first time we will expect to get back a status code of 201, CREATED. Because we may very well run this test multiple times, there is also the possibility that we will be making duplicate posts. For those the controller should respond with the status code of 409, DUPLICATE.

To assert that the status code that comes back is either 201 or 409 we are going to be using Hamcrest matchers. These are special matching functions offered by the Hamcrest library (REST Assured includes that library automatically). These matching functions allow us to assert that a particular value (the status code in this case) meet a particular set of conditions.

More detailed information about Hamcrest matchers is available at hamcrest.org.

Our first actual round of tests

Now that we have tested whether or not we can post a set of users to the application, it's time for some more extensive tests. The next test class I created will test basic actions such as

Here is the code for our next test class:

@SpringBootTest(classes=JpaauctionApplication.class,webEnvironment = WebEnvironment.DEFINED_PORT)
@ActiveProfiles("test")
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class APIBasicTests {
  private static UserDTO testSeller;
  private static UserDTO testBuyerOne;
  private static UserDTO testBuyerTwo;
  private static String sellerToken;
  private static String buyerOneToken;
  private static String buyerTwoToken;
  private static String auctionID;
  
  @BeforeAll
  public static void setup() {
    RestAssured.port = 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("Test Seller");
    testSellerShipping.setAddressone("711 E Boldt Way");
    testSellerShipping.setCity("Appleton");
    testSellerShipping.setState("WI");
    testSellerShipping.setZip("54911");
    
    ProfileDTO testSellerProfile = new ProfileDTO();
    testSellerProfile.setFullname("Test Seller");
    testSellerProfile.setBio("I am a test seller");
    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 testPostAuction() {
    AuctionDTO auction = new AuctionDTO();
    auction.setItem("Laptop");
    auction.setDescription("Slightly used laptop");
    auction.setReserve(2000);
    auction.setOpens(LocalDate.now().toString());
    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);

  }

}

An important JUnit option that I had to exercise here involves ordering tests. Normally JUnit gives us no guarantees that the tests in a test class will run in any particular order. For what we are doing here we need certain things to happen in a carefully ordered sequence. For example, you can't post bids to an auction until you have first posted the auction. To control the order of our tests I have attached an @Order annotation to each test that tells JUnit the precise order to run the tests in. For the test ordering to work correctly we also need to attach the annotation

@TestMethodOrder(MethodOrderer.OrderAnnotation.class)

to the class as a whole.

The tests above include some special operations I had to perform in REST Assured. One special operation is obtaining the body of a response as a String. You will see an example of that in the testLogin() test. To obtain the body of a response we use the extract() method in the then() section of the test. If we follow up the call to extract() with a call to asString() we will get the entire body of the response as a string.

Another example of extracting data from the body of the response appears in the testGetAuctions() method. The test in this case fetches a list of all auctions currently available. We don't need to make use of the full list that comes back in the response; instead, we only want to grab the id of one of the auctions in the body. To pick out just one piece of data from the body we first use the extract() method to get the body, but then we call the path() method to extract one particular piece of data from the body. The path() method makes use of a special JsonPath syntax to pick out one item in the body. As you can see in the example, we use the path specifier "[0].auctionid" to specify that we want to pick out just the auctionid property of the first auction in the returned list of auctions. REST Assured uses an implementation of the JsonPath specification called GPath. More details about the full syntax of GPath along with examples is available here.

Another important example here is inserting an authorization header in a request. As you can see in several examples above we can do this by using the header() method in the given() section of the test.

Testing the purchase mechanism

The most complex part of the auction server is the logic to handle all of the steps involved in a purchase. To test this more complex sequence of steps I wrote a second test class:

@SpringBootTest(classes=JpaauctionApplication.class,webEnvironment = WebEnvironment.DEFINED_PORT)
@ActiveProfiles("test")
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class APIPurchaseTests {
  private static UserDTO testSeller;
  private static UserDTO testBuyer;
  private static String sellerToken;
  private static String buyerToken;
  private static String purchaseID;

  @BeforeAll
  public static void setup() {
    RestAssured.port = 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 prerequisites() {
    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/concludeauctions")
    .then().statusCode(200);
  }

  @Test
  @Order(2)
  public void processOffer() {
    // Get the buyer's offers and grab the purchase id of the first one
    purchaseID = given()
        .header("Authorization","Bearer "+ buyerToken)
        .when().get("/users/offers")
        .then()
        .statusCode(200)
        .extract()
        .path("[0].purchaseid");
    
    // Post a shipping address for the buyer
    ShippingDTO shipping = new ShippingDTO();
    shipping.setDisplayname("Eager Buyer");
    shipping.setAddressone("711 E Boldt Way");
    shipping.setCity("Appleton");
    shipping.setState("WI");
    shipping.setZip("54911");
    
    given()
    .header("Authorization","Bearer "+ buyerToken)
    .contentType("application/json")
    .body(shipping)
    .when().post("/users/shipping")
    .then()
    .statusCode(201);
    
    // Get the buyer's shipping addresses and grab the id of the first one
    int shippingid = given()
        .header("Authorization","Bearer "+ buyerToken)
        .when().get("/users/shipping")
        .then()
        .statusCode(200)
        .extract()
        .path("[0].shippingid");
      
    // Post an offer response accepting the offer
    OfferResponse or = new OfferResponse();
    or.setPurchaseid(purchaseID);
    or.setAccept(true);
    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() {
    // Get the seller's sales and grab the purchase id of the first one
    purchaseID = given()
        .header("Authorization","Bearer "+ sellerToken)
        .when().get("/users/accepted")
        .then()
        .statusCode(200)
        .extract()
        .path("[0].purchaseid");
    
    // Post a shipping charge
    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() {
    // Get the buyer's bills and grab the purchase id of the first one
    purchaseID = given()
        .header("Authorization","Bearer "+ buyerToken)
        .when().get("/users/billed")
        .then()
        .statusCode(200)
        .extract()
        .path("[0].purchaseid");
    
    // Post an bill response accepting the bill
    BillResponse br = new BillResponse();
    br.setPurchaseid(purchaseID);
    br.setAccepted(true);
    
    given()
    .header("Authorization","Bearer "+ buyerToken)
    .contentType("application/json")
    .body(br)
    .when().post("/purchases/billresponse")
    .then()
    .statusCode(202);
  }

  @Test
  @Order(5)
  public void ship() {
    // Get the seller's bills and grab the purchase id of the first one
    purchaseID = given()
        .header("Authorization","Bearer "+ sellerToken)
        .when().get("/users/sold")
        .then()
        .statusCode(200)
        .extract()
        .path("[0].purchaseid");
    
    // Post a shipping confirmation
    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);
    
    // Confirm that there is a shipping entry
    given()
    .header("Authorization","Bearer "+ buyerToken)
    .when().get("/users/shipped")
    .then()
    .statusCode(200)
    .body("$.size()", greaterThan(0));
  }

}

The REST Assured code here uses the same operations I used in the first set of tests. One new thing here appears in the very last test. That test makes use of a JsonPath assertion function to check that the body of the response is a non-empty array. To test this assertion we use the body() method. This method takes two parameters, a JsonPath expression that indicates what value from the body we want to look at, and a matcher expression that specifies what we want to assert about that value.