Как передать сеттер в другой метод в виде параметра?

Мне нужно переписать тест так, чтобы сеттер передавался в виде параметра в другой метод. Т.е. внутри теста создаю валидную книгу, беру из параметра не валидное значение, беру сеттер как функцию, и эта функция невалидное значение сетит в нужное поле книги.

Код теста:

@ParameterizedTest
  @MethodSource("provideInvalidFields")
  void saveBookShouldThrowsExceptionIfInvalidArguments(
      String field, String value, String errorMessage) throws Exception {
    var book = new Book();
    setField(book, field, value);
    var bookJson = objectMapper.writeValueAsString(book);

    mockMvc
        .perform(post("/api/v1/books").contentType(MediaType.APPLICATION_JSON).content(bookJson))
        .andExpect(status().isBadRequest())
        .andExpect(
            jsonPath("$.errorMessage")
                .value("Failed to create a new book, the request contains invalid fields"))
        .andExpect(jsonPath("$.errors[0].field").value(field))
        .andExpect(jsonPath("$.errors[0].message").value(errorMessage));

    verify(libraryService, never()).addNewBook(book);
  }

Код метода setField():

private static void setField(Book book, String field, String value) {
    switch (field) {
      case "name" -> {
        book.setName(value);
        book.setAuthor("Valid Author");
      }
      case "author" -> {
        book.setName("Valid Name");
        book.setAuthor(value);
      }
    }
}

Метод, который передает параметры, выглядит так (вставил небольшой кусок, потому что Stream довольно большой):

private static Stream<Arguments> provideInvalidFields() {
    return Stream.of(
        Arguments.of(
            "name", "x", "Book name must be longer than 5 characters, shorter than 100 characters"),
// ...
    );
}

Ответы (1 шт):

Автор решения: Nowhere Man

В данном случае сеттер можно представить как BiConsumer<Book, T>, где T -- тип поля, за которое отвечает данный сеттер.

То есть, метод setField следует переписать, чтобы он принимал ссылку на метод для типа Book Book::setFoo в качестве первого параметра, а также некий экземпляр Book и невалидное значение для сеттера.
В обобщённом виде он может выглядеть так:

private static <T> setField(BiConsumer<Book, T> setter, Book book, T value) {
    setter.accept(book, value);
}

Соответственно, понадобится отдельный метод, чтобы создавать валидный экземпляр Book, для которого будет вызываться сеттер с "плохим" значением:

// создать валидный экземпляр со всеми валидными полями
private static Book newValidBook() {
    return new Book("Valid Name", "Valid Author", "Valid Whatever"); }

@ParameterizedTest
@MethodSource("provideInvalidFields")
void saveBookShouldThrowsExceptionIfInvalidArguments(
      BiConsumer<Book, String> setter, String badValue, String errorMessage) throws Exception {
    var book = newValidBook();
    // и "испортить" поле
    setField(setter, book, badValue);
    var bookJson = objectMapper.writeValueAsString(book);
    // ...
    // основной код теста
}

Соответственно, в методе-провайдере тестовых данных вместо названия поля следует возвращать нужный сеттер / невалидное значение / сообщение об ошибке:

private static Stream<Arguments> provideInvalidFields() {
    return Stream.of(
        Arguments.of(Book::setName, "", "empty"),
        Arguments.of(Book::setName, "a", "too short"),
        Arguments.of(Book::setName, "a".repeat(1000), "too long")
        // ...
    );
}
→ Ссылка