Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: framework for testing rpc #454

Closed
wants to merge 8 commits into from

Conversation

dmitry-fa
Copy link
Contributor

The suggested approach allows us to add new Storage RPC calls covered by unit tests.
The existing RPC calls could be also covered by unit tests.

Fixes #450

@google-cla google-cla bot added the cla: yes This human has signed the Contributor License Agreement. label Aug 5, 2020
@codecov
Copy link

codecov bot commented Aug 5, 2020

Codecov Report

Merging #454 (73b6f6e) into master (0ce82cc) will increase coverage by 2.00%.
The diff coverage is n/a.

Impacted file tree graph

@@             Coverage Diff              @@
##             master     #454      +/-   ##
============================================
+ Coverage     63.12%   65.12%   +2.00%     
- Complexity      610      618       +8     
============================================
  Files            32       32              
  Lines          5133     5144      +11     
  Branches        490      492       +2     
============================================
+ Hits           3240     3350     +110     
+ Misses         1729     1630      -99     
  Partials        164      164              
Impacted Files Coverage Δ Complexity Δ
...src/main/java/com/google/cloud/storage/Bucket.java 82.20% <0.00%> (+0.12%) 34.00% <0.00%> (ø%)
...main/java/com/google/cloud/storage/BucketInfo.java 82.01% <0.00%> (+0.27%) 86.00% <0.00%> (+3.00%)
...om/google/cloud/storage/spi/v1/HttpStorageRpc.java 3.62% <0.00%> (+2.02%) 5.00% <0.00%> (+3.00%)
...gle/cloud/storage/testing/RemoteStorageHelper.java 64.51% <0.00%> (+2.41%) 9.00% <0.00%> (ø%)
...va/com/google/cloud/storage/spi/v1/StorageRpc.java 68.83% <0.00%> (+2.59%) 0.00% <0.00%> (ø%)
...ogle/cloud/storage/spi/v1/HttpStorageRpcSpans.java 100.00% <0.00%> (+100.00%) 2.00% <0.00%> (+2.00%)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 0ce82cc...73b6f6e. Read the comment docs.

@dmitry-fa dmitry-fa requested review from elharo and frankyn August 5, 2020 10:47

// Objects for a test case: one object per one RPC mock.
// A test creates as many MockData objects as many RPC call it issues.
private static final Queue<MockData> TEST_MOCK_DATA = new LinkedList<>();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ArrayDeque instead of LinkedList

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

request.getStreamingContent().writeTo(outputStream);
GZIPInputStream is =
new GZIPInputStream(new ByteArrayInputStream(outputStream.toByteArray()));
byte[] buffer = new byte[1000000];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't safe or correct. Use ByteStreams.toByteArray(is)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, agree. Forgot to fix this before PR. Done.

private static final StorageRpcFactory RPC_FACTORY_MOCK =
mock(StorageRpcFactory.class, UNEXPECTED_CALL_ANSWER);

private static class TestRequest extends LowLevelHttpRequest {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make this an outer class for clarity

}
};

private static class TestResponse extends LowLevelHttpResponse {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make this an outer class for clarity


// Objects for a test case: one object per one RPC mock.
// A test creates as many MockData objects as many RPC call it issues.
private static final Queue<MockData> TEST_MOCK_DATA = new ArrayDeque<>();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this static? That's likely to cause coupling between tests. This can probably be an instance field.

private HttpStorageRpc rpc;

// Objects for a test case: one object per one RPC mock.
// A test creates as many MockData objects as many RPC call it issues.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as it issues RPC calls

}
}

