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

Added unit test to usage metrics endpoints #1133

Merged
merged 11 commits into from
Jul 24, 2024

Conversation

jfkonecn
Copy link
Contributor

@jfkonecn jfkonecn commented Jul 9, 2024

I created a private MockS3Data class with a files property which generates and stores the mock JSON S3 files and the expected responses from all the endpoints. I hard coded a seed for the random number generator so that the tests always have the same data passed to them which should make it easier to debug any breaks.

I made the class private since it seemed unnecessary to create file for a class that is only needed once.

I think I may have caught some bugs with some endpoints, but I opted to maintain the same behavior so the current expected response data may not look quite right. My goal is to make it obvious what the behavior changes are when I make the 3 year timeline change in UsageAnalysisController.

@jfkonecn jfkonecn requested a review from zhx828 July 9, 2024 14:26
@jfkonecn jfkonecn marked this pull request as ready for review July 9, 2024 14:26
Copy link
Member

@zhx828 zhx828 left a comment

Choose a reason for hiding this comment

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

Not completed done with my review but like to hear from you on my comments before moving on further. Thanks!

@@ -35,6 +35,7 @@ public final class Constants {

public static final String MONTH_USERS_USAGE_SUMMARY_FILE_PREFIX = "public-website/usage-analysis/month-user-summary_";
public static final String YEAR_USERS_USAGE_SUMMARY_FILE_PREFIX = "public-website/usage-analysis/year-user-summary_";
public static final String MONTH_RESOURCES_USAGE_SUMMARY_FILE_PREFIX = "public-website/usage-analysis/year-resource-summary_";
Copy link
Member

Choose a reason for hiding this comment

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

Do you mean to use month-resource-summary_?

Comment on lines 207 to 208
public final X first;
public final Y second;
Copy link
Member

Choose a reason for hiding this comment

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

Give some meaningful names here? It's used by this test anyway, may as well just use lastKey and jsonObject?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's use by the logic that creates the mock json file, but I'll change it to KeyValuePair

}

private User createMockUser(
int userIndex,
Copy link
Member

Choose a reason for hiding this comment

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

Nit picking, should this be userId?

Comment on lines +740 to +741
user.setFirstName(String.valueOf(userName.charAt(userName.length() - 1)));
user.setLastName(userName.substring(0, userName.length() - 1));
Copy link
Member

Choose a reason for hiding this comment

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

I plan to make a comment about the logic of parsing username to F/L. But I guess it doesn't matter because we don't really care how user is named?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It doesn't matter too much, but the way I have the user name setup is last name ending with the first initial. So doej break down into doe and j.

The only reason why it sort of matter is that if the data is too random then it can be challenging to debug a test so I wanted to have realistic names.

return userDto;
}

private CompanyDTO createMockCompany(int companyIndex, String companyName) {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
private CompanyDTO createMockCompany(int companyIndex, String companyName) {
private CompanyDTO createMockCompany(int companyId, String companyName) {

return sampleUrls[Math.abs(random.nextInt() % sampleUrls.length)];
}

private Tuple<String, JsonObject> fetchJsonObjectFromFilesObject(
Copy link
Member

Choose a reason for hiding this comment

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

I'm a bit confused about this method. This is to fetch the last key json but backfill all keys if they do not exist in obj? Maybe add some comments 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.

That is correct, but I'll add some comments.

@jfkonecn jfkonecn added the feature Feature tag for release label Jul 18, 2024
}

// update expected response for user usage endpoint
if (isThisYear) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@zhx828 I know limiting the usage metrics to only "this" year isn't what we want, but it is currently how the code works.

Copy link
Member

Choose a reason for hiding this comment

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

You expect to change this after finishing "3 years" requirement to make the test pass?

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

@jfkonecn jfkonecn requested a review from zhx828 July 19, 2024 13:15
Copy link
Member

@zhx828 zhx828 left a comment

Choose a reason for hiding this comment

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

All places where you do keys[keys.length - 1], you should expect out of bound exception. I left a comment asking whether this is your intention. If not, we need to handle it. Out of bound exception is not the best way to show why the test fail I suppose?

And thank you for wrapping logics into methods, so much easier to review!

Comment on lines 419 to 421
* @param value The value that is going to be added to value located in the JSON path.
* @param filePath First key in the files object.
* @param index Array index of the array the {@code filePath} parameter points to.
Copy link
Member

Choose a reason for hiding this comment

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

These should be deleted I suppose.

* Get value from files JSON object based on the JSON path created by the
* {@code filePath}/[{@code index}]/{@code keys}.
* If an object is missing in the JSON path then it's created.
* @param filePath First key in the files object.
Copy link
Member

Choose a reason for hiding this comment

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

Is the description for filePath accurate?

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, I'll rename it from filePath to firstKey

}
obj = (JsonObject) obj.get(key);
}
String lastKey = keys[keys.length - 1];
Copy link
Member

Choose a reason for hiding this comment

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

Do you intend to let the test fails by throwing out of bound exception when no key been added to this method?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It shouldn't happen since this code is just generating the mock data. I don't think people will touch this code very often, but I'll update the methods so that this exception cannot occur.


