2 Pagination
djmil edited this page 2023-07-25 12:14:11 +02:00

Test for Data contract list

src/test/resources/djmil/cashcard/list.json

[
  { "id": 99, "amount": 123.45 },
  { "id": 100, "amount": 1.0 },
  { "id": 101, "amount": 150.0 }
]

Now open the CashCardsJsonTest.java file. And add cashCards class-level variable to contain the following Java array:

private CashCard[] cashCards;

@BeforeEach
void setUp() {
	cashCards = Arrays.array(
			new CashCard(99L, 123.45),
			new CashCard(100L, 100.00),
			new CashCard(101L, 150.00));
}

Than add the new test:

@Test
void cashCardListSerializationTest() throws IOException {
   assertThat(jsonList.write(cashCards)).isStrictlyEqualToJson("list.json");
}

@Test
void cashCardListDeserializationTest() throws IOException {
   String expected="""
         [
            { "id": 99, "amount": 123.45 },
            { "id": 100, "amount": 100.00 },
            { "id": 101, "amount": 150.00 }
         ]
         """;
   assertThat(jsonList.parse(expected)).isEqualTo(cashCards);
}

GET list

DB data

It is important to populate our test db with known data, that would reflect desired test case scenario. src/test/resource/data.sql

INSERT INTO CASH_CARD(ID, AMOUNT) VALUES (99, 123.45);
INSERT INTO CASH_CARD(ID, AMOUNT) VALUES (100, 1.00);
INSERT INTO CASH_CARD(ID, AMOUNT) VALUES (101, 150.00);

Endpoint Test

In src/test/java/djmil/cashcard/CashCardApplication.test

@Test
void shouldReturnAllCashCardsWhenListIsRequested() {
     ResponseEntity<String> response = restTemplate.getForEntity("/cashcards", String.class);
     assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);

     DocumentContext documentContext = JsonPath.parse(response.getBody());
     int cashCardCount = documentContext.read("$.length()");
     assertThat(cashCardCount).isEqualTo(3);

     JSONArray ids = documentContext.read("$..id");
     assertThat(ids).containsExactlyInAnyOrder(99, 100, 101);

     JSONArray amounts = documentContext.read("$..amount");
     assertThat(amounts).containsExactlyInAnyOrder(123.45, 100.0, 150.00);
}

Check out these new JsonPath expressions! A good place to learn more about JsonPath is the JsonPath documentation:

  • documentContext.read("$.length()") calculates the length of the array.
  • .read("$..id") retrieves the list of all id values returned, while .read("$..amount") collects all amounts returned.

Controller code

src/main/java/djmil/cashcard/CashCardController.java

@GetMapping()
public ResponseEntity<Iterable<CashCard>> findAll() {
	return ResponseEntity.ok(cashCardRepository.findAll());
}

Test Interaction and @DirtiesContext

Depending on test execution order schedule, our new shouldReturnAllCashCardsWhenListIsRequested test might not pass! Why?

[~/exercises] $ ./gradlew test
...
 org.opentest4j.AssertionFailedError:
 expected: 3
  but was: 4
     ...
     at app//example.cashcard.CashCardApplicationTests.shouldReturnAllCashCardsWhenListIsRequested(CashCardApplicationTests.java:70)

The reason is that one of the other tests is interfering with our new test by creating a new Cash Card. @DirtiesContext fixes this problem by causing Spring to start with a clean slate, as if those other tests hadn't been run. Removing it (commenting it out) from the class caused our new test to fail.

Although you can use @DirtiesContext to work around inter-test interaction, you shouldn't use it indiscriminately; you should have a good reason. Our reason here is to clean up after creating a new Cash Card.

Leave DirtiesContext commented out at the class level, and uncomment it on the method which creates a new Cash Card:

//@DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD)
class CashCardApplicationTests {
   ...

    @Test
    @DirtiesContext
    void shouldCreateANewCashCard() {
   ...

GET pagination

Test

Since we have 3 CashCards in our Pagination#DB data, Let's set up a test to fetch them one at a time (page size of 1), then have their amounts sorted from highest to lowest (descending).

@Test
void shouldReturnASortedPageOfCashCards() {
	ResponseEntity<String> response = restTemplate.getForEntity("/cashcards?page=0&size=1&sort=amount,desc", String.class);
	assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);

	DocumentContext documentContext = JsonPath.parse(response.getBody());
	JSONArray read = documentContext.read("$[*]");
	assertThat(read.size()).isEqualTo(1);

	double amount = documentContext.read("$[0].amount");
	assertThat(amount).isEqualTo(150.00);
}

The URI we are requesting contains both pagination and sorting information: /cashcards?page=0&size=1&sort=amount,desc:

  • page=0: Get the first page. Page indexes start at 0.
  • size=1: Each page has size 1.
  • sort=amount,desc

Controller

@GetMapping
public ResponseEntity<List<CashCard>> findAll(Pageable pageable) {
	Page<CashCard> page = cashCardRepository.findAll(
			PageRequest.of(
					pageable.getPageNumber(),
					pageable.getPageSize(),
					pageable.getSort()
	));
	return ResponseEntity.ok(page.getContent());
}

Pagination with defaults

Spring provides the default page and size values (they are 0 and 20, respectively). A default of 20 for page size explains why all three of our Cash Cards were returned. Again: we didn't need to explicitly define these defaults. Spring provides them "out of the box". So the test with

ResponseEntity<String> response = restTemplate.getForEntity("/cashcards", String.class);

shall work. But there are no reasonable defaults for sorting order possible. So if the one was not provided, query would return items in order defined by DB (most likely in order of creation). To deal with this limitation, we have to define the default sort parameter in our own code, by passing a Sort object to getSortOr():

PageRequest.of(
        pageable.getPageNumber(),
        pageable.getPageSize(),
        pageable.getSortOr(Sort.by(Sort.Direction.ASC, "amount"))
));