6

I'm struggling on testing my endpoint when I set a specific date.

I don't want to use PowerMock to mock a static method instead I decided to change the implementation of my service and use the LocalDate.now(Clock clock) implementation in the way to be easier to test it.

I added to my SpringBootApplication class:

@Bean
public Clock clock() {
    return Clock.systemDefaultZone();
}

and autowired it to my Service

@Autowired
private Clock clock;

and used it in my implementation as that:

LocalDateTime localDate = LocalDateTime.now(clock);

On the test side I mocked the Clock

private final static LocalDate WEEKEND = LocalDate.of(2020, 07, 05);

@Mock
private Clock clock;
private Clock fixedClock;

and used it as that:

MockitoAnnotations.initMocks(this);

//tell your tests to return the specified LOCAL_DATE when calling LocalDate.now(clock)
fixedClock = Clock.fixed(WEEKEND.atTime(9, 5).toInstant(ZoneOffset.UTC), ZoneId.of("CET"));
doReturn(fixedClock.instant()).when(clock).instant();
doReturn(fixedClock.getZone()).when(clock).getZone();

ResponseEntity<String> response = restTemplate.postForEntity(base.toString(), request, String.class);

When I debuged it, the fixedClock has the value which I expected FixedClock[2020-07-05T09:05:00Z,CET]. Instead if I put a breakpoint on the service implementantion, the localDate variable has the value 2020-07-09 - the .now().

My issue is that: why the localDate variable hasn't the value of fixedClock variable?

Thank you very much for your time!

Later edit:

Here is the constructor of the Service:

@Autowired
  public SavingAccountService(
      SavingAccountRepository savingAccountRepository, UserRepository userRepository, Clock clock) {
    this.savingAccountRepository = savingAccountRepository;
    this.userRepository = userRepository;
    this.clock = clock;
  }

The annotations on my TestClass:

RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = RANDOM_PORT, classes = ChallengeApplication.class)
@ActiveProfiles("test")
public class SavingAccountTest {

    @Mock
    private Clock clock;
    private Clock fixedClock;

    @InjectMocks
    private SavingAccountService savingAccountService;

    @Autowired
    private TestRestTemplate restTemplate;
    private URL base;

    @LocalServerPort
    int port;

I want also to mention that from my test I'm calling the Controller and not the Service.

private final SavingAccountService savingAccountService;   

public SavingAccountRestController(SavingAccountService savingAccountService) {
    this.savingAccountService = savingAccountService;
}   

@Override   
@PostMapping   
public ResponseEntity<?> newSavingAccount(@RequestBody SavingAccount savingAccount) {
     EntityModel<SavingAccount> newSavingAccount = savingAccountService.newSavingAccount(savingAccount);
     return new ResponseEntity<>(newSavingAccount, HttpStatus.CREATED);
}
Lesiak
  • 22,088
  • 2
  • 41
  • 65
liviubiur
  • 111
  • 3
  • 13
  • Most likely, you inject your mock in the wrong way. Show us: the constructor of the service, annotations on the test class and how you instantiate the service in your test (annotation or manual instantiation?). On top of that: I would try to eliminate the mocked clock in this scenario. – Lesiak Jul 09 '20 at 20:40
  • Thanks for your answer, @Lesiak! I added the request information on the first post. – liviubiur Jul 09 '20 at 20:52

1 Answers1

6

The problem

You created a SavingAccountService in your test that uses injected mocks.

@InjectMocks
private SavingAccountService savingAccountService;

The problem is that this is not the service used by your controller. Spring boot test creates the beans defined in the application context, autowires them, and happily ignores the existence of the service defined in test.

Solution

You must make Spring boot aware of the fixed-time Clock bean

Option 1: Mock bean

You define

@MockBean
private Clock clock;
private Clock fixedClock;

and you should be good to go.

I still find this method convoluted, I'd like to pass the fixed clock as a bean to Spring Boot context, instead of creating mocks.

Option 2: Specify the component classes to use for loading an ApplicationContext.

Create a new config class in your test directories

@Configuration
public class FakeClockConfig {