static class MockData {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

outer class

@Before
public void setUp() throws Exception {
rpc = new HttpStorageRpc(STORAGE_OPTIONS);
TEST_MOCK_DATA.clear();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, this really should be an instance variable so you don't have to worry about clearing it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

refactored to be an instance variable

@@ -51,13 +51,122 @@
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

class TestRequest extends LowLevelHttpRequest {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant these should be in new files.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure? These classes unlikely will be used by anyone else but HttpStorageRpcTest.
I can move them to separate files if you insist, but IMHO it will only make understanding harder.
If it is just a matter of taste, I would prefer to see them as inner classes.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if there's an explicit style rule but Google almost never uses non-public outer classes in the same .java file.

In general inner classes encourage violation of data encapsulation and the single responsibility principle.

And perhaps most importantly here, the sheer size of a file becomes a problem. I found this harder to read as one large class than as several smaller classes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the explanation. I'll move the classes with code to new files and, if you don't mind, keep the trivial classes (MockData and RpcRequestHolder) as inner.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All inner classes moved to new files

new GZIPInputStream(new ByteArrayInputStream(outputStream.toByteArray()));
return new String(ByteStreams.toByteArray(gzipInputStream), UTF_8);
} catch (IOException e) {
throw new Error(e);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RuntimeException, or better yet just bubble the IOException

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I selected Error because An Error is a subclass of Throwable that indicates serious problems that a reasonable application should not try to catch. Most such errors are abnormal conditions.
IOException thrown from this try-catch block will signal about problems in the core Java libraries, not in the Google libraries under test.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I replaced Error with AssertionError which JUnit.fail() throws. I can't replace with fail() because the method returns a value.

new GZIPInputStream(new ByteArrayInputStream(outputStream.toByteArray()));
return new String(ByteStreams.toByteArray(gzipInputStream), UTF_8);
} catch (IOException e) {
throw new AssertionError("Unexpected exception", e);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just bubble the IOException, no need to catch it at all

@product-auto-label product-auto-label bot added the api: storage Issues related to the googleapis/java-storage API. label Aug 21, 2020
@dmitry-fa dmitry-fa requested a review from elharo August 24, 2020 16:16
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
request.getStreamingContent().writeTo(outputStream);
GZIPInputStream gzipInputStream =
new GZIPInputStream(new ByteArrayInputStream(outputStream.toByteArray()));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since you already have a byte array use an inflater, not a stream

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you mind if I kept using GZIPInputStream? java.util.zip.Inflater introduces new DataFormatException the test should handle. The inflater requires a byte buffer of a certain size, but the size is unknown. And I think, I can't use the java.util.zip.Inflater from the box to decompress GZIP content:

 To fully decode the gzip format with this class, you will need to process the gzip header as described in RFC 1952, use inflater to decompress the raw deflate data, compute the crc32 of the uncompressed data using that class, and then verify the crc and length (modulo 2^32) in the gzip trailer, again as specified in the RFC.

Or do you mean another inflater?

Copy link
Contributor

@elharo elharo Aug 24, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, it's weird to copy this into a byte array first. You should be able to go straight from the LowLevelHttpRequest to the decompressor, but LowLevelHttpRequest doesn't offer an input stream to read from, which is really strange. Maybe use a piped stream but that would need an extra thread.


// Objects for a test case: one object per one RPC mock.
// A test creates as many MockData objects as it issues RPC calls.
private Queue<MockData> testMockData;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can initialize the queue here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are you sure it's necessary? This queue is re-initialized before each test case.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not necessary, just a little cleaner. When possible, it's good habit to initialize and declare in the same line to avoid unexpected failure initialize or reuse of variables

Copy link
Contributor

@elharo elharo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM


// Objects for a test case: one object per one RPC mock.
// A test creates as many MockData objects as it issues RPC calls.
private Queue<MockData> testMockData;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not necessary, just a little cleaner. When possible, it's good habit to initialize and declare in the same line to avoid unexpected failure initialize or reuse of variables

ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
request.getStreamingContent().writeTo(outputStream);
GZIPInputStream gzipInputStream =
new GZIPInputStream(new ByteArrayInputStream(outputStream.toByteArray()));
Copy link
Contributor

@elharo elharo Aug 24, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, it's weird to copy this into a byte array first. You should be able to go straight from the LowLevelHttpRequest to the decompressor, but LowLevelHttpRequest doesn't offer an input stream to read from, which is really strange. Maybe use a piped stream but that would need an extra thread.

@dmitry-fa dmitry-fa requested a review from elharo August 24, 2020 20:14
@dmitry-fa dmitry-fa added the kokoro:force-run Add this label to force Kokoro to re-run the tests. label Nov 25, 2020
@yoshi-kokoro yoshi-kokoro removed the kokoro:force-run Add this label to force Kokoro to re-run the tests. label Nov 25, 2020
@frankyn
Copy link
Contributor

frankyn commented Jun 11, 2021

Closing for now, will consider in the future at a later time.

@frankyn frankyn closed this Jun 11, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api: storage Issues related to the googleapis/java-storage API. cla: yes This human has signed the Contributor License Agreement.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Need a test framework for Storage RPC tests
4 participants