ababuji
ababuji

Reputation: 1731

Mock an instance method call of another class that takes a URL and File object and writes a file to a HTTP connection

I'm trying to write UTs for a file called DocumentStoreAccessor.java. Here is the class below:

package com.company.main.accessor;

import com.company.main.dagger.component.AccessorComponent;
import com.company.main.dagger.component.DaggerAccessorComponent;
import com.company.main.util.aws.s3.AWSS3Util;

import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;

@Slf4j
@RequiredArgsConstructor
public class DocumentStoreAccessor {

    private final DocumentStore documentStoreClient; //Comes from Dagger

    
    public DocumentStoreAccessor() {
        AccessorComponent accessorComponent = DaggerAccessorComponent.create();
        this.documentStoreClient = accessorComponent.provideDocumentStoreClient();
  
    }

    private int putContentsIntoS3(CreateUploadS3UrlResult createUploadS3UrlResponse,
                                  @NonNull File file) {

        int uploadStatusCode = 0;
        try {
            URL url = new URL(createUploadS3UrlResponse.getS3Url());
            uploadStatusCode = new AWSS3Util().upload(url, file); //Instance comes from a util class

        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        
        return uploadStatusCode;
    }

    public String uploadFile(File file, DocumentFileExtension fileExtension) throws Exception {

        String documentId = null;
        CreateUploadS3UrlResult createUploadS3Urlresult = documentStoreClient.createUploadS3Url(new CreateUploadS3UrlRequest());
        int putContentsStatusCode = putContentsIntoS3(createUploadS3Urlresult, file);
        if (putContentsStatusCode == 200) {
            CreateDocumentRequest createDocumentRequest = new CreateDocumentRequest()       
            CreateDocumentResult document = documentStoreClient.createDocument(createDocumentRequest);
            documentId = document.getDocumentId();
        } else {
            throw new Exception("Status code is: " + putContentsStatusCode);
        }
        return documentId;
    }
}

Inside this file I do new AWSS3Util().upload(url, file).

And here is the AWSS3Util.java

package com.company.main.util.aws.s3;

import com.company.main.exception.DataAccessException;
import com.company.main.exception.RetriableDataAccessException;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;

@Slf4j
@AllArgsConstructor
@Builder
public class AWSS3Util {

    private static final String PUT_REQUEST_METHOD = "PUT";

    public int upload(@NonNull final URL url, @NonNull final File file) throws IOException {

        final InputStream inputStream = new FileInputStream(file);
        final Reader fileReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
        final BufferedReader br = new BufferedReader(fileReader);
        HttpURLConnection connection = null;
        int responseCode = 0;
        try {
            // Create the connection and use it to upload the new object using the pre-signed URL.
            connection = create(url);
            connection.setDoOutput(true);
            connection.setRequestMethod(PUT_REQUEST_METHOD);
            final OutputStreamWriter out = new OutputStreamWriter(connection.getOutputStream(), StandardCharsets.UTF_8);
            String st;
            while ((st = br.readLine()) != null) {
                out.write(st);
            }
            out.close();
            responseCode = connection.getResponseCode();

        } catch (IOException e) {
            throw new RetriableDataAccessException(String.format("S3 upload request failed with request: %s", url.toString()), e);
        } finally {
            inputStream.close();
            fileReader.close();
            br.close();
        }
        return responseCode;
    }

    private HttpURLConnection create(URL url) throws IOException {
        return (HttpURLConnection) url.openConnection();
    }
}

I want to make new AWSS3Util().upload(url, file) return a 200..

I'm unable to do so.. I keep getting a NullPointerException. Here is what I have for the past day:

package com.company.main.accessor;

import com.company.main.util.aws.s3.AWSS3Util;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
public class DocumentStoreAccessorTest {

    @Mock
    private DocumentStore mockDocumentStoreClient;

    @Mock
    private HttpURLConnection httpURLConnection;

    @Mock
    private OutputStream outputStream;

    @InjectMocks
    private DocumentStoreAccessor classUnderTest;

    private URL url;
    private AWSS3Util awss3Util;
    private CreateUploadS3UrlRequest dummyCreateUploadS3UrlRequest;
    private CreateUploadS3UrlResult  dummyCreateUploadS3UrlResult;
    private CreateDocumentRequest dummyCreateDocumentRequest;
    private CreateDocumentResult dummyCreateDocumentResult;

    @BeforeEach
    void setUp() throws IOException {

        awss3Util = new AWSS3Util();

        url = getMockUrl(httpURLConnection);

        dummyCreateUploadS3UrlRequest = new CreateUploadS3UrlRequest();
        dummyCreateUploadS3UrlResult = new CreateUploadS3UrlResult().withS3Url("http://foo.io://:99");

        dummyCreateDocumentRequest = new CreateDocumentRequest();
        dummyCreateDocumentResult = new CreateDocumentResult().withDocumentId("foo");
    }


    @Test
    void uploadSuccess() throws IOException {


        when(mockDocumentStoreClient.createUploadS3Url(dummyCreateUploadS3UrlRequest)).thenReturn(dummyCreateUploadS3UrlResult);
        AWSS3Util aSpy = Mockito.spy(awss3Util);
        Mockito.when(aSpy.upload(url, getDataToUpload())).thenReturn(200);

        when(mockDocumentStoreClient.createDocument(dummyCreateDocumentRequest)).thenReturn(dummyCreateDocumentResult);

        String id = classUnderTest.uploadFile(getDataToUpload(), DocumentFileExtension.XLSX);
        assertEquals(id, "foo");
        verify(mockDocumentStoreClient, times(1)).createUploadS3Url(dummyCreateUploadS3UrlRequest);
    }

    private File getDataToUpload() {
        return new File("TestFileName.xlsx");
    }

    /**
     * We cannot directly use Mockito to mock URL. This helper method, helps us to create the mock url.
     * <p>
     * https://stackoverflow.com/questions/565535/mocking-a-url-in-java
     */
    private URL getMockUrl(HttpURLConnection httpURLConnection) throws IOException {
        final URLStreamHandler handler = new URLStreamHandler() {
            @Override
            protected URLConnection openConnection(final URL arg0)
                    throws IOException {
                return httpURLConnection;
            }
        };
        final URL url = new URL("http://foo.io", "foo.io", 80, "", handler);
        return url;
    }
}

Try incorporating Yan's comments:

@Test
void verifyCreateUploadS3UrlInvocation() throws Exception {
    when(mockDocumentStoreClient.createUploadS3Url(dummyCreateUploadS3UrlRequest)).thenReturn(dummyCreateUploadS3UrlResult);
    when(httpURLConnection.getOutputStream()).thenReturn(outputStream);
    when(httpURLConnection.getResponseCode()).thenReturn(200);


    when(awss3Util.upload(url, getDataToUpload())).thenReturn(200);

    when(mockDocumentStoreClient.createDocument(dummyCreateDocumentRequest)).thenReturn(dummyCreateDocumentResult);

    String id = classUnderTest.uploadFile(getDataToUpload(), DocumentFileExtension.XLSX);
    assertEquals(id, "foo");
    verify(mockDocumentStoreClient, times(1)).createUploadS3Url(dummyCreateUploadS3UrlRequest);
}

I get a 405 thrown when I specifically want it to send a 200, and therefore java.lang.Exception: Status code is: 405 thrown from my function.

Upvotes: 0

Views: 651

Answers (2)

ababuji
ababuji

Reputation: 1731

I had to violate the second rule - I had to DependencyInject it via Dagger

package com.company.main.dagger.module;

import com.company.main.util.aws.s3.AWSS3Util;
import dagger.Module;
import dagger.Provides;

import javax.inject.Singleton;

@Module
public class AWSS3UtilModule {
    @Provides
    @Singleton
    public static AWSS3Util provideAwss3Util() {
        return new AWSS3Util();
    }
}

and then in my component

import javax.inject.Singleton;

@Singleton
@Component(modules = {
        ShipDocsDMSAccessorModule.class,
        AWSS3UtilModule.class
})
public interface AccessorComponent {
    ShipDocsDMS provideShipDocsDMSClient();
    AWSS3Util provideAwss3Util();
}

and then in my main class

@Slf4j
@RequiredArgsConstructor
public class ShipDocsDMSAccessor {

    private final ShipDocsDMS shipDocsDMSClient;
    private final AWSS3Util awss3Util;

    public ShipDocsDMSAccessor() {
        AccessorComponent accessorComponent = DaggerAccessorComponent.create();
        this.shipDocsDMSClient = accessorComponent.provideShipDocsDMSClient();
        this.awss3Util = accessorComponent.provideAwss3Util();
    }
.
.
.
.

and then in my test, I can freely mock AWSS3Util dependency and carry ahead and force out the required response.

Upvotes: 0

Yan
Yan

Reputation: 328

Firstly I would change a little bit AWSS3Util, because reading binary files as string can lead to very interesting results.


import lombok.NonNull;

import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;

public class AWSS3Util {

    private static final String PUT_REQUEST_METHOD = "PUT";

    public int upload(@NonNull final URL url, @NonNull final File file) throws IOException {
        HttpURLConnection connection = create(url);
        try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file))) {
            connection.setDoOutput(true);
            connection.setRequestMethod(PUT_REQUEST_METHOD);

            try (BufferedOutputStream bos = new BufferedOutputStream(connection.getOutputStream())) {
                byte[] buffer = new byte[1024];
                int len;
                while ((len = bis.read(buffer)) > 0) {
                    bos.write(buffer, 0, len);
                }
            }

            return connection.getResponseCode();
        }
    }

    private HttpURLConnection create(URL url) throws IOException {
        return (HttpURLConnection) url.openConnection();
    }
}

Test for upload method could be like this:


import org.junit.jupiter.api.Test;

import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.nio.file.Files;

import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class MyTest {

    @Test
    public void test() throws IOException {
        final HttpURLConnection mockUrlCon = mock(HttpURLConnection .class);
        URLStreamHandler stubUrlHandler = new URLStreamHandler() {
            @Override
            protected URLConnection openConnection(URL u) throws IOException {
                return mockUrlCon;
            }
        };
        URL url = new URL("http://foo.io", "foo.io", 80, "", stubUrlHandler);

        when(mockUrlCon.getResponseCode()).thenReturn(200);

        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        when(mockUrlCon.getOutputStream()).thenReturn(outputStream);

        File file = new File("c:\\anyFile.png");
        int responseCode = new AWSS3Util().upload(url, file);

        assertEquals(200, responseCode);

        byte[] expectedBytes = Files.readAllBytes(file.toPath());
        byte[] actualBytes = outputStream.toByteArray();
        assertArrayEquals(expectedBytes, actualBytes);

    }
}

Upvotes: 1

Related Questions