    private final static LocalDate WEEKEND = LocalDate.of(2020, 07, 05);

    @Bean
    public Clock clock() {
        return Clock.fixed(WEEKEND.atTime(9, 5).toInstant(ZoneOffset.UTC), ZoneId.of("CET"));
    }
}

Let Spring Boot test know about this additional config

@SpringBootTest(webEnvironment = RANDOM_PORT, 
                classes = {ChallengeApplication.class, FakeClockConfig.class})

I find this method preferable, you are already specifying one component class yourself.

Constant time clock will replace your original Clock

Option 3: @TestConfiguration

See Spring boot – @TestConfiguration

@TestConfiguration is specialized form of @Configuration that can be used to define additional beans or customizations for a test.

In spring boot, any beans configured in a top-level class annotated with @TestConfiguration will not be picked up via component scanning. We must explicitly register the @TestConfiguration class with the class that contains the test cases.

There are two ways to include this additional test configuration for tests:

1.1. @Import annotation

1.2. Static nested classes

Let's go with the latter approach:

@SpringBootTest(properties = "spring.main.allow-bean-definition-overriding=true")
public class ProjetRepositoryTest {

    private static final LocalDate WEEKEND = LocalDate.of(2020, 07, 05);

    @TestConfiguration
    static class FakeClockConfig {

        @Bean
        public Clock clock() {
            return Clock.fixed(WEEKEND.atTime(9, 5).toInstant(ZoneOffset.UTC), ZoneId.of("CET"));
        }
    }
}

Note that this method creates additional beans, so I needed to allow bean overriding.

See Spring-Boot 2.1.x and overriding bean definition

Other remarks

You seem to be using TestRestTemplate for backend tests. You may prefer to use MockMvc instead.

See Difference between MockMvc and RestTemplate in integration tests

Lesiak
  • 22,088
  • 2
  • 41
  • 65
  • Thank you very much for your answer! With the first method, it is still use the Clock.now(). I will try the second method. – liviubiur Jul 10 '20 at 09:12
  • The second method is working but the problem is that. To create a Bean on the test configurations class, I need to remove the bean from the SpringBootApplication class and then the project isn't starting anymore `Parameter 2 of constructor in SavingAccountService required a bean of type 'Clock' that could not be found`. And if I try to add the @Configuration annotation to the SpringBootClass then the project can run but the test can't run anymore because of that`Invalid bean definition with name 'clock' defined in com.ingtech.challenge.MockClockConfig: Cannot register bean definition` – liviubiur Jul 10 '20 at 09:33
  • I use method 2 in my current project. I have a separate ClockConfig in the src tree, and FakeClockConfig in test tree. Thus, a Clock is available for app and for tests – Lesiak Jul 10 '20 at 09:40
  • I just created a @Configuration ClockConfig in the src tree with this content `@Bean public Clock clock() { return Clock.systemDefaultZone(); }` and removed the bean from the SpringBootApplication but the error persist. – liviubiur Jul 10 '20 at 09:49
  • How do you discover your config? Manual or default? If default, did you place it in a sub-package of package containing the app class? – Lesiak Jul 10 '20 at 09:51
  • Solved! I forgot to add to the application.yaml that `spring: main: allow-bean-definition-overriding: true` Thank you very much for your time and for your help! You teach me a lot! – liviubiur Jul 10 '20 at 09:52
  • I have another question regarding the test. I want to have another test when I test a different day then the one of `WEEKEND`. How can I do that? Because I guess I can't create another @Bean on the FakeClockConfig – liviubiur Jul 10 '20 at 09:55
  • I would create 2 different fakeclockconfig classes, and select the appropriate one in each test case. – Lesiak Jul 10 '20 at 09:57
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/217593/discussion-between-liviubiur-and-lesiak). – liviubiur Jul 10 '20 at 10:00