Метакласс Groovy переопределяет java.time.Статический метод Year now(), но продуктивный код не использует переопределенное поведение now() во время выполнения

#testing #groovy #overriding #runtime #spock

#тестирование #groovy #переопределение #время выполнения #спок

Вопрос:

Я попытался переопределить java.time.Общедоступный статический метод now() класса Year, потому что я хочу, чтобы он возвращал другое значение, а не только текущий год. В моем производительном коде у меня есть метод, который я тестирую, и он использует метод Year.now() как:

Тестируемый класс Java

 public class SomeClass {

  public LocalDate methodUnderTest() {
    return Year.now()
        .atMonth(JANUARY)
        .atDay(1);
  }

}

 

В тесте Spock groovy я попытался переопределить метод java.time.Year.now() таким образом:
Я предполагаю, что переопределенное поведение статического метода now() используется в моем производительном коде, в методе methodUnderTest()

 class SomeClassSpec extends Specification {

  @Subject
  SomeClass classUnderTest = new SomeClass()

  def "test method with overriden behaviour"() {

    setup:
    Year.metaClass.'static'.now = { -> Year.of(year) }

    expect:
    classUnderTest .methodUnderTest() == expectedDate

    where:
    year           || expectedDate
    2019           || LocalDate.of(2019, JANUARY, 1)
    2020           || LocalDate.of(2020, JANUARY, 1)
    2021           || LocalDate.of(2021, JANUARY, 1)
  }
}
 

Однако тест работает только для 2020 года (текущего года), остальные 2 значения 2019 и 2021 не работают, потому что кажется, что предполагаемое переопределенное поведение явно не используется в производительном коде.
Почему предполагаемое переопределенное поведение не используется в производительном коде methodUnderTest() во время выполнения?

Ответ №1:

Некоторые основы:

  1. Метакласс Groovy работает только для классов Groovy, а не для классов Java. Т.Е. Класс Java ничего не знает о каких-либо определенных переопределениях метакласса, только Groovy, вызывающий ваш код класса Java.
  2. Класс начальной загрузки JRE java.time.Year является final , т. Е. Вы не можете обычными средствами создавать фиктивные экземпляры. Вам нужно будет отменить финализацию класса с помощью специального загрузчика классов при его загрузке. Мой собственный инструмент Sarek, который все еще находится в стадии разработки, предлагает эту функцию. Другие, такие как PowerMock, предоставляют более косвенные способы помочь вам заглушить конечные классы, используя инструменты классов, которые их вызывают.
  3. Метод Year.now() , который вы хотите отключить, находится не только в final классе, но и static . У Spock нет встроенных средств для отключения статических методов, кроме методов Groovy. Опять же, ваш метод находится в классе Java и также вызывается классом Java, так что это вам не поможет. Опять же, Sarek или другие инструменты, такие как PowerMock, могут вам помочь.

Вот небольшой пример для выполнения этого в Sarek. Я только что отправил снимок в Maven Central для вас, так что вы должны быть в состоянии использовать это.

     <dependency>
      <groupId>dev.sarek</groupId>
      <artifactId>sarek</artifactId>
      <version>1.0-SNAPSHOT</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>dev.sarek</groupId>
      <artifactId>sarek-spock-extension</artifactId>
      <version>1.0-SNAPSHOT</version>
      <scope>test</scope>
    </dependency>
 
 package de.scrum_master.stackoverflow.q65321086;

import java.time.LocalDate;
import java.time.Year;

import static java.time.Month.JANUARY;

public class SomeClass {
  public LocalDate methodUnderTest() {
    return Year.now()
      .atMonth(JANUARY)
      .atDay(1);
  }
}
 
 package de.scrum_master.stackoverflow.q65321086

import dev.sarek.agent.mock.MockFactory
import spock.lang.Specification
import spock.lang.Subject
import spock.lang.Unroll

import java.time.LocalDate
import java.time.Year

import static java.time.Month.JANUARY
import static net.bytebuddy.matcher.ElementMatchers.named

class SomeClassTest extends Specification {
  @Subject
  SomeClass classUnderTest = new SomeClass()

  @Unroll
  def "override static JRE method Year.now() for #year"() {
    setup:
    MockFactory<Year> mockFactory = MockFactory
      .forClass(Year.class)
      .mockStatic(
        named("now"),
        { method, args -> false },
        { method, args, proceedMode, returnValue, throwable -> Year.of(year) }
      )
      .build()

    expect:
    classUnderTest.methodUnderTest() == expectedDate

    cleanup:
    mockFactory.close()

    where:
    year || expectedDate
    2019 || LocalDate.of(2019, JANUARY, 1)
    2020 || LocalDate.of(2020, JANUARY, 1)
    2021 || LocalDate.of(2021, JANUARY, 1)
  }
}
 

Этот тест проходит для меня. Для Sarek пока нет подробной документации или руководства, кроме довольно хорошего JavaDoc (проверьте исходные тексты) и множества примеров тестов в разных модулях.

Что касается Sarek, я планирую интегрировать его в Spock еще лучше, чем сейчас, поэтому его использование в будущем будет выглядеть как издевательство над «spocky». Я просто был очень занят в последнее время. Но он уже полностью пригоден для использования, а также поставляется с интеграциями в JUnit 4, Junit 5, TestNG.

Комментарии:

1. Обновление: ранее я забыл загрузить снимок Sarek в Maven Central, потому что у меня были некоторые проблемы с аутентификацией, которые теперь исправлены. Теперь все готово, если вы хотите использовать Sarek.