-1

I have what I think is a pretty standard StorageService that exists in a Spring Boot app for a web API.

@Component
@Service
@Slf4j
public class StorageService {
    @Autowired
    private AmazonS3 s3Client;

    @Autowired
    private RestTemplate restTemplate;

    @Value("${app.aws.s3.bucket}")
    private String bucket;

    @Async
    public boolean fetchAndUpload(List<URI> uris) {
        List<CompletableFuture<PutObjectResult>> futures = uris.stream().map(uri ->
                fetchAsync(uri).thenApplyAsync((asset) -> put(getName(uri.toString()), asset))
        ).collect(Collectors.toList());

        CompletableFuture.allOf(futures.toArray(new CompletableFuture[futures.size()])).join();

        return true;
    }

    @Async
    private CompletableFuture<InputStream> fetchAsync(URI uri) {
        return CompletableFuture.supplyAsync(() -> {
            InputStream resp;
            try {
                // asdf is null here when running my unit tests
                Resource asdf = restTemplate.getForObject(uri, Resource.class);
                resp = Objects.requireNonNull(asdf).getInputStream();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
            return resp;
        });
    }

    private PutObjectResult put(String name, InputStream data) {
        PutObjectRequest request = new PutObjectRequest(bucket, name, data, new ObjectMetadata());
        return s3Client.putObject(request);
    }
}

Here is an integration test, which at minimum successfully fetches the images given by the integration test:

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureWebClient
public class StorageServiceIT {
    @Value("${app.aws.s3.access.key}")
    private String accessKey;

    @Value("${app.aws.s3.secret.key")
    private String secretKey;

    @Spy
    private AmazonS3 s3Client = AmazonS3ClientBuilder.standard()
            .withCredentials(new AWSStaticCredentialsProvider(new BasicAWSCredentials(accessKey, secretKey)))
            .withRegion(Regions.US_EAST_1)
            .build();

    @Spy
    private RestTemplate restTemplate = new RestTemplateBuilder().build();

    @MockBean
    private SignService signingService;

    @Autowired
    private StorageService storageService;

    @Before
    public void setUp() throws Exception {
        MockitoAnnotations.initMocks(this);
    }

    @Test
    public void fetchAsync() throws URISyntaxException {
        List<URI> uris = List.of(
                new URI("https://upload.wikimedia.org/wikipedia/commons/e/ec/Mona_Lisa%2C_by_Leonardo_da_Vinci%2C_from_C2RMF_retouched.jpg"),
                new URI("https://upload.wikimedia.org/wikipedia/commons/e/ec/Mona_Lisa%2C_by_Leonardo_da_Vinci%2C_from_C2RMF_retouched.jpg")
        );

        storageService.fetchAndUpload(uris);
    }
}

However, the following unit test does not successfully mock the restTemplate.getForObject call, it constantly returns null, even when setting both arguments to any().

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureWebClient
public class StorageServiceTest {
    @MockBean
    private AmazonS3 s3Client;

    @MockBean
    private RestTemplate restTemplate;

    @MockBean
    private SignService signingService;

    @Autowired
    // @InjectMocks ???
    private StorageService storageService;

    @Value("${app.aws.s3.bucket}")
    private String bucket;

    @Before
    public void setUp() throws Exception {
        MockitoAnnotations.initMocks(this);
    }

    List<URI> randomUris(int num) {
        final String base = "https://example.com/%s";
        return Stream.iterate(0, i -> i)
                .limit(num)
                .map(o -> URI.create(String.format(base, UUID.randomUUID().toString())))
                .collect(Collectors.toList());
    }

    @Test
    public void fetchAsyncTest() {
        List<URI> uris = randomUris(2);

        uris.forEach(uri -> {
            ByteArrayInputStream data = new ByteArrayInputStream(
                    Integer.toString(uri.hashCode()).getBytes());
            PutObjectRequest request = new PutObjectRequest(
                    bucket, getName(uri.toString()), data, new ObjectMetadata());

            Resource getResult = mock(Resource.class);
            try {
                doReturn(data).when(getResult).getInputStream();
            } catch (IOException e) {
                e.printStackTrace();
            }

            doReturn(data).when(restTemplate).getForObject(any(), any());

//          none are working
//          doReturn(data).when(restTemplate).getForObject(eq(uri), eq(Resource.class));
//          doReturn(data).when(restTemplate).getForObject(uri, Resource.class);
//          when(restTemplate.getForObject(uri, Resource.class)).thenReturn(data);
//          when(restTemplate.getForObject(eq(uri), eq(Resource.class))).thenReturn(data);
//          when(restTemplate.getForObject(any(), any())).thenReturn(data);

            PutObjectResult res = new PutObjectResult();
            doReturn(res).when(s3Client).putObject(eq(request));

//            not working here as well, i think
//            doReturn(res).when(s3Client).putObject(request);
//            doReturn(res).when(s3Client).putObject(any());
//            when(s3Client.putObject(eq(request))).thenReturn(res);
//            when(s3Client.putObject(request)).thenReturn(res);
//            when(s3Client.putObject(any())).thenReturn(res);

        });

        boolean res = storageService.fetchAndUpload(uris);
    }
}

And just in case it's relevant, this is how I'm building the RestTemplate:

  @Bean
  public RestTemplate restTemplate(RestTemplateBuilder builder) {
    return builder.build();
  }

I'm stumped, all advice is appreciated! :|

Michael Petch
  • 46,082
  • 8
  • 107
  • 198
Justin
  • 2,692
  • 2
  • 18
  • 24
  • while using `@SpringBootTest` you don't need `@InjectMocks` or `MockitoAnnotations.initMocks(this);` – Ryuzaki L Sep 20 '19 at 19:27
  • ahh, thanks. still seeing the mocking issue though – Justin Sep 20 '19 at 19:30
  • and also your code is really mess in `fetchAsyncTest` can you please clean it all the way and also please stick to one approach that is not mocking. – Ryuzaki L Sep 20 '19 at 19:30
  • In which class do you have problem? this one `StorageServiceIT` or `StorageServiceTest`? – Ryuzaki L Sep 20 '19 at 19:32
  • the problem is in `StorageServiceTest`. `StorageServiceIT` successfully downloads the images. the messiness/commented code is just to show that all of those versions were also failing – Justin Sep 20 '19 at 19:33
  • Is restTemplate not null at: doReturn(data).when(restTemplate).getForObject(any(), any()); –  Sep 20 '19 at 21:44
  • it isn't null - i can see that it's a Mockito mock – Justin Sep 21 '19 at 01:45
  • You might want to check my basic mockito example for `restTemplate.getObjectFor` [`here`](https://stackoverflow.com/a/57394411/11514534). Also verify that the `restTemplate` mock is indeed injected into your class under test. As your `@InjectMocks` is commented (and you used `@MockBean` instead of `@Mock`, I doubt `Spring` does that for you automatically. – second Sep 21 '19 at 17:52
  • Also try to test it only with one `uri`. You override parts of your mocking behaviour when you do it twice (like it is currently implemented). I dont think that is your intention. – second Sep 21 '19 at 18:03

1 Answers1

0

To follow up on this - my problem was because the method I was attempting to test was marked with Spring Boot's @Async annotation, causing race conditions and some mocks to not be configured properly.

Justin
  • 2,692
  • 2
  • 18
  • 24