for (int i = 0; i < keys.length - 1; i++) {
String key = keys[i];
if (!obj.has(key)) {
Copy link
Member

Choose a reason for hiding this comment

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

When key doesn't exist, should we directly return 0 instead of continuing?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Basically I'm doing the equivalent of a mkdir -p here. I want to make sure the entire path exists and also get the value from the path.


String yearKey = String.valueOf(year);

for (int month = 1; month <= 12; month++) {
Copy link
Member

Choose a reason for hiding this comment

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

Interesting testing stratagy. I'm not testing expert. But would dynamic mock data introduces unpredictability?

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, but I'm hard coding the seed for the random number generator so the data is always the same unless the calls to the random number change which would only happen if we make changes to the mock data.

I thought about making just a bunch of JSON files, but I felt like this would be easier to make changes to.

That said, I could switch this to reading a bunch of JSON files instead of dynamically generating the mock data if you feel it's better to maintain.

Copy link
Member

Choose a reason for hiding this comment

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

The data is not always the same because you also use today as timestamp. The file will be different every day. Is that ok given the use case here? If it's ok, fine to me to keep the current logic.

Copy link
Contributor Author

@jfkonecn jfkonecn Jul 23, 2024

Choose a reason for hiding this comment

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

I forgot about that part, but since the endpoint logic is dependent on today's date there isn't a way I can think of that wouldn't require changes to UsageAnalysisController. You'd have to mock out all library calls to java.time.

I don't think it's worth mocking that logic since this will only shift the data daily.

In my experience randomness is only worth addressing if it makes debugging tests harder or the tests randomly fail but failure cannot be reproduced.

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 figured it out. I can use the built-in java clock object.

* @param keys The keys in JSON path
* @return The resulting {@code KeyValuePair}
*/
private KeyValuePair<String, JsonObject> fetchJsonObjectFromFilesObject(
Copy link
Member

Choose a reason for hiding this comment

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

Two fetchJsonObjectFromFilesObject methods have the same name and same return but produce completely different JSONObject, one with last index included and set the initial value and one without last index.
Can we maybe update the comment and even the method name?

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'll remove the logic to set the initial value to avoid confusion.

Comment on lines +319 to +321
if (!obj.has(lastKey)) {
obj.addProperty(lastKey, 0);
}
Copy link
Member

Choose a reason for hiding this comment

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

Why isn't this part of fetchJsonObjectFromFilesObject? The other fetchJsonObjectFromFilesObject method does have 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.

It's not really needed in this method so I'll remove it to avoid confusion.

* @param value Sets the specified value at the given JSON path.
* @param keys The keys in JSON path.
*/
private void safeSetNestedValueInFilesObject(String value, String... keys) {
Copy link
Member

Choose a reason for hiding this comment

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

You added safe here. Do you imply a logic which could be not safe?

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 called it safe because if there are missing objects then they are created for you instead of an exception being thrown.

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'll change fetchJsonObjectFromFilesObject to safeFetchJsonObjectFromFilesObject

String usageResourcesEndpoint = "/api/usage/resources";
safeAddNestedValueInFilesObject(
value,
usageResourcesEndpoint + "?endpoint=" + oncokbEndpoint,
Copy link
Member

Choose a reason for hiding this comment

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

This can be a constant within this method.

}

@Test
public void shouldGetUsageSummaryResources() throws Exception {
Copy link
Member

Choose a reason for hiding this comment

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

Am I correct to say these tests are to test whether the endpoints will return previously generated files properly?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah they're to test what the current behavior of the endpoints are.

@jfkonecn jfkonecn requested a review from zhx828 July 22, 2024 15:25
*/
private KeyValuePair<String, JsonObject> safeFetchJsonObjectFromFilesObject(
String firstKey,
int index,
Copy link
Member

Choose a reason for hiding this comment

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

Is it accurate to say this method is safeFetchJsonObjectFromFilesJsonArray? Also do you assume all values of restOfTheKeys are JsonObject? Is there a way we can include number as key in restOfTheKeys and get JsonArray when the key is number? A bit risky to assume that I suppose.

Copy link
Contributor Author

@jfkonecn jfkonecn Jul 23, 2024

Choose a reason for hiding this comment

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

Kind of. It's assuming the first key points to an array of JSON objects.

Yes, it is assumed that the rest of the values are JSON objects.

It is risky to assume there will be an array, but it's only being used for this test code. The class is also scoped to just this test class so there isn't a risk someone will use it outside of this class unless they change the code. I don't it's worth making this code more generic if this is the only place it's used.

If we want absolute type safety then I should switch to classes instead of building out JSON.

Comment on lines +302 to +312
for (int i = 0; i < restOfTheKeys.length - 1; i++) {
String key = restOfTheKeys[i];
if (!obj.has(key)) {
obj.add(key, new JsonObject());
}
obj = (JsonObject) obj.get(key);
}
String lastKey = restOfTheKeys.length == 0
? firstKey
: restOfTheKeys[restOfTheKeys.length - 1];
return new KeyValuePair<>(lastKey, obj);
Copy link
Member

Choose a reason for hiding this comment

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

This is the dup of 263-275

Copy link
Member

@zhx828 zhx828 left a comment

Choose a reason for hiding this comment

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

Left few comments.

@jfkonecn jfkonecn requested a review from zhx828 July 23, 2024 13:47

@Bean
public Clock clock() {
return Clock.systemDefaultZone();
Copy link
Member

Choose a reason for hiding this comment

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

Can we make sure this is UTC, if not we should make it be.

@jfkonecn jfkonecn requested a review from zhx828 July 24, 2024 17:00
@jfkonecn jfkonecn merged commit 43fab0d into oncokb:master Jul 24, 2024
6 checks passed
@jfkonecn jfkonecn deleted the feature/usage branch July 24, 2024 18:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature Feature tag for release
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants